《C语言程序设计基础》课件第8章_第1页
《C语言程序设计基础》课件第8章_第2页
《C语言程序设计基础》课件第8章_第3页
《C语言程序设计基础》课件第8章_第4页
《C语言程序设计基础》课件第8章_第5页
已阅读5页,还剩199页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第8章

指针——对存储信息的引用机制8.1指针的概念8.2通过指针引用变量的值8.3通过指针引用一维数组8.4通过指针引用二维数组8.5通过指针引用字符串8.6通过指针调用函数8.7多重指针与指针数组8.8用于动态内存分配的指针型函数实训任务十五

熟悉指针数据类型,掌握指针的正确使用实训任务十六

学习指针的应用

8.1指

指针是什么?为什么通过指针可以访问存储器中的信息?

指针的一般意义大家都不会陌生,那就是建立一种指向,如仪器仪表的指针,路标指针等。C语言指针也是建立一种指向,只是指向的是存储器单元,按其所指访问存储器单元中的信息。要理解C语言中指针的内涵,需了解对存储器的访问机制。计算机对存储器是按地址进行访问的。存储器包含大量的信息单元,为能方便地访问需要的信息单元,每一个单元都有一个编号,这个编号称为地址。要访问某一单元,只要给出该单元地址,就能准确引用该单元信息。打个通俗的比方,一个楼房包含许多房间,为能方便地寻找某个房间,给每一个房间编一个号码,知道了房号就能准确地找到这个房间。由此可知,地址就是对存储器单元的指向。即给出地址就能访问存储器单元,访问存储器单元必须有地址。C语言中的指针就是存储器单元的地址,也可以反过来说,地址就是存储器单元的指针。简言之,指针就是地址,地址就是指针。因此,指针是对存储器单元信息的一种引用机制。

地址属于一个与存储器硬件特性相关的概念。在高级语言中,编程人员不涉及计算机的硬件特性,对硬件资源的分配与处理由编译系统来完成。在前面学习变量、数组、函数时,仅是按语言规定进行定义,按所定义的符号名称进行引用,未涉及按地址引用的问题。事实上,程序在编译时,系统给定义的数据对象或函数都要分配相应的存储单元,符号名称也具有相应的地址值,符号名实际上标识了一个存储单元的地址。比如,在一个程序中定义了3个整型变量i、j、k,程序编译时,系统就会给3个变量分别分配4个字节的存储单元,单元中存储变量的值,i、j、k也分别具有相应的地址值。假定3个变量的值分别为2、4、8,编译时给i、j、k分配的地址值分别是1000、1004、1008,其存储情况如图8.1所示。

图8.1数据引用示意图对存储器单元的访问有两种方式:一种是直接访问,另一种是间接访问。

直接访问是直接引用地址所指向单元中的内容。按变量名引用变量的值属于直接访问。

间接访问是将欲访问变量的地址存放在另一个变量中,先通过该变量取得欲访问变量的地址,再按这个地址引用变量的值。打个通俗的比方,从一个抽屉中存取东西,人们也可采取两种方式:一种是将一个抽屉A的钥匙带在身上,需要时直接用A抽屉钥匙,打开抽屉,存取所需东西;另一种是将A抽屉钥匙放在另一抽屉B中,锁起来,需要打开A抽屉时,先要用B钥匙打开B抽屉,取出A钥匙后,再打开A抽屉,才能存取其中的东西。这就是一个间接访问的过程。例如,我们将i变量所标识的存储单元的地址(1000)存放在i_pointer变量中,现要引用i变量的值,先要访问i_pointer变量,从中取得i变量的地址,然后才能按此地址所指向的i变量取得所需要的值。访问过程如图8.1所示。

为了实现一个变量的间接访问,需引入另外一种变量,称之为指针变量。如果一个变量专门用来存放另一个变量的地址(即指针),则称这个变量为指针变量。也就是说,指针变量的值是地址(即指针)。

从存储器的访问机制,我们引入了“指针”和“指针变量”,弄清楚这两个概念的内涵,对学习本章后续内容是至关重要的。

8.2通过指针引用变量的值

8.2.1指针变量的定义与初始化

在程序中怎样使用指针变量?

指针变量是有别于普通变量的,也必须先定义后使用。

指针变量定义的一般形式为

基类型符*指针变量名;

其中,基类型符是基本数据类型关键字,如int、float、char、double等,用来指定所定义指针变量可以指向的变量类型。简单地说,就是指针变量所指的数据对象的类型。“*”规定了所定义的变量是指针型变量。反过来说,没有此星号,那就成为普通变量的定义了。

指针变量名是所定义的指针变量的标识符,其命名规则同普通变量定义中的变量名一样。

注:*和指针变量名之间可以有空格,也可以没有空格,两者均可。

下面是指针变量定义的几个例子:

int*pi1;

语句定义了一个指针变量pi1,它只可以指向整型变量。

char*pc1,*pc2;

语句定义了两个指针变量pc1和pc2,它们只可以指向字符型变量。

float*pf1,*pf2,*pf3;

语句定义了三个指针变量pf1、pf2和pf3,它们只可以指向实型变量。

从以上举例可以看出,一个定义语句可以定义一个指针变量,也可以定义多个指针变量,但只可以是同一类型的变量。定义了指针变量,仅声明了所定义的变量是指针类型和可指向的变量类型,并没有确定的指向。也就是说,没有具体指向哪一个变量或存储器单元。打个通俗的比方,我们加工出一些指针,在没有安装到某一个仪器仪表前,它们没有具体的指向。要使所定义的指针变量指向某一变量,必须给指针变量初始化,将所要指向变量的地址赋给指针变量。指针变量中只能存放地址(即指针)。若要给指针变量赋数值则毫无意义。

指针变量初始化可有两种方式:一是先定义指针变量,然后进行初始化;二是定义指针变量的同时初始化。

8.2.2指针变量的引用

通过指针变量怎样间接引用变量的值?

在指针变量的引用中要使用两个运算符 & 和 *。“&”是取地址运算符,例如&a是取变量a的地址;“*”是间接引用运算符,其后跟指针变量,是取该指针变量所指向的数据,例如*p表示指针变量p所指向的数据。

