APP下载

指针教学内容改革之一

2009-03-17王恒滨

计算机教育 2009年3期
关键词:指针C语言教学内容

文章编号:1672-5913(2009)02-0077-05

摘 要:C语言指针教学内容组织近乎千篇一律,除相关概念至今未见在教材中求精外,理论基础的介绍既不精准又不一致更不直观,大体停留在罗列各种使用情形上。本文选取了已投入教学实践并取得良好效果的自编讲义中的一段,以展示教改成果。其中的字面指针能使介绍直观化。

关键词:教学内容;C语言;指针

中图分类号:G642

文献标识码:B

1 基本指针类型

C语言中,指针是一种特殊类型的整型值,所以也说成是指针值。指针与地址不同,地址是反映计算机存储介质逻辑构造的物理概念。存储介质由字节组成,每个字节都有一定的序号(如0,1,2,3,…,100,101,…,1000,1001,……),这些连续的序号就是它们的物理地址。地址作为字节的序号,用整数就能表示。

然而仅仅地址并不能反映该地址处的若干字节是由什么类型的变量或数组所用,正像仅凭“长安大街100号”并不能指明该处是大商场还是小店铺,乃至是围有几栋楼的居民小区。为了描述“是由什么所用”这层含义,引入了指针概念,并给出了指针类型及其表达方法。

有了指针类型,指针就能由整型值地址转化而成,在程序需要指针时有计可施。

指针类型并不像结构体类型,需要专门做出类型声明,而只需在已有的类型名上添加运算符就能表达,甚至像数组类型之类,只需添加运算符[]和其中的整型常量值。

尽管有些指针类型的表达可能非常复杂,但这里仅讨论表达简洁的基本指针类型,即在已有的非数组类型后跟随星号。例如:

short * char * struct tag *

(假设struct tag是已声明的结构体类型,且全文有效)。

如果想把程序中出现的整数2000看成是存储介质序号2000字节的地址,就必须配合使用指针类型。例如:

① (short *)2000

② (char *)2000

③ (struct tag *)2000

都是合理的,参照-2000是字面负整数的说法,它们都可以认为是字面指针。此时,其中的2000代表地址,不过它们还有另一层含义,那就是地址2000处的若干字节,在①看来是用两个字节为short型变量所用;在②看来是用一个字节为char型变量所用,等。

1.1 指针

指针是整型值配上指针类型,此时的整型值代表地址。例如,假设k是整型变量,则

(short *)k (long *)(2*k+1)

(char *)2000 (struct tag *)3000

的结果都是指针。若k的值为1000,那么(short *)k与(long *)(2*k+1)分别等于

(short *)1000 与 (long *)2001

为了叙述方便,可以称:(1)指针类型星号前的类型为基类型,(2)类型后的整型值为指针地址,如下表:

1.2 指针变量

这种类型的变量可以用来存储指针,定义指针变量要用指针类型来完成。以下写出的都是指针变量的定义。

short * p1; char * p2;

struct tag * p3;

之后,p1、p2、p3便都是指针变量名。

使用赋值运算可以让指针变量存储指针,但类型应当一致。例如

p1 = (short *)k; p2 = (char *)2000;

p3 = (struct tag *)2000;

都是正确的(k的意义同上文)。

用指针变量来存储指针,不过是存储了指针地址,指针类型的信息全凭这个指针变量的类型体现。例如,p2存储的是2000,由于它是char * 型的指针变量,所以它的值为:

(char *)2000

指针变量对应内存的字节数,由编程环境中设置的模式决定,small模式等为2字节,large模式等为4字节。

定义指针变量的方法,可以动态地描述成:在已有变量定义的基础上,在变量名前加星号。例如

int p1, v, p2; => int *p1, v, *p2;

那么,p1,p2变成为指针变量。

注意,在星号的前后增减空格不会影响语义。

1.3 输出指针

凡是能够输出整型值的格式输出,都可以输出指针的指针地址。但不能忘记指针是在2字节的small模式下还是在4字节的large模式下。

