张忆文
(华侨大学 计算机科学与技术学院,福建 厦门 361021)
C语言指针教学难点透析
张忆文
(华侨大学 计算机科学与技术学院,福建 厦门 361021)
指针既是C语言的重点,又是教学难点。文章从指针的基本概念入手,由浅入深地讨论指针教学的重点与难点,重点介绍指向数组元素的指针、指向数组的指针、指针数组、指针函数以及函数指针变量等容易混淆的概念,通过应用实例揭示它们之间的区别,进而阐释指针的实质。
C语言;指针;函数;数组
C语言程序设计在计算机程序设计语言中占有重要的一席之地,它以语法简洁紧凑、程序精炼、运算符和数据结构丰富、编程灵活、可移植性好而著称[1]。然而由于教科书内容僵化、过于抽象,且授课的对象往往是大学低年级学生,造成某些知识点难以理解,甚至理解错误。其中,C语言指针教学就是公认的教学难点[2],因为指针是C语言的一大特色,用途极其广泛,所以,如何让学生透彻地理解指针,避免在使用过程中不犯错误或者少犯错误,是C语言教学中的一个重要问题。
1.1 指针变量的定义
所谓指针是指变量在内存中的地址,是一个常量,其实质是对地址的操作实现对数据的操作。“&”和“*”是指针的两个最基本的运算符。“&”是取地址运算符,也就是将变量在内存的地址取出来,其结合性为自右向左。“*”是取内容运算符,也就是将指针变量所指向变量的值取出,其结合性为自左向右。用来存放指针(地址)的变量成为指针变量。其定义如下:
类型标识符 *指针变量名表;
例如:
void main( ){
(1) int i=20;
(2) int *p;
(3) p=&i;
(4) printf(“i address is %x p address is %x *p=%d”,&i,&p,*p);}
语句(2)定义了指针变量p,语句(3)对指针变量p进行初始化,使它指向普通变量i。注意到语句(2)指针定义中的“*”不是指针运算符,不进行任何运算,它仅仅是标志所定义的变量为指针变量。运行程序可知, i address is 0018FF44,p address is 0018FF40,*p=20。普通变量i和指针变量p的关系如图1所示。
图1 普通变量i与指针变量p
从图1可以看出指针变量p的值为普通变量i的地址,也就是说指针变量p指向了普通变量i。普通变量i的值为20。指针变量和普通变量都是变量,其在内存中都占据一定的空间,例如:指针变量p在内存的地址为0018FF40,而普通变量i在内存的地址为0018FF44。指针变量与普通变量的最大区别就是:指针变量所存储的内容是其他变量地址,而普通变量所存储的内容是值。
1.2 指针变量的引用
指针变量的使用要注意以下几点:
(1) 必须先定义,后使用。
(2)对指针变量的操作,其类型要一致。例如int i=50;char *p=&i;这个操作就是非法的,因为普通变量i其类型是整形,而指针变量p其类型为字符型,这两个变量的类型不匹配。
(3)不能将数值直接赋值给指针变量,例如int *p=62353;这个语句也是非法的,因为指针变量只能存储其他变量的地址。
(4)指针变量在使用之前必须对其进行初始化,例如 int *p;printf(“*p is %d”);这个操作是非法的,因为没有对指针变量进行初始化,也就是说指针变量没有所指。
(5)指针变量可以进行算术运算,例如 int a[5]={0,1,2,3,4}; int *p=a; p+3;语句p+3不是指针变量p的值简单加3,而是使指针变量p指向a[3],假设一个int占用4个字节(不同的编译器,所占用的字节不一样),p+3相当于移动了12个字节。
(6)注意const int *p;int const *p; const int const *p;三者的区别。const int *p表示值不变地址可变,也就是说不能利用指针变量p对其所指向的对象的值进行修改,但指针变量p可以指向同类型的其他变量。int const *p表示地址不变,值可变,也就是说可以利用指针变量p对其所指向的对象的值进行修改,但指针变量p不能指向同类型的其他变量。const int const *p表示值不变,地址也不变,也就是说不能利用指针变量p对其所指向的对象的值进行修改,也不能使指针变量p指向同类型的其他变量。
数组是C语言中的一个重要构造类型,是具有相同数据类型的有序数据集合。数组中的每个元素都在内存中占用存储单元,且每个存储单元占据的存储空间都是相同的。数组名代表数组元素的首地址,可以将其直接赋值给相同数据类型的指针变量。在指针的教学过程中,必须注意指向数组元素的指针、指向指针的指针、指向数组的指针、指针数组等概念的区别与联系。
2.1 指向数组元素的指针
指向数组元素的指针变量的定义与以前的指针变量的定义一样,但要确保数组的类型与指针变量的类型一致。例如:
以上的两条语句定义了指向一维数组a的元素的指针变量p,并且对指针变量p进行初始化,使其指向数组的首元素a[0]。接下来我们就可以使用指针变量p访问数组元素。例如p+1指向元素a[1],注意p+1不是将p的数值简单加1,而是使其指向当前元素的下一元素。所以*(p+1)与a[1]等价。更一般化,p+i (0≤i≤4)表示指针变量p指向数组的a[i]元素,所以*(p+i)与a[i]等价。当然也可以直接用数组名直接访问数组元素,因为数组名代表数组首元素的地址,所以有*(a+i)与*(p+i)等价。
以上介绍了用指针变量访问一维数组元素,接下来介绍指针变量访问二维数组元素。例如:
想要访问二维数组中的元素a[2][3],可以用如下的方法:
(1)直接用数组下标a[2][3];
(2)用一维数组名a访问:*(a[2]+3)或*(a[0]+11)或*(a[3]-1];
(3)用二维数组名a访问,只要将(2)中的一维数组名改成相应的二维数组名即可:*(*(a+2)+3)或*(*(a+0)+11)或*(*(a+3)-1);
(4)用指针变量p访问:*(*(p+2)+3)或*(*(p+0)+11)或*(*(p+3)-1)等等。
2.2 指向指针的指针
指向指针的指针是指一个指针变量指向了另外一个指针变量,常称为多级间址。例如:
语句(1)、(2)、(3)定义了指向指针的指针变量q并且对其进行初始化,指针变量q指向了指针变量p,而指针变量p指向了普通变量i。 运行程序可知,i address is 0018FF34,p address is 0018FF30,q address is 0018FF2C,p=0018FF34,q=0018FF30,*p= 20,**q=20。普通变量i、指针变量p和指向指针的指针变量q的关系如图2所示。
图2 指向指针的指针变量关系
从图2可以看出指向指针的指针变量q的值为指针变量p的地址,也就是说指向指针的指针变量q指向了指针变量p。指针变量p的值为普通变量i的地址,也就是说指针变量p指向了普通变量i。*q指向了普通变量i,因此,**q的值就是i的值等于20。
2.3 指向数组的指针
指向数组的指针是数组名的指针,即数组首元素的指针,其实质为指针。接下来的实例将介绍指向一维数组的指针。
void main( ){
(1) int a[]={10,20,30,40,50};
(2) int (*p)[5];
(3) p=&a;
(4) printf(“%d ”, *(*p+3) );
(5) int b[2][4]={{0,1,2,3},{4,5,6,7}};
(6) printf(“%d ”, *(*(b+1)+3) );}
语句(2)声明p是一个指向一维数组的指针,且所指向的一维数组的长度为5,其实质相当于二级间址。语句(3)将p初始化,使其指向一维数组a。*p的值和a的值一样,都指向a[0],(*p+3)指向a[3],所以语句(4)的输出结果为40。注意语句(3)不可以写成p=a,因为尽管a是数组名代表数组元素的首地址,但其与数组名的地址是两回事。实际上,二维数组名就是一个指向一维数组的指针,所以语句(6)的输出结果为7。接下来的实例将介绍指向二维数组的指针。
void main( ){
(1) int a[2][3]={10,20,30,40,50,60};
(2) int (*p)[2][3];
(3) p=&a;
(4) printf(“%d ”, *(*(*p+1)+2));
语句(2)声明p是一个指向二维数组的指针,且所指向的二维数组的第一维长度为2,第二维长度为3,其实质是指向二级指针的指针,相当于三级间址。语句(3)将p初始化,使其指向二维数组a。*p的值为a,(*p+1)指向一维数组a[1],*(*p+1)+2指向二维数组元素a[1][2],所以语句(4)的输出结果为60。
2.4 指针数组
指针数组是指由若干个相同类型的指针组成的数组,其实质是数组。指针数组与普通数组的本质区别在于指针数组的元素是指针,而普通数组的元素是数值。指针数组与指向数组的指针的本质区别在于指针数组的实质是数组,而指向数组的指针其实质是指针。接下来的实例将介绍指针数组的定义及使用。
void main( ){
(1) char *p[4]={“apple”, “orange”, “pear”, “banana”};
(2) int i=2;
(3) printf(“%s ”, p[2]);}
语句(1)定义了指针数组p,它由4个元素(p[0]~p[3])组成,每个元素都是一个指向字符类型的指针。此外语句(1)还初始化了指针数组p,使p[0]的值指向字符串apple的首地址,p[1]的值指向字符串orange的首地址,p[2]的值指向字符串pear的首地址,p[3]的值指向字符串banana的首地址。特别需要注意区分char *p[4]与char (*p)[4],前者是指针数组,后者是指向一维数组的指针。语句(3)输出的结果为pear。
3.1 指针作为函数参数
整型、实型、字符型、数组、指针等都可以作为函数的参数,但整型、实型、字符型等作为函数参数时,实参与形参间是单向的值传递,也就是说形参的值发生变化不会影响到实参。而指针作为参数时,实参与形参间是传递的是地址,也就是说实参与形参共享一个地址空间,如果形参的值发生改变,实参的值也会相应的改变。例如:
上述实例中,函数void swap(int *p, int *q)是以指针作为形参,其作用是交换两个形参的值。在主函数中,定义了两个整形变量a、b并且分别赋值为10、20;此外,还定义了两个整形的指针变量pa、pb使其分别指向a和b。调用函数swap时,需要注意的是实参的类型和形参的类型必须一致,在调用函数swap之后,程序输出a=20,b=10,说明以指针作为函数参数,形参的值变化,相应的实参的值也发生改变。
3.2 指针函数
指针函数是指函数的返回值为指针类型,也就是说函数最后返回的是一个地址,而不是一个数值。指针函数的定义形式为:
类型标识符 *函数名(参数名){
函数体
以上实例中,首先定义了全局变量a,并且对其初始化,使其值为10;接下来定义了指针函数fun(),其返回类型为指向整型的指针,其参数列表为空。fun()函数的主要作用是返回全局变量a的地址。主函数首先定义了整型指针变量p,接下来对其初始化,使其指向fun()函数,接着输出p所指向对象的值,最终的运行结果为*p=10。注意到指针函数不能返回局部变量的地址,因为局部变量在程序执行完成后,其所占用的内存空间会被系统回收。
3.3 函数指针变量
一个函数在内存中的起始地址就是该函数的指针。函数指针变量是用来存储函数指针,通过函数指针变量可以调用函数。函数指针变量的定义如下:
类型标识符 (*指针变量名)(参数类型1,参数类型2,…,参数类型n);
例如:
以上实例中,首先定义了add函数,其有两个整型的参数a和b,add函数最终返回一个整型值,add函数的作用是求a与b的和。主函数首先定义了整型变量a和b,且分别对其初始化,a的值为5,b的值为10。语句int (*p_fun)(int,int);定义了函数指针变量p_fun。注意函数指针变量与指针函数的区别,int *p_fun(int,int)表示p_fun是指针函数,其返回一个指向整型的指针。区别函数指针变量与指针函数主要看这二者在定义过程中“*”号是否用圆括号()括起来,有圆括号表示函数指针变量,没有圆括号表示指针函数。接下来的语句p_fun=add;对函数指针变量进行初始化,使其指向add函数在内存中的首地址,注意到在C语言中函数名称代表函数在内存的起始地址。接下来的语句输出(*p_fun)(a,b)的值,其结果为15。注意到(*p_fun)(a,b)也可以写成p_fun(a,b)。
总之,在C语言的教学中要注意结合实例讲清以下几点:①指向指针的指针其实质是二级间址;②指向数组的指针其实质是指针,而指针数组其实质是数组;③指针函数其实质是指针,而函数指针其实质为函数。
[1] 刘韶涛, 潘秀霞, 应晖. C语言程序设计[M]. 北京: 清华大学出版社, 2015.
[2] 赵辉, 冯东栋. C语言中指针的教学方法研究[J]. 福建电脑, 2011, 27(4): 187-188.
(编辑:彭远红)
1672-5913(2017)01-0155-04
G642
华侨大学引进人才科研启动基金项目(2016BS104)。
张忆文,男,讲师,研究方向为绿色计算、实时调度,zyw@hqu.edu.cn。