指针变量有两种引用:一是通过指针变量引用所指向变量的值,也就是说,通过指针变量实现变量值的间接引用;二是指针变量是变量,其值也可以引用,只不过引用的是地址(指针)值。下面通过两个例子来说明两种引用和指针变量的应用。

例8.1

分析下面程序的运行结果:

运行结果:

分析:程序中第1个printf函数调用中,输出变量a、b的值,属于变量值的直接引用,对应执行结果的第1行;第2个printf函数调用中,输出指针变量pointer_1、pointer_2所指向变量的值,pointer_1指向变量a,pointer_2指向变量b,属于变量值的间接引用,对应执行结果的第2行;第3个输出函数调用中,以十六进制格式输出指针变量pointer_1、pointer_2的值,即变量a、b的地址,也属于变量值的直接引用,其地址值是系统分配的。

例8.2利用指针方法实现:输入两个整数,按先大后小的顺序输出。

编程思路:输入两个整数,分别赋给两个变量,比较两个变量的值,利用指针变量可以不交换两个变量的值,而只交换两个指针变量的值(即改变指向)。

运行结果:

分析:输入值2、8,即a<b,指针变量p1和p2的值进行交换。交换前的情况如图8.2(a)所示,交换后的情况如图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指针变量作函数参数

指针变量作函数参数传递什么值?函数之间能建立什么联系?

在函数一章的学习中,已经知道,变量作函数的参数,把实参变量的值传递给形参变量。指针变量也可作函数的参数,由于指针变量的值是地址(即指针),因此实参与形参之间传递的是指针,在被调函数中按指针的指向可得到变量的值。下面通过一个例子来说明指针变量作函数的参数,指针传递的过程。

例8.3例8.2(输入两个整数,按先大后小的顺序输出)中,用指针变量作参数的函数来实现,并分析参数传递及其处理过程。

编程思路:用一个子函数实现交换两个变量的值,主函数中判断两数大小,需要交换时调用子函数,指针变量作函数参数,传递指针。

分析:程序运行先执行主函数,输入2、8分别赋给变量a、b,两个指针赋值语句使两个指针变量分别指向变量a、b,如图8.3(a)所示。执行if语句,判a<b,调用swap函数,两个指针变量pointer_1、pointer_2的值(地址)传递给形参指针变量,使p1的值为&a,p2的值为&b,此时,对应的实参和形参指向同一个变量,如图8.3(b)所示。执行swap函数,使p1、p2指向的两个变量的值交换,如图8.3(c)所示。函数调用结束后,形参p1、p2被释放,即解除对变量a、b的指向,但在swap函数中的操作结果却保留在变量a、b中,调用返回后输出的a、b是交换后的值,如图8.3(d)所示。

图8.3例8.3函数调用参数传递示意图通过本例应该深刻理解,指针变量作函数参数,实参传递地址(指针)给形参,使对应的实参指针变量和形参指针变量指向同一个变量(存储单元),在被调函数中通过形参指针变量对所指向的变量(存储单元)的操作结果,在主函数中引用。相当于通过被操作的变量(存储单元)传送了值。这样就给函数间增加了一种数据传递的渠道。需特别注意:通过实参和形参所指向变量值的传递是双向的(在主调函数和被调函数中都可以引用变量的值),但实参指针变量向形参指针变量传递地址值仍是单向传递。也就是说,形参值不可能传递给实参。因为函数调用结束后,形参就不复存在了。变量作函数参数,函数调用只能得到一个返回值(即函数值),使用指针变量作函数参数,可以得到多个操作变量的值。如果想通过函数调用得到n个操作数据,只需在函数中设置n个指针变量参数,函数调用时,将n个变量的指针传递给形参,在被调函数中通过形参指针变量对所指向变量进行操作,在主调函数中就可得到操作结果。下面通过一个例子来说明。

例8.4输入3个整数,按由大到小的顺序输出。

编程思路:对3个数排序,必须两两比较,根据大小关系进行交换。这样,需要3次比较,可能需要3次两数的交换操作。为避免交换操作的重复书写,可设计两个函数,一个函数实现两数的交换功能,一个函数进行两数比较,根据判定结果调用交换函数。

运行结果:

分析:在主函数中使指针变量pi1、pi2、pi3分别指向变量a、b、c,exchange(pi1,pi2,pi3)函数调用,将变量a、b、c的地址值分别传递给形参指针变量pj1、pj2、pj3。exchange(int*pj1,int*pj2,int*pj3)中,通过形参指针变量的间接引用来比较a与b、a与c、b与c,如果前者小于后者,调用swap函数,将对应变量的地址传递给形参指针变量pk1、pk2。在swap(int*pk1,int*pk2)中利用形参指针变量的间接引用来实现所指向变量值的交换。

8.3通过指针引用一维数组

8.3.1一维数组的存储结构与指针

如何建立数组元素与其指针的对应关系?

一维数组的存储是:系统根据所定义的类型,按元素顺序分配一个连续的存储空间。类型规定了数组元素的存储长度,即一个元素所占的存储字节数。char型数组一个元素占一个字节,int型数组一个元素占2个或4个字节,float型数组一个元素占4个字节,double型数组一个元素占8个字节。例如,有语句“inta[5]={1,3,5,7,9};”,假定编译系统分配的存储区首地址是2000,则存储结构如图8.4所示。图8.4一维数组存储结构示意图在C语言中,可以不使用存储地址来引用数组元素,而以数组名代表数组存储首地址(指针),以数组元素的引用符号取地址运算符代表元素地址(指针)。如a表示所定义数组首地址,&a[0]表示第0个元素地址,&a[1]表示第1个元素地址,…

知道数组及数组元素指针后,就可以定义指针变量来实现数组元素的间接引用。例如:

“p=a;”可以置换成“p=&a[0];”,这两个语句是等效的,都使指针变量p指向所定义数组的首元素。注意,数组名就表示数组首地址,一个数组元素相当于一个变量,必须用“&”才得到元素地址。

定义了指向数组的指针变量后,就可以利用指针变量及其相关运算,以方便灵活的方法引用数组元素。