%p格式的printf()专门用来输出指针的指针地址。不同的是,它能自动察觉程序完成时的编程环境是什么模式,以便相应地采取类似%x格式或%lx格式的输出,但相比之下输出形式略有差异:

●small模式时,输出地址的4位十六进制数字。

●large模式时,输出地址的8位十六进制数字,且两个4位之间用冒号隔开。

例如,

printf("%p", (int *)30);

在small模式下,将输出001e;在large模式下,将输出0000:001e

1.4 直接代表指针

1.4.1一维数组名代表指针

C语言的一维数组名和变量名,在语义上存在着本质区别。尽管它们都唯一地对应一块内存,并且内存块的首字节地址正是这种对应的确切反映。然而,在程序中简单地使用变量名意味着要向那块内存储存数据或要从中取得数据;简单地使用一维数组名却意味着仅仅要利用那块内存的首字节地址形成的有相同基类型的指针。亦即,指针的基类型就是数组的基类型。由于内存块的位置是确定的,所以经常看到的说法类似于:数组名是指针常量。

可以用printf("%p", 数组名);来输出数组对应内存块的首字节地址。这从一个侧面表明,尽管数组的内存是系统分配的,但我们还是可以知道它在什么地方。

1.4.2 字符串常量代表指针

在程序中,经常可以看到这样的写法:

p = "e:\turboc2\student.dat";

其中,p是char* 型的指针变量。实际上,系统要为这样的字符串常量专门开辟一块内存来存储它包含的字符。可是,它在程序中留下的可供直接使用的只有char* 型的指针,指针地址就是那块内存的首字节地址,所以才要用一个指针变量来保存,以便之后随时可用。

在此额外提一句,整型常量的存储位置很隐蔽,但幸好没必要知道。表达式计算的中间结果存储在什么地方也是隐蔽的。

有了以上两点认识,就可以这样说,%s格式的输出部分可以是指针值,只要指针地址处的若干字节存储了恰当的字符串。至于在输出部分处写的是字符串常量还是数组名,都无关紧要。

2 基本指针的运算

指针能参与的运算很有限。除赋值运算以外,还有其他七个种类。前四个种类分别是“*”、“[ ]”、“&”和强制指针类型转换运算。

2.1 指针的指针运算

* —— 除了用来构成指针类型之外,还可以出现在指针前,与指针一起构成一个基类型的变量。“*指针”变量的内存就是指针地址处的若干字节。它是单目运算符。

例如,*(int *)2000是一个int型的变量,这个表达式的值是这个变量的值,而不是2000。

再如,若an是int[100]型数组名,*an就为int型的变量,也就是an的第0号元素an[0]。

再如,*"Hello"是一个char型的变量,其中存储的是字符'H'。

2.2 指针的下标运算

[ ] —— 除用来构成数组类型之外,当其间包含有整型值并出现在指针后,将与指针结合成一个基类型的变量。“指针[整型值]”变量就是“*(指针+整型值)”变量。它是双目运算,需要一个指针和一个整型值。例如:

((int*)2000)[4] <=> *((int*)2000 + 4)

是一个int型的变量。

再如,若a是int[100]型数组名,那么

a[4] <=> *(a + 4)

是一个int型的变量,也就是a的第4号元素。请不必为先有鸡——指针a与4做[ ]运算为a[4],还是先有蛋——数组a的第4号元素为a[4]所困,只需记住:下标变量就是做了下标运算所得的变量。再如

"Hello"[4] <=> *("Hello"+4)

是一个char型的变量,存储的是字符'o'。

2.3 指针的求取运算

& —— 是取指针运算符。把它置于变量前,算出的是该变量的指针,这个指针以变量的类型为基类型,以变量内存的首字节地址为其指针地址。

例如,有如下变量定义,并假设它们的内存首字节地址分别为:1000,1002,1003

short v1; char v2; struct tag v3;

