1、第8章 指针对存储信息的引用机制8.1 指针的概念指针的概念8.2 通过指针引用变量的值通过指针引用变量的值8.3 通过指针引用一维数组元素通过指针引用一维数组元素8.4 通过指针引用二维数组通过指针引用二维数组8.5 通过指针引用字符串通过指针引用字符串8.6 通过指针调用函数通过指针调用函数8.7 多重指针与指针数组多重指针与指针数组8.8 用于动态内存分配的指针型函数用于动态内存分配的指针型函数实操训练实操训练课外练习课外练习8.1 指指针针的的概概念念指针指针是什么?为什么通过指针可以访问存储器中的信是什么?为什么通过指针可以访问存储器中的信息?息?指针的一般意义大家都不会陌生,那就是
2、建立一种指向,如仪器仪表的指针,路标指针等。C语言指针也是建立一种指向,只是指向的是存储器单元,按其所指访问存储器单元中的信息。要理解C语言中指针的内涵,需了解对存储器的访问机制。计算机对存储器是按地址进行访问的。存储器包含大量的信息单元,为能方便地访问需要的信息单元,每一个单元都有一个编号,这个编号称为地址。要访问某一个单元,只要给出该单元的地址,就能准确引用该单元信息。打个通俗的比方,一个楼房包含许多房间,为能方便地寻找某个房间,给每一个房间编一个号码,知道了房号就能准确地找到这个房间。由此可知,地址就是对存储器单元的指向,即给出地址就能访问存储器单元,访问存储器单元必须有地址。C语言中的
3、指针就是存储器单元的地址,也可以反过来说,地址就是存储器单元的指针。简言之,指针就是地址,地址就是指针。因此,指针是对存储器单元信息的一种引用机制。地址属于一个与存储器硬件特性相关的概念。在高级语言中,编程人员不涉及计算机的硬件特性,对硬件资源的分配与处理由编译系统来完成。在前面学习变量、数组、函数时,仅是按语言规定进行定义,按所定义的符号名称进行引用,未涉及按地址引用的问题。事实上,程序在编译时,系统给定义的数据对象或函数都要分配相应的存储单元,符号名称也具有相应的地址值,符号名实际上标识了一个存储单元的地址。比如,在一个程序中定义了3个整型变量i、j、k,程序编译时,系统就会给3个变量分别
4、分配4个字节的存储单元,单元中存储变量的值,i、j、k也分别具有相应的地址值。假定3个变量的值分别为2、4、8,编译时给i、j、k分配的地址值分别是1000、1004、1008,其存储情况如图8.1所示。图8.1 数据引用示意图在程序中,用变量名引用变量的值,在计算机内部执行时,是按变量名所具有地址值的单元引用其存储内容。例如:printf(“%d”,i);输出结果是1000地址单元的内容“2”。又如:printf(“%X”,&i);则输出变量i的地址值“1000”。对存储器单元的访问有两种方式:一种是直接访问,另一种是间接访问。直接访问是直接引用地址所指向单元中的内容。按变量名引用变量的值属
5、于直接访问。间接访问是将欲访问变量的地址存放在另一个变量中,先通过该变量取得欲访问变量的地址,再按这个地址引用变量的值。打个通俗的比方,从一个抽屉中存取东西,人们也可采取两种方式:一种是将一个抽屉A的钥匙带在身上,需要时直接用A抽屉钥匙,打开A抽屉,存取所需东西;另一种是将A抽屉钥匙放在另一抽屉B中,锁起来,需要打开A抽屉时,先要用B钥匙打开B抽屉,取出A钥匙后,再打开A抽屉,才能存取其中的东西。这就是一个间接访问的过程。例如,我们将i变量所标识的存储单元的地址(1000)存放在i_pointer变量中,现要引用i变量的值,先要访问i_pointer变量,从中取得i变量的地址,然后才能按此地址
6、所指向的i变量取得所需要的值。访问过程如图8.1所示。为了实现一个变量的间接访问,需引入另外一种变量,称之为指针变量。如果一个变量专门用来存放另一个变量的地址(即指针),则称这个变量为指针变量。也就是说,指针变量的值是地址(即指针)。从存储器的访问机制,我们引入了“指针”和“指针变量”,弄清楚这两个概念的内涵,对学习本章后续内容是至关重要的。8.2 通过指针引用变量的值通过指针引用变量的值怎样通过指针引用变量的值?怎样通过指针引用变量的值?从上节已知,通过指针可以引用变量的值,通过指针变量可以实现变量值的间接引用。使用指针和指针变量给变量值的引用带来了许多灵活多变的方法和技巧,请读者在本节内容
7、的学习中认真体会。8.2.1 指针变量的定义与初始化指针变量的定义与初始化在在程序中怎样使用指针变量?程序中怎样使用指针变量?指针变量是有别于普通变量的,也必须先定义后使用。指针变量定义的一般形式为基类型符*指针变量名;其中,基类型符是基本数据类型关键字,如int、float、char、double等,用来指定所定义指针变量可以指向的变量类型,简单地说,就是指针变量所指的数据对象的类型。“*”规定了所定义的变量是指针型变量。反过来说,没有此星号,那就成为普通变量的定义了。指针变量名是所定义的指针变量的标识符,其命名规则同普通变量定义中的变量名一样。注注:*和指针变量名之间可以有空格,也可以没有
8、空格,两者均可。下面是指针变量定义的几个例子:int*pi1;语句定义了1个指针变量pi1,它只可以指向整型变量。char*pc1,*pc2;语句定义了2个指针变量pc1和pc2,它们只可以指向字符型变量。float*pf1,*pf2,*pf3;语句定义了3个指针变量pf1、pf2和pf3,它们只可以指向实型变量。定义了指针变量,仅声明了所定义的变量是指针类型和可指向的变量类型,并没有确定的指向,也就是说,没有具体指向哪一个变量或存储器单元。打个通俗的比方,我们加工出一些指针,在没有安装到某一个仪器仪表前,它们没有具体的指向。要使所定义的指针变量指向某一个变量,必须将指针变量初始化,将所要指向
9、变量的地址赋给指针变量。指针变量中只能存放地址(即指针)。若要给指针变量赋数值则毫无意义。指针变量初始化可有两种方式:一是先定义指针变量,然后进行初始化;二是定义指针变量的同时初始化。这是一个先定义指针变量再初始化的例子。下面则是一个定义指针变量的同时初始化的例子:注意:注意:定义指针变量可指向变量的类型与初始化赋地址的变量类型必须保持一致,否则会出现错误。如下面的初始化是错误的。因定义的两个指针变量是指向整型数据的,而变量a、b被定义为实型,所以出现了类型不一致的错误。8.2.2 指针变量的引用指针变量的引用通过指针变量怎样间接引用变量的值?通过指针变量怎样间接引用变量的值?在指针变量的引用
10、中要使用两个运算符“&”和“*”。“&”是取地址运算符,例如&a是取变量a的地址;“*”是间接引用运算符,其后跟指针变量,是取该指针变量所指向的数据,例如*p表示指针变量p所指向的数据。指针变量有两种引用:一是通过指针变量引用所指向变量的值,也就是说,通过指针变量实现变量值的间接引用;二是指针变量是变量,其值也可以引用,只不过引用的是地址(指针)值。下面通过两个例子来说明两种引用和指针变量的应用。例例8.1 分析下面程序的运行结果:分析:程序中第1个printf函数调用中,输出变量a、b的值,属于变量值的直接引用,对应执行结果的第1行;第2个printf函数调用中,输出指针变量pointer_
11、1、pointer_2所指向变量的值,pointer_1指向变量a,pointer_2指向变量b,属于变量值的间接引用,对应执行结果的第2行;第3个输出函数调用中,以十六进制格式输出指针变量pointer_1、pointer_2的值,即变量a、b的地址,也属于变量值的直接引用,其地址值是系统分配的。例例8.2 利用指针方法实现:输入两个整数,按先大后小的顺序输出。编程思路:输入两个整数,分别赋给两个变量,比较两个变量的值,利用指针变量可以不交换两个变量的值,而只交换两个指针变量的值(即改变指向)。分析:输入值2、8,即ab,指针变量p1和p2的值进行交换。交换前的情况如图8.2(a)所示,交换
12、后的情况如图8.2(b)所示。从图可以看出,变量a、b的值并未交换,交换了指针变量p1、p2的值,实际上改变了指向,p1原指向a,交换值后指向b,p2原指向b,交换值后则指向a。这样输出*p1、*p2时,就输出变量b和变量a的值。if语句中采用了两个指针变量值的交换p=p1;p1=p2;p2=p;,可改为p1=&b;p2=&a;,效果是一样的。图8.2 指针变化示意图8.2.3 指针变量作函数参数指针变量作函数参数指针变量作函数参数传递什么值?函数之间能建立什指针变量作函数参数传递什么值?函数之间能建立什么联系?么联系?在“函数”一章的学习中,已经知道,变量作函数的参数,把实参变量的值传递给形
13、参变量。指针变量也可作函数的参数,由于指针变量的值是地址(即指针),因此实参与形参之间传递的是指针,在被调函数中按指针的指向可得到变量的值。下面通过一个例子来说明指针变量作函数的参数时指针传递的过程。例例8.3 例8.2(输入两个整数,按先大后小的顺序输出)中,用指针变量作参数的函数来实现,并分析参数传递及其处理过程。编程思路:用一个子函数实现交换两个变量的值,主函数中判断两数大小,需要交换时调用子函数,指针变量作函数参数,传递指针。分析:程序运行时先执行主函数,输入2、8,分别赋给变量a、b,两个指针赋值语句使两个指针变量分别指向变量a、b,如图8.3(a)所示。执行if语句,判断ap1,则
14、p2-p1是两个指针之间的元素个数。例如:8.3.3 通过指针引用数组元素通过指针引用数组元素如何通过指针或指针变量引用数组元素?二者有何区如何通过指针或指针变量引用数组元素?二者有何区别?别?在“数组”一章中已经介绍了“下标法:ai”的形式引用数组元素。通过指针法,即以*(a+i)或*(p+i)形式引用数组元素,将使程序设计更方便和灵活。下面通过例子来说明通过指针引用一维数组元素的方法。例例8.5 一维数组的输入与输出。下面用4种数组元素的引用方法编写程序,请认真分析并体会数组元素的引用方法与编程技巧。(1)下标法。(2)指针法。运行结果同下标法。程序中使用了数组元素指针a+i和指针法引用数
15、组元素:*(a+i)。(3)指针变量法(使用p+和*(p+)。思考思考:将*(p+)改为*(p+i),影响程序的正确性吗?两个“p=a;”语句的作用是什么?如果去掉第2个“p=a;”语句,程序的执行结果会正确吗?为什么?(4)指针变量法(使用p+和*(-p)。分析:显然,程序运行结果以逆序输出。这是因为循环输入后,p指向最后一个元素之后,输出中使用*(-p)先使p减1,指向前一个元素,从而产生了从后向前的输出顺序。例例8.6 输出一个数组中序号为0,3,6,9,的元素。8.3.4 一维数组指针作函数参数一维数组指针作函数参数一维数组指针作函数参数传递什么值?函数间建立什一维数组指针作函数参数传
16、递什么值?函数间建立什么联系?么联系?用一维数组指针作函数参数,实参向形参传递数组首地址,使形参指针和实参指针指向同一个数组存储区。在调用函数期间,如果改变了形参数组的值,也就改变了实参数组的值,在主调函数中就可以引用改变后的值。如有下面定义的函数关系,则存储区共享情况如图8.5所示。图8.5 函数调用共享数组存储区示意图数组指针可用数组名和指向数组的指针变量两种形式传递。实参可以是数组名或指针变量,形参是接收实参传递的数组首元素地址的,应该是指针变量。但因C语言程序编译时是把形参数组名作为指针变量处理的,这样形参也可以是数组名。数组指针作函数参数,实参与形参可有表8.1列出的组合关系。例例8
17、.7 将一个数组中n个元素按相反的顺序存放。编程思路:定义n个元素的数组a,将a0与an-1对换,再将a1与an-2对换,直到将aint(n-1)/2-1与aint(n-1)/2对换。显然,两元素对调用循环结构程序很容易实现。设“两个位置指示”变量i和j,i指向首元素,其初值为0;j指向尾元素,其初值为n-1。使当前所指示的两个元素交换,然后调整指针,使i的值加1,j的值减1,如此循环,直到i=int(n-1)/2,即直到两指针指向中间两相邻的元素。其操作关系如图8.6所示。图8.6 例8.7操作关系图下面用4种参数组合分别编写程序。(1)用一个函数来实现交换,实参用数组名,形参用数组名。分析
18、:在定义函数inv时,形参是数组名x,形参数组没有定义大小,用n来接收数组元素个数。因为编译系统把形参数组名作为一个指针变量处理,并不开辟数组空间。在主函数中,函数调用语句“inv(a,10);”实参是数组名和数组元素个数,把数组首元素地址传递给形参数组名x,数组元素的个数传递给形参n,使形参x指向实参数组,在inv中操作数组,实际上就是实参数组。用函数名作为形参,可以用“下标法”引用形参数组元素,有些人习惯这种直观的引用方法。(2)用一个函数来实现交换,实参用数组名,形参用指针变量。运行结果同(1)。在定义函数inv时,指针变量作形参。引用形参数组时,要用指针变量法。在inv函数中,定义了3
19、个指针变量,i、j是指向当前交换的前元素和后元素的指针变量,p是指向终止交换操作元素的指针变量。函数调用过程及参数传递同(1)。(3)用一个函数来实现交换,实参用指针变量,形参用数组名。(4)用一个函数来实现交换,实参用指针变量,形参用指针变量。例例8.8 用指针法对n个整数按由大到小的顺序排序。编程思路:定义一个一维数组,存放n个整数。定义一个函数,实现对n个整数排序。主函数中的函数调用传递被排序数组的首地址和数据个数。函数参数的组合关系可采用4种组合关系中的任一种。下面通过实参是指针变量、形参是数组名来实现。分析:sort函数中用数组名作形参,用“下标法”引用形参数组元素。如果形参改为指针
20、变量,函数中也可以用与“下标法”类似的形式引用形参数组元素,可采用x+i和x+j作指针来引用数组元素。可将函数修改如下:8.4 通过指针引用二维数组通过指针引用二维数组怎样通过指针引用二维数组元素?怎样通过指针引用二维数组元素?通过指针引用二维数组元素的原理和方法与一维数组元素引用基本相同,但因一维数组的存储结构与其逻辑结构基本一致,而二维数组的存储结构与其逻辑结构是不一致的,从而二维数组的指针比一维数组的指针要复杂一些。8.4.1 二维数组的存储结构与指针二维数组的存储结构与指针如何建立二维数组元素与其指针的对应关系?如何建立二维数组元素与其指针的对应关系?二维数组的逻辑结构是由行、列组成的
21、平面结构。因存储器对数据的存储结构是线性的,所以要将二维平面结构数据变换成一维线性结构来存储。二维数组是按行依次线性展开存储的。设有一个3行4列的二维整型数组,其定义为int a34=1,4,7,10,2,5,8,11,3,6,9,12;其逻辑结构和存储结构的对应关系如图8.7所示。从图中可以看出,按元素顺序存第0行元素、第1行元素、第2行元素。设定义一个n行m列的数组,则aij相对应数组首元素的位置计算公式为im+j若定义了指针变量p,“p=&a00;”,则元素aij的指针为p+(im+j)通过指针对元素aij的引用形式为*(p+(im+j)同一维数组指针一样,上述表示的指针仍是以元素为单位
22、的逻辑指针,不代表元素存储的物理地址。如前面定义数组a的首地址为2000,a21的指针是&a00+(24+1),表示按存储顺序a数组的第9个元素,该元素的物理存储地址应为2000+94=2032。再一次强调:C语言程序中,指向数组元素的指针是逻辑指针,并不是物理存储地址。图8.7 二维数组存储结构示意图根据二维数组的线性存储结构,二维数组可以看成由一维行数组组成的一维数组,如图8.8所示。图8.8 二维数组一维行数组组成示意图可以把二维数组看成由两层一维数组组成。第1层是每一行的4个元素组成一个一维数组,数组名分别为a0、a1、a2。第2层是以a0、a1、a2为元素的一维数组。第2层仅仅是一个
23、逻辑上的一维数组。因为二维数组的存储结构仍按元素存储的线性结构,系统只给二维数组的元素分配存储单元,a0、a1、a2虽是第2层一维数组的元素,但只表示每一行首元素的地址,并不占据存储单元。这样划分的目的是把二维数组转化为一维数组范围内的问题,可以按一维数组元素的引用方法来分析二维数组元素的引用。从一维数组元素的指针来看,a表示首元素地址,应该有a=&a0,但因a0只是逻辑层一维数组的元素,不占有存储单元,谈不上有地址,而a0表示第0行一维数组首元素的地址,即a0=&a00。又从a是二维数组的数组名,代表二维数组首元素地址,即有a=&a00。由此可推出a=a0=&a00。这正是问题理解的关键。我
24、们可以从逻辑引用的角度来分析,*(a+0)=a0,*(a0+0)=a00。为帮助理解,又引用本章前所打比方:从B抽屉取东西,将B钥匙放在A抽屉,随身带A抽屉钥匙,用A钥匙打开A抽屉,取出B钥匙,再用B钥匙打开B抽屉。现在改为B抽屉钥匙放在桌面上,只指示从桌面上拿到B钥匙就可打开B抽屉,即不需要用A钥匙打开A抽屉来得到B钥匙,如图8.9所示。也就是说,从第2层数组直接得到第1层数组元素的地址,再按地址引用第1层的数组元素。图8.9 二维数组引用示意图对于任意元素,*(a+i)=ai,*(ai+j)=aij,即由*(a+i)引用第2层一维数组元素ai,得到第1层行一维数组首元素地址,再由*(ai+
25、j)引用第1层行一维数组元素aij。例如,*(a+1)=a1,*(a1+2)=a12。从以上分析可知,a、a+i、ai、*(a+i)、*(a+i)+j、ai+j都是地址(指针),而*(ai+j),*(*(a+i)+j)是对二维数组元素aij的引用。由于把二维数组看成两层一维数组的特殊性,定义指针变量也有别于直接指向二维数组元素的指针变量定义。其定义的一般形式为基类型符(*指针变量名)m;其中,m表示行一维数组元素个数,即二维数组的列数;基类型符是二维数组元素的类型。这样定义,表示所定义的指针变量指向包含m个基类型元素的一维数组,即指向逻辑层上的行一维数组。注意,指针变量两侧的括号不能缺省。例如
26、,int(*p)4定义了指向包含4个整型元素的一维数组的指针变量p。若“p=&a;”,则*(p+i)=ai,*(*(p+i)+j)=aij。在*(p+i)的指针调整中,是以第2层数组元素为单位的,而第2层数组中的一个元素表示二维数组的一行,所以(p+i)就使指针指向第i行。例如,(p+1)使指针指向第1行,(p+2)使指针指向第2行。在*(p+i)+j的指针调整中,由于经过*(p+i)运算,得到ai,即&ai0,它已经转化为指向列元素的指针了,因此加j是以元素为单位的,即加j就使指针指向第i行的第j个元素。例如,*(p+1)+1使指针指向第1行的第1列元素,*(p+1)+2使指针指向第1行的第
27、2列元素。8.4.2 通过指针引用二维数组元素通过指针引用二维数组元素如何通过指针或指针变量引用二维数组元素?如何通过指针或指针变量引用二维数组元素?由前述已知,二维数组指针变量可有两种定义,对数组元素的引用也有两种方式。1通过指向元素的指针变量引用二维数组元素通过指向元素的指针变量引用二维数组元素设定义了nm二维数组a,并定义了指向数组元素的指针变量p,则对数组元素的引用形式为*(p+(i(m-1)+j)(i(m-1)+j)是以元素为单位的相对位移量。若对p赋首元素指针,则引用aij元素;若p非首元素地址,则引用p当前所指元素后的第i(m-1)+j个元素。这种引用方式就是把二维数组的元素看作
28、一个普通变量,通过定义指向变量的指针变量来间接引用数组元素。2通过指向一维数组的指针变量引用二维数组元素通过指向一维数组的指针变量引用二维数组元素设定义了二维数组a,并定义了指向一维数组的指针变量p,则对数组元素的引用形式为*(*(p+i)+j)若对p赋首元素指针,则引用aij元素;若p非首元素地址,则引用p当前所指元素后的第im+j个元素。这种引用方式是把二维数组看作两层一维数组来引用的。先定义指向行一维数组的指针变量p,*(p+i)取得第i行地址,*(p+i)+j就是第i行第j列元素的地址。例例8.9 设有一个34的二维数组,要求用指向元素的指针变量以平面矩阵的形式输出二维数组各元素的值。
29、编程思路:按二维数组的线性存储结构,指向变量的指针变量可指向任一元素,可用单重循环结构程序来实现;也可结合逻辑结构的行、列下标,对指针变量进行运算调整来引用任一元素,可用双重循环程序结构来实现。(1)按存储结构引用元素。(2)结合逻辑结构下标,调整指针,用双重循环实现程序。分析:运行结果同(1)。外循环控制行标,即控制行的顺序;内循环控制列标,即控制列的顺序。p+i+j是指向aij的指针,依次指向二维数组的每一个元素。外循环每循环一次,p的值为&a00+i3。第1次外循环时,p的值为&a00;第2次外循环时,p的值为&a00+3;第3次外循环时,p的值为&a00+6。分析清楚p值的变化,是理解
30、使用指针变量引用数组元素的关键。例例8.10 输出二维数组任一行、任一列元素的值。编程思路:由键盘输入数组元素的行、列号,根据行、列号,使指针指向该元素输出。可使用指向行一维数组的指针变量。分析:定义了指向由4个整型元素组成的一维数组指针变量p,输入2、3,*(p+i)+j=a2+3,即第2行的第3个元素。如果将第7行“p=a;”置换为“p=&a0;”,或置换为“p=a+0;”,程序能正确编译执行。如果将第7行“p=a;”置换为“p=a0;”,或置换为“p=&a00;”,或置换为“p=*(a+0);”,程序编译时都将出现如下出错信息。前面已介绍,a、a+0、a0、&a00、*(a+0)都是数组
31、首元素的地址。那么,上述情况为什么会不一样呢?问题的关键是:定义了“int(*p)4;”,系统是按两层一维数组进行引用的,p是指向第2层由a0、a1、a2组成的一维数组的指针,只能指向这3个元素,而不能指向第1层每一行的数组元素。下面对几种情况进行分析。(1)数组名a既表示二维数组首元素地址,又表示第2层数组首元素地址,“p=a;”使p指向第2层数组首元素。(2)a+0表示第2层一维数组首元素指针,“p=a+0;”同样使p指向第2层数组首元素。(3)a0、a1、a2表示3个行一维数组的数组名,组成逻辑层的一维数组,虽然不占据存储单元,不具有物理地址,但p=&a0属于一种逻辑操作,其意义使p指向
32、逻辑层的一维数组的首元素。(4)a0虽表示第0行首元素的地址,但它毕竟属于第2层一维数组的元素,将数组元素直接赋指针变量是不符合规定的,所以出错。(5)语句“int(*p)4;”定义了p是指向行一维数组的指针,&a00表示二维数组首元素地址,“p=&a00;”使p指向了二维数组元素,所以出现指向类型错误。8.4.3 二维数组指针作函数参数二维数组指针作函数参数二维数组指针作函数参数传递什么值?函数间建立什二维数组指针作函数参数传递什么值?函数间建立什么联系?么联系?同一维数组指针作函数参数一样,二维数组指针作函数参数,参数间传递指针,使实参和形参指针共同指向一个数组,形参数组与实参数组共享一个
33、二维数组存储区,在被调函数中对形参数组的操作结果,在主调函数中可以引用。用二维数组指针作函数参数,可以方便灵活地处理二维表格数据。下面通过例子来说明。例例8.11 一个班有n个学生,开设m门课程,计算总平均分,并输出任一学生各门课成绩。编程思路:(1)为了突出程序设计方法和程序结构,而不被众多的数据干扰,先设有3个学生、4门课程。在此基础上,只要添加数据,设置相应变量的初值,就能实现更多学生、更多课程的处理。(2)班级成绩表显然属于二维数据表格,按功能设计处理函数,按求总平均分设计一个处理函数,按输出一个学生各门课成绩设计一个处理函数,数组指针作函数参数,减少数据传递数量,提高程序效率。分析:
34、(1)求平均分函数average定义中,第一个形参p定义为指向实型数组元素的指针,第2个形参n是二维数组元素的个数,即全班学生所有课程的成绩数目。函数调用时,第1个实参*score是第2层逻辑一维数组首元素的引用,即score0,也就是&score00,即score00的地址,使形参p指向成绩数组首元素。(2)查找并输出任一个学生成绩函数search定义中,第1个参数定义p为指向第2层行一维数组的指针,第2个参数n表示学号。函数调用时,第1实参是score,也代表第2层行一维数组首元素score0的指针,使形参p指向第0个学生的第0门课程成绩。*(*(p+n)+i)表示第n个学生的第i门课程分
35、数,即元素scoreni的值。特别强调:实参和形参如果是指针类型,应当注意它们类型的一致性。不能把int*型的指针(即元素的地址)传给int(*)型(指向一维数组)的指针变量,反之亦然。例如,函数调用score(score,2)置换成score(*score,2)就产生错误。因为*score=score0=&score00是二维数组首元素地址,而形参是int(*)型(指向一维数组)的指针变量。例例8.12 在例8.11的基础上,查找有一门以上课程不及格的学生,并输出这些学生全部课程的成绩。分析:search函数定义中,第一个形参p定义为指向第2层一维数组的指针,第2个形参n是班级学生数。在函数
36、中定义了一个标志变量flag,在内循环外赋初值0,在内循环中若查出当前查找的学生有不及格课程,则赋值1。结束内循环后,检查标志,如果flag为1,则通过一个循环输出该学生的号码和各门课成绩。函数调用时,传递实参score,即score0的地址,使形参p指向score数组的第0行。8.5 通过指针引用字符串通过指针引用字符串怎样通过指针引用字符数组中的字符?怎样通过指针引用字符数组中的字符?在前面的章节中已经使用过字符串,但都是以字符串常量的形式出现的,即用一对双撇号界定的字符串。在“字符数组”一节中,介绍了通过字符数组存储及引用字符串的方法,字符串还可以通过指针来引用。8.5.1 字符串的存储
37、结构与指针字符串的存储结构与指针如何建立字符串中字符与其指针的对应关系?如何建立字符串中字符与其指针的对应关系?在前面已经介绍过,字符串是按字符数组进行存储的,只是在最后一个字符后加存一个字符串结束符“0”。例如,char string=I love China!,其存储结构如图8.10所示。根据数组指针的概念,字符串指针就是其存储的首地址,字符数组名也表示字符串的首地址图8.10 字符串存储结构与指针8.5.2 通过指针引用字符串通过指针引用字符串如何通过指针或指针变量引用字符数组如何通过指针或指针变量引用字符数组(字符串字符串)字符?字符?同前面讲过的数组引用一样,通过指针引用字符串也有两
38、种方法:一种是通过所定义的字符数组名(指针)引用;另一种是通过所定义的字符串指针变量来引用。1通过字符数组名通过字符数组名(指针指针)引用引用在“字符数组”一节中已经介绍,通过字符数组名可以引用字符串,使用“%s”格式描述符,可以整体输入或输出,只不过没有引入指针的概念。字符数组名就表示字符串指针,通过指针可以对字符串及字符串中的字符进行引用。下面通过一个例子说明引用方法。例例8.13 定义一个字符数组,存放字符串“I love China!”,输出该字符串和第8个元素。分析:在“printf(%sn,string);”中使用%s格式描述符和数组名(指针)对字符串整体输出,这里与数值型数组不同
39、。在C语言中,“%s”格式描述符与指针配合,能实现字符串的整体输出或输入。先使指针指向串的首字符,输出,指针自动加1,指向下一个字符,依次输出,直到字符串结束符“0”。在“printf(No.8%cn,*(string+7);”中的“*(string+7)”是用指针引用字符串中第7个元素string7,因数组元素编号从0开始,实际上就是串中第8个字符。2通过字符串指针变量引用通过字符串指针变量引用指向字符串的指针变量,也就是指向字符数组的指针变量。指向字符数组的指针变量也必须先定义后使用,而且指针变量只有赋初值后才能建立指向。字符型指针变量定义的一般形式为char*指针变量名;其中,char属
40、于基类型,规定了指针变量指向字符型数据(即指向字符串),其他部分同指针变量定义的意义一样。给字符型指针变量赋初值的方式也有两种:一是定义的同时赋初值;二是先定义,再赋初值。例如,下面两种定义和赋初值的方式是等价的。方法一:char*p_string=I love China!;方法二:char*p_string;p_string=I love China!;注意:注意:给字符型指针变量赋初值与给数值型指针变量赋初值是不一样的。从形式上看,给字符型指针变量赋了一个字符串,实际上,系统是把字符串首地址赋给字符型指针变量。可以这样想象,字符型指针变量是一个变量,只占一个存储单元,它不可能存放一个任意
41、长度的字符串。所以,不要理解为把一个字符串赋给了一个字符型指针变量。例例8.14 通过指针变量来实现例通过指针变量来实现例8.13的功能。的功能。分析:与例8.13相比,只是定义了字符型指针变量,用指针变量来引用字符串和字符串中的字符,运行结果也完全一样。但仍要注意,用数组名作指针,其地址值是固定的,而用字符型指针变量,其值是可以变化的。在本例中体现得不是太明显,但在比较复杂的问题中就会表现出来。例例8.15 将字符串a复制到字符串b中,再输出字符串b。编程思路:对字符串的输入或输出,可以使用%s格式描述符,进行整体的输入或输出。但对字符串的其他操作,只能对字符串中的字符逐个进行处理,即对字符
42、数组中的元素逐个进行处理。所以,对字符串的处理,常采用循环结构。下面分别通过指针引用法和指针变量引用法来编写程序。分析:数组b的长度应大于等于数组a,否则无法完整复制。复制循环中使用了指针引用法,把a数组中的字符依次逐个复制到b数组中,循环结束的判定条件是字符串结束符“0”,不能把结束符复制到b数组中。所以循环结束后,向b数组后添加结束符“0”。输出循环中使用了数组下标法,可以与指针引用法比较。分析:该程序的运行结果同(1)。循环复制中使用指针变量法实现逐个字符复制,指针变量既作指针,又作循环控制变量,使p1、p2同步移动。这也是指针变量使用的一种技巧。两字符串的输出仍采用了数组名(指针法),
43、体现了通过指针引用字符串的多种方法。8.5.3 字符指针作函数参数字符指针作函数参数字符指针作函数参数传递什么值?函数间建立什么联字符指针作函数参数传递什么值?函数间建立什么联系?系?同数组指针作函数参数一样,字符指针作函数参数,参数间传递指针值,使实参和形参指针共同指向一个字符串存储区。在被调函数中可以改变字符串的内容,主调函数中可以引用改变后的字符串。实参和形参的形式同样可有4种组合关系。例例8.16 用函数调用实现字符串的复制。编程思路:定义一个函数来实现字符串复制的功能,在主函数中提供源串,调用复制函数后,输出源串和目标串。函数参数可以是字符数组名(字符串指针)或字符指针变量。下面分别
44、给出实参和形参4种组合关系的程序,以供分析比较,体会编程方法与技巧。(1)实参、形参都用字符数组名(指针)。分析:程序中函数实参和形参都是数组名,系统仍把形参数组名作指针变量处理。在函数调用时,将a和b第1个字符的地址分别传给形参from和to,使from指向字符串a,to指向字符串b。函数调用后,字符串b产生变化。函数调用前后字符串数组的状态如图8.11(a)和(b)所示。从图中可以看到,由于b数组原来长度大于a数组,在a数组复制到b数组后,未能全部覆盖b数组原有内容。b数组最后3个元素仍保留原状。在输出b时,由于按%s格式输出,遇到“0”即结束,因此第1个“0”后的字符不输出。如果采用%c
45、格式,则后面的字符是可以输出的。图8.11 函数调用前后的字符数组状态(2)实参用字符数组名,形参用字符指针变量。分析:程序的运行结果同(1)。形参是指针变量,调用时接收参数传递的源串和目标串的首地址,利用指针变量加1调整运算,能很方便地使用for循环来实现逐个字符的复制。(3)实参用字符指针变量,形参用字符数组名。分析:程序的运行结果同(1)。参数传递的是指针变量,但被赋给字符数组名,调用时,传递的是字符串首地址。形参是数组名,编译系统按指针变量看待,可接收实参传递的字符串首地址,在字符复制循环中用“下标法”实现逐个字符复制。(4)实参、形参都用字符指针变量。分析:程序的运行结果同(1)。参
46、数传递及函数调用过程请读者自行分析。字符型指针作函数参数,引用字符串元素的方法有多种,给程序设计带来许多技巧。下面通过对copy_string函数的改写来说明程序设计的方法与技巧。8.6 通过指针调用函数通过指针调用函数通过指针可以调用函数吗?通过指针可以调用函数吗?在程序中定义的每一个函数,编译时均生成代码,系统为其分配一定的存储空间。一个函数代码存储空间的起始地址就是函数调用的入口地址。在引入指针之前,用函数名调用函数。事实上是用函数名表示函数的入口地址。本质上,函数调用会使程序指针转到函数入口处执行。通过指针或指针变量调用函数,反映函数调用的实质,会产生函数调用的灵活方法,实现一些用函数
47、名调用实现不了的功能。8.6.1 函数指针与指针变量的定义函数指针与指针变量的定义何谓函数指针?在程序中如何使用函数指针或函数指何谓函数指针?在程序中如何使用函数指针或函数指针变量?针变量?函数的指针就是函数代码存储的起始地址,也就是函数调用时的入口地址。可以通过指向函数入口的指针变量(或简称指向函数的指针变量)来调用函数。指向函数的指针变量又是一种新类型的指针变量,也必须先定义后使用。指向函数的指针变量定义的一般形式为基类型符(*变量名)(函数参数表列)其中,基类型符是指函数返回值的类型。变量名前加*表示是指针变量,两侧再加括号,连同后面的括号内的函数参数表列一起,表示所定义的变量是指向返回
48、值为基类型的函数的指针变量。也就是说,变量名两侧的括号和其后的括号与参数列表都不能遗漏。例如:int(*p)(int,int);定义了指向返回值为整型、有两个整型参数的函数的指针变量p。与一般指针变量定义和使用一样,定义了一个指向函数的指针变量后,并没有指向哪一个函数,只有当赋初值后,才能建立起明确的指向。如果把一个已定义的函数名赋给已定义的函数的指针变量,则指针变量与函数就建立了明确的指向。8.6.2 通过函数指针调用函数通过函数指针调用函数如何通过函数指针或指针变量调用函数?如何通过函数指针或指针变量调用函数?通过函数指针调用函数有两种方式:一是通过函数指针(函数名);二是通过指向函数的指
49、针变量。通过函数名调用已经介绍过,因函数名仅表示一个固定地址,故只能调用一个固定的函数。指向函数的指针变量的值可以变化,可使其指向不同的函数,分别实现不同函数的调用。例例8.17 用函数求两个整数中的大数。用函数求两个整数中的大数。编程思路:定义一个函数,通过比较求出两个整数中的大数。在主函数中定义指向函数的指针变量,通过指针变量调用函数,得到两数中的大数。分析:程序第5行定义了指向返回值为整型、有两个整型参数函数的指针变量p。第7行将函数名表示的函数起始地址赋给指针变量p,使其指向函数max。第10行“c=(*p)(a,b);”是指向函数的指针变量对函数的调用,a、b是实参,把函数的返回值赋
50、给变量c。函数调用过程及参数传递同函数名调用。如果将“c=(*p)(a,b);”置换为“c=max(a,b);”且不需定义指向函数的指针变量和赋初值语句,则程序的执行结果完全一样。这个例子说明了通过指向函数的指针变量调用函数的方法,但看不出通过指向函数的指针变量调用函数的优越性。在后面将会看到,利用指针变量值的可变性,可以实现一类函数的调用。8.6.3 用指向函数的指针作函数的参数用指向函数的指针作函数的参数函数指针作函数参数传递什么值?调用函数与被调函函数指针作函数参数传递什么值?调用函数与被调函数间建立什么关系?数间建立什么关系?用指向函数的指针作函数参数,实参把函数的入口地址传递给形参,