8.3.2一维数组指针调整与指针变量的运算

1.使指针指向下一个元素:p+1,p++,++p

这3种运算都可以使指针从当前元素指向下一个元素。注意指针变量加1不是指针变量值加1的结果,而是下一个元素的地址值。如图8.4所定义的数组,若“p=a;”或“p=&a[0];”,p的值是2000,p+1后使指针指向元素a[1],p+1的值是2004,而不是2001。若“p=&a[2];”,p的值是2008,p+1后使指针指向元素a[3],p+1的值是2012,而不是2009。由此可以理解为:指向数组指针变量的运算,是以元素个数为单位的逻辑指针运算,而不是纯地址值的运算。

p++和++p都可以使指针指向下一个元素,但引用元素时就有区别了。若“p=&a[2];”,则*(p++)是先引用元素a[2],再使p指针指向a[3];*(++p)先使指针指向a[3],引用的元素是a[3]。

2.使指针指向前一个元素:p-1,p--,--p

这3种运算都可以使指针从当前元素指向前一个元素。它们对指针的调整作用类似于加1运算。

p--、--p也是以元素个数为单位的逻辑指针运算,而不是纯地址值的运算。

3.使指针指向第i个元素:a+i(a是所定义的数组名),p+i

这两种运算都可以使指针指向i个元素,但有本质的不同。a代表数组的首地址,也是首元素的地址,a属于常量,a+i是数组中第i个元素的指针。p是指针变量,可以是数组中任一元素的指针,p+i是从当前所指元素后移到第i个元素的指针。若“p=a;”或“p=&a[0];”,则a+i和p+i是一样的,*(a+i)和*(p+i)都是引用数组a中的第i个元素。若“p=&a[2];”,则*(p+i)是引用a[2]后的第i个元素,即引用的元素是a[2+i]。

4.使指针指向前第i个元素:p-i

p-i是指针从p当前所指的元素指向前第i个元素。例如p=&a[4],则*(p-2)引用的是a[2]。注意,a-i是没有意义的。

5.p2-p1运算的意义

如果两个指针变量p1、p2都指向同一数组,p2>p1,则p2-p1是两个指针之间的元素个数。例如:8.3.3通过指针引用数组元素

例8.5一维数组的输入与输出。

下面用4种数组元素的引用方法编写程序,请认真分析并体会数组元素的引用方法与编程技巧。

(1)下标法。

运行结果同下标法。程序中使用了数组元素指针a+i和指针法引用数组元素:*(a+i)。

思考:将*(p++)改为*(p+i),影响程序的正确性吗?两个“p=a;”语句的作用是什么?如果去掉第2个“p=a;”语句,程序的执行结果会正确吗?为什么?

(4)指针变量法(使用p++和*(--p))。

运行结果:

显然,程序运行结果以逆序输出。这是因为,循环输入后,p指向最后一个元素之后,输出中使用*(--p)先使p减1,指向前一个元素,从而产生了从后向前的输出顺序。

例8.6将一个数组中序号为0,3,6,9,…的元素输出。

8.3.4一维数组指针作函数参数

一维数组指针作函数参数传递什么值?函数间建立什么联系?

用一维数组指针作函数参数,实参向形参传递数组首地址,使形参指针和实参指针指向同一个数组存储区。在调用函数期间,如果改变了形参数组的值,也就改变了实参数组的值,在主调函数中就可以引用改变后的值。如有下面定义的函数关系,则存储区共享情况如图8.5所示。

图8.5函数调用共享数组存储区示意图数组指针可用数组名和指向数组的指针变量两种形式传递。实参可以是数组名或指针变量,形参是接收实参传递的数组首元素地址的,应该是指针变量。但因C语言程序编译时,是把形参数组名作为指针处理的,这样,形参也可以是数组名。数组指针作函数参数,实参与形参可有表8.1列出的组合关系。表8.1函数实参与形参的组合关系下面通过例子来说明一维数组指针作函数参数的程序设计方法。

例8.7

将一个数组中n个元素按相反的顺序存放。