那么,&v1、&v2、&v3的值分别为:

(short *)1000 (char *)1002

(struct tag *)1003

2.4 强制指针类型转换运算

(指针类型)——跟其他强制类型转换运算符的构成一样,比如(short *)、(char *)……都是这样的运算符。把它们置于z指针或整型值之前,结果就是所要类型的指针。例如

(int *)"Hello"

就能从char*型的指针转换成int*型的指针,指针地址不变。再如,曾经写出的指针

(short *)k (long *)(2*k+1)

(char *)2000 (struct tag *)3000

其实分别为:

对k的值做了(short *)运算

对2*k+1的值做了(long *)运算

对2000做了(char *)运算

对3000做了(struct tag *)运算

作为类比

-k,-(2*k+1),-2000

分别为:对k、2*k+1和字面整数2000的值做了求负运算,等。

后三个种类分别是加减运算、关系运算和逻辑运算

2.5 指针的加减运算

i) 指针增减值:指针作为第一运算对象和整型值相加减。结果仍为同类型的指针,但指针地址加减了整型值的倍数,具体几倍要看基类型对应几个字节。例如

(short *)2000+10等于(short *)(2000+10*2) 即(short *)2020

ii) 两指针相减:类型相同的指针可以相减。结果的类型,若small模式为short,若large模式为long,得到的整型值等于指针地址之差除以基类型的字节数。例如

(long *)2000 – (long *)1800

=> (2000 – 1800) / 4 => 50

所以计算结果,或是50或是50L。

数组的两个元素的指针与它俩的下标之间的关系,用这类运算确定极为方便。

比如a是任意数组,i和j是两个下标,那么

&a[i] + j - i = &a[j]

&a[j] - &a[i] = j - i

特别地

a + i = &a[i] &a[i] - a = i

2.6 指针的比较运算

相同类型的指针可做比较运算。计算结果不是0就是1。比较仅仅使用指针地址。例如,

(int *)3 > (int *)2

的结果为1。

如果使用减法,把(int *)3 – (int *)2的结果为0作为(int*)3等于(int*)2的依据,就犯错误了!特别地,整数0可以跟任何类型的指针做比较。

2.7 指针的逻辑运算

不同类型的指针以及整型值之间,可以做逻辑运算,计算结果非0即1。指针的逻辑“真”、“假”仅以指针地址是否为0作为依据。

例如,假设有int* pi, v; char* pc;并且都存储了适当的值。那么

pi && v || pc

是正确的表达式。若它们存储的分别为(int*)0、10、(char*)20,则上式的计算过程为:

((int*)0 && 10) || pc => 0 || (char*)20 => 1

即结果为“真”。

指针运算和下标运算表明,C语言中存在着纯属表达式形式的变量。使用上,这种表达式的整体相当于变量名。

正是基于指针能够通过运算得到变量这一事实,便有了较直观的说法:指针指向变量。甚至用箭头来图示“指向”,方法如下图所示。其中假设a是一维数组、v是变量,p和q是指针变量,分别存储了指针&v和a(言下之意,p、q有适合的指针类型)。

注:箭头起始方框内是存储的指针,方框外给出的是变量名或相当于变量名的表达式。

必须心中有数,尽管指针值是多少并不会妨碍用指针运算或下标运算得到变量,然而这样得到的变量,其内存是否靠得住并没有论及。这和《建筑施工手册》仅介绍了房屋建造的方法,至于私搭乱建将产生什么后果并未涉及一样。都需另立专题说明。

2.8 指针类型的自动转换

1) 值,任何类型的指针值,甚至整型值,都可以作为赋值表达式的右端,最后将自动转化为赋值运算符左端指针变量的类型。

2) 减,硬让不同类型的指针做减法时,减号右边的指针将自动转化为左边的指针类型。

3) 较,指针也可以硬性地和任何其他类型的指针,甚至整型值做比较,而利用的仅仅是指针地址或整型值。

尽管以上三条行得通,但编译系统还是有可能给出警告信息,以表明那么做是不希望的。作为例子,特给出以下3个式子的计算。

(short*)50 – 10

=> (short*)(50 – 10*2)

=> (short*)30

(short*)50 – (char*)10

=> (short*)50 – (short*)10

=> (50 – 10)/2 => 20

(char*)50 – (short*)10

=> (char*)50 – (char*)10

=> (50 – 10)/1 => 40

3 相关运算的优先级与结合性

这里仅讨论前四个种类的运算符:*、[ ]、&和强制指针类型转换运算符。

其中,“[]”的优先级最高,且结合性从左到右;“*”、“&”以及“(指针类型)”运算符的优先级次之,等同于其他单目运算符,且它们的结合性也相同,都是从右到左。顺便提一下,具备这种结合性的双目运算符只有赋值运算符“=”。

C语言对表达式的计算求解依赖于运算符的优先级和结合性。作为练习,在此给出7例,并做了适度推导。

1) (long*)(short *)2000 = (long *)2000

因 (short *)2000 的指针地址为2000,配上最后运算生效的强制类型转化运算符 (long*),必然等于右侧。

2) (short *)2000 + 10

≠ (short *)(2000 + 10)

因 (short *)2000 + 10 = (short *)2020 而 (short *) (2000 +10) = (short *)2010

3) *((short *)2000 + 5)

<≠> *(short *)2000 + 5

因 *( (short *)2000 + 5 ) <=>

*( (short *)2010 )

再由于“(short *)”与指针运算符“*”的优先级相同,结合性“从右到左”,所以括号可以去掉,即为:

* (short *)2010

此乃变量,可以给它存储值或从中得到值。而

*(short *)2000+5

=> (*(short *)2000) + 5 => 变量 + 5

只能算得一个值

【引申】如果把指针 (short *)2000用指针变量p替换,相当于

*(p + 5) <≠> *p + 5

4) (long *)(2*k+1) ≠> (long *)2*k+1

因“(long *)”的优先级高于乘法运算符“*”,故

(long *)2*k+1 => ((long *)2)*k+1

=> 指针* k + 1,

可是指针不能做乘法运算。

5) ((short *)2000)[5]

≠> (short *)2000[5]

因“[ ]”的优先级高于“(int *)”,故

(int *)2000[5] => (int *)( 2000[5] )

可是整型值与整型值之间不能做下标运算。

6) &*(int *)2000 = (int *)2000

因 &*(int *)2000 => & (* (int *)2000)

而变量 * (int *)2000 的首地址为2000,基类型为int,取指针的结果必然为:(int *)2000

【引申】如果把指针 (int *)2000用指针变量p替换,相当于

&*p = p

7) *&*(int *)2000 <=> *(int *)2000

因 *&*(int *)2000 <=>

*(&* (int *)2000) <=> *( (int *)2000)

<=> * (int *)2000

【引申】如果把变量 *(int *)2000用变量v替换,相当于

*&v <=> v

从后两例可见,“&”与“*”是一对互逆运算符。

补充说明:1)严格地说,C语言没有字面负整数;2)本文仅就Turbo C 2.0开发环境而言。

参考文献:

[1] 王恒滨,闫东升. 关于C语言指针定义的讨论[J]. 辽宁财专学报,2004.

[2] 傅育熙等译. 程序设计语言:设计与实现(第四版)[M]. 北京:电子工业出版社,2001.

[3] 金戈,汤凌等译. 代码大全(第二版)[M]. 北京:电子工业出版社,2006.

猜你喜欢

指针C语言教学内容
新冠疫情期间小学信息技术在线教学内容的选择和实践
“C语言程序设计”课程混合教学探索
郊游
基于C语言的计算机软件编程技术探究
中职C语言单片机课堂教学中的趣味性探讨
为什么表的指针都按照顺时针方向转动
计算机原理中C语言的应用价值
等差数列教学内容的深化探究
浅析C语言指针