编程思路:定义n个元素的数组a,将a[0]与a[n-1]对换,再将a[1]与a[n-2]对换,…,直到将a[int(n-1)/2-1]与a[int(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操作关系图运行结果:

分析:在定义函数inv时,形参是数组名x,形参数组没有定义大小,用n来接收数组元素个数。因为编译系统把形参数组名作为一个指针变量处理,并不开辟数组空间。在主函数中,函数调用语句“inv(a,10);”实参是数组名和数组元素个数,把数组首元素地址传递给形参数组名x,数组元素的个数传递给形参n,使形参x指向实参数组,在inv中操作数组,实际上就是实参数组。用函数名作为形参,可以用“下标法”引用形参数组元素,有些人习惯这种直观的引用方法。

运行结果同(1)。在定义函数inv时,指针变量作形参。引用形参数组时,要用指针变量法。在inv函数中,定义了3个指针变量,i、j是指向当前交换的前元素和后元素的指针变量,p是指向终止交换操作元素的指针变量。函数调用过程及参数传递同(1)。

(3)用一个函数来实现交换,实参用指针变量,形参用数组名。

函数运行结果同(1)。与(1)对比,只是在主函数中定义了指针变量,使其指向数组首地址,作函数的实参。

(4)用一个函数来实现交换,实参用指针变量,形参用指针变量。运行结果同(1)。与(1)、(2)、(3)比较,很容易找到异同,请读者自行分析。

上面通过一个问题,用函数指针作参数的4种组合关系来实现,通过对比,可反映各自特点,并揭示程序设计的方法与技巧。

例8.8

用指针法对n个整数按由大到小的顺序排序。

编程思路:定义一个一维数组,存放n个整数。定义一个函数,实现对n个整数排序。主函数中的函数调用传递被排序数组的首地址和数据个数。函数参数的组合关系可采用4种组合关系中的任一种。下面通过实参是指针变量、形参是数组名来实现。运行结果:

分析:sort函数中用数组名作形参,用“下标法”引用形参数组元素。如果形参改为指针变量,函数中也可以用与“下标法”类似的形式引用形参数组元素,可采用x+i和x+j作指针来引用数组元素。

8.4通过指针引用二维数组

8.4.1二维数组的存储结构与指针

如何建立二维数组元素与其指针的对应关系?

二维数组的逻辑结构是由行、列组成的平面结构。因存储器对数据的存储结构是线性的,所以要将二维平面结构数据变换成一维线性结构来存储。二维数组是按行依次线性展开存储的。设有一个3行4列的二维整型数组,其定义为

inta[3][4]={{1,4,7,10},{2,5,8,11},{3,6,9,12}};

其逻辑结构和存储结构的对应关系如图8.7所示。从图中可以看出,按元素顺序存第0行元素、第1行元素、第2行元素。图8.7二维数组存储结构示意图设定义一个n行m列的数组,则a[i][j]相对应数组首元素的位置计算公式为

i × m + j

若定义了指针变量p,“p=&a[0][0];”,则元素a[i][j]的指针为

p + (i × m+j)

通过指针对元素a[i][j]的引用形式为

*(p + (i × m + j))同一维数组指针一样,上述表示的指针仍是以元素为单位的逻辑指针,不代表元素存储的物理地址。如前面定义数组a的首地址为2000,a[2][1]的指针是&a[0][0]+(2×4+1),表示按存储顺序a数组的第9个元素,该元素的物理存储地址应为2000 + 9 × 4 = 2032。再一次强调:C语言程序中,指向数组元素的指针是逻辑指针,并不是物理存储地址。

根据二维数组的线性存储结构,二维数组可以看成由一维行数组组成的一维数组,如图8.8所示。

图8.8二维数组一维行数组组成示意图可以把二维数组看成由两层一维数组组成。第一层是每一行的4个元素组成一个一维数组,数组名分别为a[0]、a[1]、a[2]。第2层是以a[0]、a[1]、a[2]为元素的一维数组。第2层仅仅是一个逻辑上的一维数组。因为二维数组的存储结构仍按元素存储的线性结构,系统只给二维数组的元素分配存储单元,a[0]、a[1]、a[2]虽是第2层一维数组的元素,但只表示每一行首元素的地址,并不占据存储单元。这样划分的目的是把二维数组转化为一维数组范围内的问题,可以按一维数组元素的引用方法来分析二维数组元素的引用。从一维数组元素的指针来看,a表示首元素地址,应该有a=&a[0],但因a[0]只是逻辑层一维数组的元素,不占有存储单元,谈不上有地址,而a[0]表示第0行一维数组首元素的地址,即a[0]=&a[0][0]。又从a是二维数组的数组名,代表二维数组首元素地址,即有a=&a[0][0]。由此可推出a=a[0]=&a[0][0]。这正是问题理解的关键。我们可以从逻辑引用的角度来分析,*(a+0)=a[0],*(a[0]+0)=a[0][0]。为帮助理解,又引用本章前所打比方:从B抽屉取东西,将B钥匙放在A抽屉,随身带A抽屉钥匙,用A钥匙打开A抽屉,取出B钥匙,再用B钥匙打开B抽屉。现在改为B抽屉钥匙放在桌面上,只指示从桌面上拿到B钥匙就可打开B抽屉,即不需要用A钥匙打开A抽屉来得到B钥匙,如图8.9所示。也就是说,从第2层数组直接得到第1层数组元素的地址,再按地址引用第1层的数组元素。

图8.9二维数组引用示意图对于任意元素,*(a+i)==a[i],*(a[i]+j)==a[i][j]。即由*(a+i)引用第2层一维数组元素a[i],得到第1层行一维数组首元素地址,再由*(a[i]+j)引用第1层行一维数组元素a[i][j]。例如,*(a+1)==a[1],*(a[1]+2)==a[1][2]。

从以上分析可知,a、a+i、a[i]、*(a+i)、*(a+i)+j、a[i]+j都是地址(指针),而*(a[i]+j),*(*(a+i)+j)是对二维数组元素a[i][j]的引用。

由于把二维数组看成两层一维数组的特殊性,定义指针变量也有别于直接指向二维数组元素的指针变量定义。其定义的一般形式为

基类型符(*指针变量名)[m];其中,m表示行一维数组元素个数,即二维数组的列数;基类型符是二维数组元素的类型。这样定义,表示所定义的指针变量指向包含m个基类型元素的一维数组,即指向逻辑层上的行一维数组。注意,指针变量两侧的括号不能缺省。

例如,int(*p)[4],定义了指向包含4个整型元素的一维数组的指针变量p。若“p=&a;”,则*(p+i)==a[i],*(*(p+i)+j)==a[i][j]。

在*(p+i)的指针调整中,是以第2层数组元素为单位的,而第2层数组中的一个元素表示二维数组的一行,所以(p+i)就使指针指向第i行。例如,(p+1)使指针指向第1行,(p+2)使指针指向第2行。在*(p+i)+j的指针调整中,由于经过*(p+i)运算,得到a[i],即&a[i][0],它已经转化为指向列元素的指针了,因此加j是以元素为单位的,即加j就使指针指向第i行的第j个元素。例如,*(p+1)+1使指针指向第1行的第1列元素,*(p+1)+2使指针指向第1行的第2列元素。

8.4.2通过指针引用二维数组元素

1.通过指向元素的指针变量引用二维数组元素

设定义了n × m二维数组a,并定义了指向数组元素的指针变量p,则对数组元素的引用形式为

*(p + (i × (m-1) + j))

(i × (m-1) + j)是以元素为单位的相对位移量。若对p赋首元素指针,则引用a[i][j]元素;若p非首元素地址,则引用p当前所指元素后的第i × (m-1) + j个元素。这种引用方式就是把二维数组的元素看作一个普通变量,通过定义指向变量的指针变量来间接引用数组元素。

2.通过指向一维数组的指针变量引用二维数组元素

设定义了二维数组a,并定义了指向一维数组的指针变量p,则对数组元素的应用形式为

*(*(p + i) + j)

若对p赋首元素指针,则引用a[i][j]元素;若p非首元素地址,则引用p当前所指元素后的第i × m + j个元素。这种引用方式是把二维数组看作两层一维数组来引用的。先定义指向行一维数组的指针变量p,*(p+i)取得第i行地址,*(p+i)+j就是第i行第j列元素的地址。

例8.9

设有一个3 × 4二维数组,要求用指向元素的指针变量以平面矩阵的形式输出二维数组各元素的值。

编程思路:按二维数组的线性存储结构,指向变量的指针变量可指向任一元素,可用单重循环结构程序来实现;也可结合逻辑结构的行、列下标,对指针变量进行运算调整来引用任一元素,可用双重循环程序结构来实现。

(1)按存储结构引用元素。

运行结果:

分析:a数组包含12个元素,循环变量i从0到11变化,(p+i)就依次指向每一个元素。

(2)结合逻辑结构下标,对指针调整,用双重循环实现程序。分析:运行结果同(1)。外循环控制行标,即控制行的顺序;内循环控制列标,即控制列的顺序。(p+i+j)是指向a[i][j]的指针,依次指向二维数组的每一元素。外循环每循环一次,p的值为&a[0][0]+i×3。第1次外循环时,p的值为&a[0][0];第2次外循环时,p的值为&a[0][0]+3;第3次外循环时,p的值为&a[0][0]+6。分析清楚p值的变化,是理解使用指针变量引用数组元素的关键。

例8.10

输出二维数组任一行任一列元素的值。

编程思路:由键盘输入数组元素的行、列号,根据行、列号,使指针指向该元素输出。可使用指向行一维数组的指针变量。

运行结果:

分析:定义了指向由4个整型元素组成的一维数组指针变量p,输入2、3,*(p+i)+j=a[2]+3,即第2行的第3个元素。如果将第7行“p=a;”置换为“p=&a[0];”,或置换为“p=a+0;”,程序能正确编译执行。如果将第7行“p=a;”置换为“p=a[0];”,或置换为“p=&a[0][0];”,或置换为“p=*(a+0);”,程序编译时都将出现如下出错信息。

前面已介绍,a、a+0、a[0]、&a[0][0]、*(a+0)都是数组首元素的地址。那么,上述情况为什么会不一样呢?问题的关键是:定义了“int(*p)[4];”,系统是按两层一维数组进行引用的,p是指向第2层由a[0]、a[1]、a[2]组成的一维数组的指针,只能指向这3个元素,而不能指向第1层每一行的数组元素。下面对几种情况进行分析。

(1)数组名a既表示二维数组首元素地址,又表示第2层数组首元素地址,“p=a;”使p指向第2层数组首元素。

(2) a+0表示第2层一维数组首元素指针,“p=a+0;”同样使p指向第2层数组首元素。

(3) a[0]、a[1]、a[2]表示3个行一维数组的数组名,组成逻辑层的一维数组,虽然不占据存储单元,不具有物理地址,但p=&a[0]属于一种逻辑操作,其意义使p指向逻辑层的一维数组的首元素。

(4) a[0]虽表示第0行首元素的地址,但它毕竟属于第2层一维数组的元素,将数组元素直接赋指针变量是不符合规定的,所以出错。

(5)语句“int(*p)[4];”定义了p是指向行一维数组的指针,&a[0][0]表示二维数组首元素地址,“p=&a[0][0];”使p指向了二维数组元素,所以出现指向类型错误。

8.4.3二维数组指针作函数参数

例8.11一个班有n个学生,开设m门课程,计算总平均分数,并输出任一学生各门课成绩。

编程思路:

(1)为了突出程序设计方法和程序结构,而不被众多的数据干扰,先设有3个学生、4门课程。在此基础上,只要添加数据,设置相应变量的初值,就能实现更多学生、更多课程的处理。

(2)班级成绩表显然属于二维数据表格,按功能设计处理函数,按求总平均分数设计一个处理函数,按输出一个学生各门课成绩设计一个处理函数,数组指针作函数参数,减少数据传递数量,提高程序效率。

运行结果:

分析:

(1)求平均分数函数average定义中,第一个形参p定义为指向实型数组元素的指针,第2个形参n是二维数组元素的个数,即全班学生所有课程的成绩数目。函数调用时,第1个实参*score,是第2层逻辑一维数组首元素的引用,即score[0],也就是&score[0][0],即score[0][0]的地址,使形参p指向成绩数组首元素。

(2)查找并输出任一个学生成绩函数search定义中,第1个参数定义p为指向第2层行一维数组的指针,第2个参数n表示学号。函数调用时,第1实参是score,也代表第2层行一维数组首元素score[0]的指针,使形参p指向第0个学生的第0门课程成绩。*(*(p+n)+i)表示第n个学生的第i门课程分数,即元素score[n][i]的值。

特别强调:实参和形参如果是指针类型,应当注意它们类型的一致性。不能把int*型的指针(即元素的地址)传给int(*)[]型(指向一维数组)的指针变量,反之亦然。例如,函数调用score(score,2)置换成score(*score,2)就产生错误。因为*score==score[0]==&score[0][0]是二维数组首元素地址,而形参是int(*)[]型(指向一维数组)的指针变量。

例8.12在例8.11的基础上,查找有一门以上课程不及格的学生,并输出这些学生全部课程的成绩。运行结果:

分析:search函数定义中,第一个形参p定义为指向第2层一维数组的指针,第2个形参n是班级学生数。在函数中定义了一个标志变量flag,在内循环外赋初值0,在内循环中若查出当前查找的学生有不及格课程,则赋值1。结束内循环后,检查标志,如果flag为1,则通过一个循环输出该学生的号码和各门课程成绩。函数调用时,传递实参score,即score[0]的地址,使形参p指向score数组的第0行。

8.5通过指针引用字符串

8.5.1字符串的存储结构与指针

如何建立字符串中字符与其指针的对应关系?

在字符数组一节中已经介绍过,字符串是按字符数组进行存储的,只是在最后一个字符后加存一个字符串结束符“\0”。例如,charstring[]="IloveChina!",其存储结构如图8.10所示。根据数组指针的概念,字符串指针就是其存储的首地址,字符数组名也表示字符串的首地址。

图8.10字符串存储结构与指针8.5.2通过指针引用字符串

1.通过数组名(指针)引用

在字符数组一节中已经介绍,通过字符数组名可以引用字符串,使用“%s”格式描述符,可以整体输入或输出,只不过没有引入指针的概念。字符数组名就表示字符串指针,通过指针可以对字符串及字符串中的字符进行引用。下面通过一个例子说明引用方法。

例8.13定义一个字符数组,存放字符串“Ilovechina!”,输出该字符串和第8个元素。

分析:在“printf("%s\n",string);”中使用%s格式描述符和数组名(指针)对字符串整体输出,这里与数值型数组不同。在C语言中,“%s”格式描述符与指针配合,能实现字符串的整体输出或输入。先使指针指向串的首字符输出,指针自动加1,指向下一个字符,依次输出,直到字符串结束符“\0”。在“printf("No.8%c\n",*(string+7);”中的“*(string+7)”是用指针引用字符串中第7个元素string[7],因数组元素编号从0开始,实际上就是串中第8个字符。

2.通过字符串指针变量引用

指向字符串的指针变量,也就是指向字符数组的指针变量。指向字符数组的指针变量也必须先定义后使用,而且指针变量只有赋初值后才能建立指向。

字符型指针变量定义的一般形式为

char*指针变量名;

其中,char属于基类型,规定了指针变量指向字符型数据(即指向字符串),其他部分同指针变量定义的意义一样。

给字符型指针变量赋初值的方式也有两种:一是定义的同时赋初值;二是先定义,再赋初值。

分析:与例8.13对比,只是定义了字符型指针变量,用指针变量来引用字符串和字符串中的字符。运行结果也完全一样。但仍要注意,用数组名作指针,其地址值是固定的,而用字符型指针变量,其值是可以变化的。在本例中体现得不是太明显,但在比较复杂的问题中就会表现出来。

例8.15将字符串a复制到字符串b中,再输出字符串b。

编程思路:对字符串的输入或输出,可以使用%s格式描述符,进行整体的输入或输出。但对字符串的其他操作,只能对字符串中的字符逐个进行处理,即对字符数组中的元素逐个进行处理。所以,对字符串的处理,常采用循环结构。下面分别通过指针引用法和指针变量引用法来编写程序。运行结果:

分析:

①数组b的长度应大于等于数组a,否则无法完整复制。

②复制循环中使用了指针引用法,把a数组中的字符依次逐个复制到b数组中,循环结束的判定条件是字符串结束符“\0”,不能把结束符复制到b数组中。所以循环结束后,向b数组后添加结束符“\0”。

③输出循环中使用了数组下标法,可以与指针引用法比较。分析:该程序的运行结果同(1)。循环复制中使用指针变量法实现逐个字符复制,指针变量既作指针,又作循环控制变量,使两个p1、p2同步移动。这也是指针变量使用的一种技巧。两字符串的输出仍采用了数组名(指针法),体现了通过指针引用字符串的多种方法。8.5.3字符指针作函数参数

例8.16用函数调用实现字符串的复制。

编程思路:定义一个函数来实现字符串复制的功能,在主函数中提供源串,调用复制函数后,输出源串和目标串。函数参数可以是字符数组名(字符串指针)或字符指针变量。下面分别给出实参和形参4种组合关系的程序,以供分析比较,体会编程方法与技巧。

(1)实参、形参都用字符数组名(指针)。

运行结果:

分析:程序中函数实参和形参都是数组名,系统仍把形参数组名作指针变量处理。在函数调用时,将a和b第1个字符的地址分别传给形参from和to,使from指向字符串a,to指向字符串b。函数调用后,字符串b产生变化。函数调用前后字符串数组的状态如图8.11(a)和(b)所示。从图中可以看到,由于b数组原来长度大于a数组,在a数组复制到b数组后,未能全部覆盖b数组原有内容。b数组最后3个元素仍保留原状。在输出b时,图8.11函数调用前后的字符数组状态由于按%s格式输出,遇到“\0”即结束,因此第1个“\0”后的字符不输出。如果采用%c格式,则后面的字符是可以输出的。

(2)实参用字符数组名,形参用字符指针变量。分析:程序的运行结果同(1)。形参是指针变量,调用时接收参数传递的源串和目标串的首地址,利用指针变量加1调整运算,能很方便地使用for循环来实现逐个字符的复制。

(3)实参用字符指针变量,形参用字符数组名。

分析:程序的运行结果同(1)。参数传递的是指针变量,但被赋给字符数组名,调用时,传递的是字符串首地址。形参是数组名,编译系统按指针变量看待,可接收实参传递的字符串首地址,在字符复制循环中用“下标法”实现逐个字符复制。

分析:程序的运行结果同(1)。参数传递及函数调用过程请读者自行分析。

字符型指针作函数参数,引用字符串元素的方法有多种,给程序设计带来许多技巧。下面通过对copy_string函数的改写来说明程序设计的方法与技巧。

对copy_string函数可有如下一些改写,都能保持功能不变。

8.6通过指针调用函数

8.6.1函数指针与指针变量的定义

何谓函数指针?在程序中如何使用函数指针或函数指针变量?

函数的指针就是函数代码存储的起始地址,也就是函数调用时的入口地址。可以通过指向函数入口的指针变量(或简称指向函数的指针变量)来调用函数。

指向函数的指针变量又是一种新类型的指针变量,也必须先定义后使用。指向函数的指针变量定义的一般形式为

基类型符(*变量名)(函数参数表列)其中,基类型符是指函数返回值的类型。变量名前加*表示是指针变量,两侧再加括号,连同后面的括号内的函数参数表列一起,表示所定义的变量是指向返回值为基类型的函数的指针变量。也就是说,变量名两侧的括号和其后的括号与参数列表都不能遗漏。

例如:

int(*p)(int,int);

定义了指向返回值为整型、有两个整型参数的函数的指针变量p。与一般指针变量定义和使用一样,定义了一个指向函数的指针变量后,并没有指向哪一个函数,只有当赋初值后,才能建立起明确的指向。如果把一个已定义的函数名赋给已定义的函数的指针变量,则使指针变量与函数建立了明确的指向。8.6.2通过函数指针调用函数

如何通过函数指针或指针变量调用函数?

通过函数指针调用函数有两种方式:一是通过函数指针(函数名);二是通过指向函数的指针变量。通过函数名调用已经介绍过,因函数名仅表示一个固定地址,故只能调用一个固定的函数。指向函数的指针变量的值可以变化,可使其指向不同的函数,分别实现不同函数的调用。

例8.17用函数求两整数中的大数。

编程思路:定义一个函数,通过比较求出两个整数中的大数。在主函数中定义指向函数的指针变量,通过指针变量调用函数,得到两数中的大数。

分析:程序第5行定义了指向返回值为整型、有两个整型参数函数的指针变量p。第7行将函数名表示的函数起始地址赋给指针变量p,使其指向函数max。第10行“c=(*p)(a,b);”是指向函数的指针变量对函数的调用,a、b是实参,把函数的返回值赋给变量c。函数调用过程及参数传递同函数名调用。如果将“c=(*p)(a,b);”置换为“c=max(a,b);”且不需定义指向函数的指针变量和赋初值语句,程序的执行结果完全一样。这个例子说明了通过指向函数的指针变量调用函数的方法,但看不出通过指向函数的指针变量调用函数的优越性。在后面将会看到,利用指针变量值的可变性,可以实现一类函数的调用。

8.6.3用指向函数的指针作函数的参数

例8.18实现两个整数的加、减、乘、除运算,用户通过输入1、2、3、4分别进行加法、减法、乘法、除法运算并输出结果。

编程思路:分别设计实现两整数加、减、乘、除的函数,再设计一个参数为指针的函数,在主函数中根据用户的选择,传递相应运算函数的指针,形参接收函数指针后,调用不同的运算函数。

分析:在arith函数定义中,x、y是两个接收运算数的形参,int(*p)(int,int)是指向函数的指针变量,接收函数的指针。在函数体中,由指针调用相应运算函数,相当于构建了一个四则运算类函数。

程序运行时,在主函数中接收用户的运算选择,switch语句依据选择调用arith函数,把两运算数和相应功能运算函数指针传递给形参,由“result=(*p)(x,y);”语句调用相应功能运算函数。显然,p是指向函数的指针变量,可以接收不同函数的指针(函数入口地址),通过它来实现多个函数的调用。参数传递及p的指向如图8.12所示。

图8.12例8.17参数传递及指针变量的指向8.6.4返回指针值的函数

函数返回指针值,调用函数与被调函数间建立什么联系?

函数的返回值可以是指针。指针作为函数的返回值,可以减少函数间传递的数据量,是提高程序效率的有效方法。特别是在主调函数和被调函数都对一个数组进行操作的情况下,数组指针作函数的返回值,在主调函数中,通过返回指针就可以引用操作后的数组元素。

返回指针值的函数,要将返回值类型定义为指针型。其定义的一般形式为

基类型符*函数名(参数表列);显然,返回指针值函数的定义和返回基本类型值函数定义的区别仅在于基类型符和函数名之间加了一个“*”。这样,就定义了该函数是返回指向一种基类型数据的指针。从C语言运算符优先级来讲,因()优先级高于*,所以数组名先与()结合,显然这是函数形式。函数前面有一个*,表示此函数是指针型函数(函数值是指针)。最前面的基类型是返回的指针所指向数据的类型。

下面通过例子来说明使用返回指针值函数的程序设计方法。

例8.19

编程实现一个班学生课程成绩的查询。要求输入一个学生的序号后,能输出该学生全部课程的成绩。用指针型函数来实现。

编程思路:一个班学生成绩表用二维数组来表示。定义一个查询函数,根据学生序号,确定一个指向该学生各门课成绩行的指针,此指针作为函数返回值。在主函数中,通过返回指针可输出该学生的各门课成绩。为简化数据而突出程序的设计方法,假设一个班有3个学生、4门课成绩。

分析:函数search定义为指针型函数,形参pointer是指向包含4个实型数据元素的一维数组的指针变量。函数中,定义pt是指向变量的指针变量。*(pointer+n)是引用第2层逻辑一维数组的第n个元素,即得到第n行首元素地址,把此地址赋给变量pt,作为函数返回值。主函数中,调用查询函数,将返回指针赋给p,在for循环中,通过p输出所指向学生的各门课成绩。请注意程序中所定义的指针变量p、pt和pointer的区别。

例8.20

在一个班学生成绩表中,查找不及格课程的学生,输出序号及成绩。

编程思路:在例8.19的基础上,定义查询函数为查找一个学生有无不及格成绩。如有则确定一个指向该学生成绩行首的指针;如无,则设置指针为空。指针作为函数返回值。在主函数中,要按学生序号逐一调用查询函数,根据返回指针值判定是否有不及格成绩。若有,则通过返回指针输出该学生各门课成绩。若无则不输出。分析:定义函数search检查一个学生有无不及格成绩。形参pointer是指向包含4个实型数据元素的一维数组的指针变量。函数中,定义pt是指向实型变量的指针变量。循环实现检查一个学生每门课成绩是否低于60。若是,则将该学生成绩行首地址赋给pt;若无,则pt为NULL。函数调用后,返回pt值。在主函数中将pt值赋给p,if语句判定p的值是否等于一个学生成绩行首地址*(score+i)。若是,则该学生有不及格成绩,用for循环输出该学生各门课成绩。若不是,则该学生无不及格成绩,不输出。

8.7多重指针与指针数组

何谓多重指针?怎样通过多重指针引用数据?

在本章开头已经介绍,通过指针变量来引用另一变量值的方式是间接访问。这属于“单级间址”。如果再有一个指针变量,使其指向目标变量的指针变量,就形成了“二级间址”的方法,即形成了二重指针。从理论上讲,间址方法可以延伸到更多的层级,即多重指针。多重指针及其指向关系如图8.13所示。在实际应用中,很少使用超过二级的间址。间址层级越多,越难理解,出错机会也会增多。

图8.13多重指针示意图8.7.1指针数组

若一个数组的元素均为指针类型数据,则称为指针数组。也就是说,指针数组中每一个元素都存放一个地址,相当于一个指针变量。

使用指针数组也必须先定义。指针数组定义的一般形式为

基类型符*数组名[数组长度];

容易看出,指针数组与基本数据类型数组定义的区别在于:基类型符与数组名之间加了一个“*”,表示数组元素是指针类型,这些指针都指向基类型数据。从C语言运算符优先级看,由于[]比*优先级高,因此数组名先与[数组长度]结合,这显然是数组形式,再与*结合,表示此数组是指针类型。

例如:

int*a[4];

定义了数组a,包含4个指针类型元素,这些指针元素都指向整型数据。请注意指针数组定义与指向一维数组的指针变量定义的区别。如果写成“int(*a)[4];”,就成为指向一维数组的指针变量的定义了。

指针数组最典型的应用是:指向若干个字符串,使字符串处理更加方便灵活。例如,图书馆有若干本书,把书名放在一起组成书目,经常需要对书目进行排序和查询。因字符串本身就是一个字符数组,存放书目的多个字符串需要设计一个二维字符数组。但在定义二维数组时,需要确定列数,一旦列数设定,二维数组中每一行包含的元素个数就是相等的。实际上,书名字符串的长度一般是不相等的。如果按最长的字符串来定义列数,则会浪费许多存储单元,而且在进行字符串处理时也会降低效率。使用指针数组,可以分别定义一些字符串,用指针数组的元素分别指向各字符串,如图8.14所示。这样能有效解决上述问题。

图8.14多个字符串的存储及与指针数组的指向示意图

例8.21将书目字符串按字母顺序输出。

编程思路:定义一个指针数组,用各字符串对其进行初始化,即把各字符串首字符地址赋给指针数组各元素。用选择法排序,不移动字符串,而只改变指针数组各元素的指向。

分析:定义sort函数的作用是对字符串排序。形参name是指针数组名,接收实参传递的name数组0行首字符地址,使形参name和实参name指向同一个数组。用选择法对字符串排序。strcmp是系统提供的字符串比较函数,name[k]和name[j]是第k个和第j个字符串首字符的地址。strcmp(name[k],name[j])的值为:如果name[k]所指的字符串大于name[j]所指的字符串,其值为正;若相等,其值为0;若小于,其值为负。if语句的作用是比较两个字符串,并将小串的序号记录在变量k中。当执行完内循环for语句后,第k串最小。若k≠i,就表示最小串不是第i串。故将name[i]和name[k]对换,也就是将其指向互换。执行完sort函数后,指针数组元素的指向如图8.15所示。图8.15排序后指针数组元素的指向

printf的作用是输出排好序的字符串。因name[0]~name[4]分别指向从小到大排好序的字符串,所以按指针数组元素顺序,用“%s”格式控制,就能输出排好序的字符串。

8.7.2指向指针数据的指针

指针数组的存储同基本类型数组一样,按元素顺序连续存储在一个存储区,每个元素都有一个存储地址。所以,可定义一个指向指针数组元素的指针变量,通过该指针变量可以引用指针数组中的元素。这种指针变量称为指向指针数据的指针变量。指向指针数据的指针变量定义的一般形式为

基类型符**变量名;

从C语言运算符的优先级来看,*运算符的结合性是从右到左,因此,“**变量名”相当于*(*变量名),显然“*变量名”是指针变量的定义形式,前面再加一个“*”,表示该指针变量指向指针型数据。

在前面定义书目指针数组的基础上,看下面语句的功能作用:

char**p;

p=name+2;

printf("%d\n",*p);

printf("%s\n",*p);

其中,第1个printf函数语句中,“*p”是引用字符型指针数组元素name[2]的值,输出的是一个地址。第2个printf函数语句中,“*p”与“%s”配合,输出的是name[2]所指向的字符串。

例8.22使用指向指针数据的指针变量输出多个字符串。

编程思路:定义一个指针数组,并对其初始化,使数组中的每一个元素分别指向5个字符串。定义一个指向指针型数据的指针变量,使其先后指向指针数组各元素,输出这些元素所指向的字符串。

运行结果:

分析:在主函数的开头,定义了指向字符的指针数组name,同时用字符串赋初值的方式初始化,分别将5个字符串的首地址赋给数组元素。又定义指向指针数据的指针变量p,在for循环中,赋值语句“p=name+i;”使p指向指针数组的第i元素,即第i个字符串的首地址。在“printf("%s\n",*p);”语句中使用“%s”,输出p所指向的字符串。上面针对指针数组元素指向字符串的典型应用举了两个例子。指针数组元素也可指向基本类型数据。下面通过一个简单例子来说明这种用法。

例8.23使用指针数组元素指向一个整型数组的元素,用指向指针数据的指针变量输出整型数组各元素的值。

运行结果:

分析:主函数中定义了一个整型数组a,又定义了一个指向整型数据的指针数组,同时对其初始化,将5个整型数组元素的地址赋给指针数组元素。在“printf("%d",**p);”语句中,“**p”属于二重间址访问,从p所指向的指针数组得到数据元素的地址,再由元素地址从数据数组中取得元素值输出。

8.8用于动态内存分配的指针型函数

8.8.1内存动态分配的函数

1.开辟动态存储空间函数(malloc)

该函数原型为

void*malloc(unsignedintsize);

其作用是在内存的动态存储区中分配一个长度为size字节的连续空间。此函数是指针型函数,返回值是所分配存储区域的起始地址,如果分配未成功,则返回空指针(NULL)。利用返回指针可以使用该存储区域中的数据。例如:

malloc(100);

开辟100字节的动态存储区,函数返回该区域首字节地址。

2.开辟数组动态存储空间函数(calloc)

该函数原型为

void*calloc(unsignedn,unsignedsize);

其作用是在内存的动态存储区中分配n个长度为size的连续空间,即可为n个元素的数组开辟一个动态存储区,每个元素的存储长度为size。函数的返回值是所分配存储区域的起始地址,如果分配未成功,则返回空指针(NULL)。利用返回指针可以使用该存储区域中的数据元素。例如:

p=calloc(50,4);

开辟

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

最新文档

评论

0/150

提交评论