厦门理工学院11级C语言-第07章-指针_第1页
厦门理工学院11级C语言-第07章-指针_第2页
厦门理工学院11级C语言-第07章-指针_第3页
厦门理工学院11级C语言-第07章-指针_第4页
厦门理工学院11级C语言-第07章-指针_第5页
已阅读5页,还剩146页未读 继续免费阅读

付费下载

下载本文档

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

文档简介

第七章指针指针在C语言中占有重要的地位,是最具有特色的语言成分,是C语言的精华。正确而灵活地使用它,可以有效地表示复杂的数据结构;能动态分配内存;直接处理内存地址;对内存中各种不同的数据结构进行快速处理,也为函数间各类数据的传递提供了简捷便利的方法。使用指针,可以编制出简洁明快、功能强和质量高的程序。但指针也是最有风险的。譬如,未初始化的指针可能造成系统的错误。也许夸张了,但指针确实很容易被误用,使得系统造成难以发现的错误。造成程序“挂死”的大部分原因都是由于错误地使用指针或数组越界所造成的。

C程序设计中使用指针可以: 使程序简洁、紧凑、高效有效地表示复杂的数据结构动态分配内存得到多于一个的函数返回值7.1指针的基本概念1.内存地址在计算机硬件系统的内存储器中,拥有大量的存储单元(以字节为单位)。为了便于管理,每个存储单元都有惟一的编号,这个编号就是存储单元的“地址”。例如,对16位机,DOS环境下的应用程序,其代码段、数据段和堆栈段放在位于内存地址0x0000~0xffff之间的640KB常规内存中。也就是说,程序中的某一变量,对应0x0000~0xfff范围内的某些存储单元。注:TC只能开发DOS下的16位命令行方式的应用程序。2.变量的存储回顾一下变量的定义:“变量代表内存中具有特定属性的一个存储单元,它用来存放数据,也就是变量的值,在程序运行期间,这些值是可以改变的,一个变量应该有一个名字,以便被引用。”下面以一个简单的程序讨论一下变量在内存中的存储情况。关键字:变量名、存放变量的内存单元格、内存单元格的地址、变量值7.1.1预备知识下面以一个简单的程序讨论一下变量在内存中的存储情况。关键字:变量名、存放变量的内存单元格、内存单元格的地址、变量值main(){

inta=1;floatb=2;

intc[2]={5,6};chard=’d’;}一个程序片段变量a变量b变量d数组c500000000H00000001H00000002H00000003H00000004H00000005H100000006H00000007H00000008H00000009H…...60000000AH00000009H‘d’2变量名a变量的地址存放变量的内存单元00000000H00000001H1变量的值图7-2变量a的局部示意图注:变量的地址是二进制的,为了便于书写而在这里写成对应的十六进制形式。等读者熟悉后在以后的章节中则会直接用十进制来书写,以便于阅读。程序中分别定义了4个变量:int变量a,float变量b,int数组c,char变量d,它们在内存中分别占据2个、4个、4个、1个字节。其中变量a的首地址是00000000H(这是假设,实际情况中程序定义的变量并不是从内存的0字节开始存放的),则变量b的首地址是00000002H,数组c的首地址是00000006H,变量d的首地址是0000000AH。首地址就简称为变量的地址。要访问变量首先就要知道变量的地址,可是通过数字形式的地址值访问变量,显然是不方便的(正如使用URL网址比IP地址要方便):不便于书写和记忆,而且数字本身没有什么具体的字面意义。需要了解硬件细节。比如当前哪些内存空间是空闲的等等。这就失去了高级语言容易使用、接近人类语言的优点。好在C语言提供了变量名,程序员通过变量名来访问变量,不需要知道变量的存储单元是如何开辟在内存的空闲区的,也不需要关心变量的实际存放地址。变量名和变量的地址之间由编译器和操作系统进行联系和转换(最终当然还是要通过地址对变量进行访问),这个转换过程对程序员来说是透明的。这样做显然是有好处的:变量名比地址好记而且可以表文达意,提高了程序的书写性和可读性;普通程序员可以把更多的精力放在程序的逻辑实现上而不需要过分关注计算机硬件系统的有关细节。这些也正是高级语言的优点之表现。注:常量是没有地址的。(以后通过对汇编语言的学习,我们可以了解到常量的存储)7.1.2指针就是地址它们具有双重含义什么是指针呢?指针其实就是地址!既然变量名比变量地址使用起来方便,那么为什么还要引入指针呢?这是因为指针可以给我们的程序带来意想不到的灵活度,随着本章的深入学习,您一定会体会到这句话的!“指针就是地址”,因此对指针的认识要建立在对地址的深刻理解之上。地址有两个方面的含义。地址值(也就是内存单元的编址)。是什么类型的数据的地址。这就存在着一个跨度也就是存储空间大小的问题。(我们已经知道,不同的数据类型其占据内存空间的大小是不同的。比如对于一个int变量的地址,应该是内存中某2个连续字节单元的首地址;如果是一个float变量的地址,那么该指针应该是内存中某4个连续字节单元的首地址)。7.1.3指针其名明白指针就是地址,这一点十分重要。多数情况下,这个地址是内存中另一个变量的位置。如果一个变量包含了另一个变量的地址,那么第1个变量就是个指针变量而且说它是“指向”第2个变量的,“指针”由此而得其名。例如,如果在地址为1000的变量指向地址为1004的变量,那么也就是说地址为1000的这个变量的值是1004。为什么要表达为“指向”呢?下一节中将会看到如果变量p的值是变量a的地址,则可以利用变量p来访问和操作变量a(其实这是很自然的事情,有了某变量的地址当然就可以访问该变量)。所以这样的变量p和a之间是有种联系的,这种联系就被表达为“指向”。图7-3解释了这一点,它仅仅用来对地址进行偏移。100410001001100210031004图7-3一个变量指向另一个变量内存单元内存地址7.1.4变量的指针与指针变量1.变量的指针一个变量x的地址就是该变量的指针,记作&x,即在变量名前加上取地址运算符“&”。例如,变量x的地址是0x2000,我们就说x的指针是0x2000。显然每个变量的地址或说指针都是客观存在的,而且是个常量。2.指针变量大家都知道整型变量就是存放整数的变量,同理,专门用来存放地址的变量称为指针变量。当指针变量中存放着某一个变量的地址时,就称这个指针变量指向那一个变量。由于地址或指针是常量,因此当我们需要对地址进行操作的时候一般要用指针变量来保存该地址再做处理。3.指针变量与它所指向的变量的关系指针变量和一般变量既有联系又与区别。指针变量也是变量,具有变量的特征,在内存中也占用一定的存储单元,也有“地址”和“值”的概念。但指针变量的“值”不同于一般变量的“值”,指针变量的“值”是另一实体(变量、数组或函数等)的地址。指针变量px与它所指向的整型变量x的关系,用指针运算符“*”表示为:“*px等价于变量x”

因此,下面四条语句的作用相同,都是将100赋给变量x:x=100;/*将100直接赋给变量x*/*px=100;/*将100间接赋给变量x*/*(&x)=100;/*将100间接赋给变量x*/*((int*)100h)=100/*将100间接赋给变量x*/(假设x的地址为100h)请考虑第4种访问方式:这里之所以要进行类型转换是因为100h只是个地址值,并没有关于其代表存储空间大小的描述,不具备我们前面讨论的地址的双重含义。为了把100放到以100h为起始地址的连续两个字节中,因此需要进行相应的类型转换,使得100h具有跨度上的含义后它作为一个地址意义才完整,也才能正确地利用它把100赋给变量x。4.指针变量的长度指针变量的长度可以是2个字节或4个字节,这取决于引用者和被引用者之间在内存的距离,通常由系统自动决定,程序员不必理会。(下述内容在学习汇编语言后更好理解,目前可暂不关注)编译系统根据设定的内存模式来安排代码段和数据段,由此确定指针变量的长度。内存模式取决于代码段和数据段的长度,早期的C系统将内存模式分为六种,如表7-1所示。表7-1内存模式模式代码段长度数据段及静态数据长度微型模式(TinyModel)代码段、数据段和数据组的总长度<64KB代码段、数据段和数据组的总长度<64KB小型模式(TinyModel)<64KB<64KB中型模式(TinyModel)无限制

<64KB紧凑型模式(TinyModel)<64KB无限制,但静态数据<64KB大型模式(TinyModel)无限制无限制,但静态数据<64KB巨型模式(TinyModel)无限制无限制当数据段(或代码段)的长度在64KB以内时,其地址长度不超过16位,因此保存这样地址的指针变量只需要2个字节即可。当数据段(或代码段)的长度超过64kB时,其地址长度超过16位,要保存这样的地址,指针变量需要4个字节。通常指针变量的长度由系统自动决定,程序员只需根据程序中代码和数据量的多少选用适当的内存模式即可,其他事都由系统自行处理。请注意:指针变量中不仅仅只能存放变量的地址,例如还能存放函数的入口地址等等,这里暂不作讨论。前面提到,常量是没有地址的。因此变量才有(指向它的)指针/指针变量,亦即指针/指针变量只能指向变量。因此“(指向)某类型的指针/指针变量”就是指“(指向)某类型的变量的指针/指针变量”。比如:“(指向)整型的指针/指针变量”就是指“(指向)整型变量的指针/指针变量”等等,以此类推。7.2指针变量的定义和赋值7.2.1指针变量的定义指针也是C的一种数据类型,指针变量的定义形式和前面学过的其他数据类型的变量之定义没有什么大的区别。

C数据类型的变量的一般定义形式类型名关键字变量名标识符[=初始值];注:[]中的是可选项定义指针变量的一般形式为:类型名关键字*变量名标识符[=初始值];注意为了与普通变量区别开来,在变量名前加”*”来说明它是指针变量。“*”并不属于指针变量名标识符的部分。“类型名关键字”是表示该指针是指向什么类型的变量的。既然指针有两个方面的含义,那么在指针变量的定义中是怎么体现这两方面的含义呢?例如:

inta=1;/*定义了一个变量a,那么变量a在内存的空闲区占据了2个字节的空间*/

int*p;/*(注意指针变量是p而不是*P)定义了一个指针变量,这个指针变量是指向int变量的(即是用来盛放int变量的地址的),所以这个指针变量中盛放的地址一定是内存中某2个连续的字节单元空间的首地址。这就把跨度含义赋予了该指针变量。*/p=&a;/*把int变量a的首地址赋予该指针变量,对指针的另一个含义“地址值”进行补充。*/

当然了,上面的三个语句也可以简化为

inta=1;

int*p=&a;

这样是在定义指针变量的同时就把地址值给了指针变量,效果是一样的。通过图7-4可以更好地理解什么是指针变量:图7-4变量在内存中的存储分配程序片段inta=10;……..……..(定义别的变量)int*p;p=&a;········00000000H00000001H········00000006H00000005H········00000065H········40000000H00000064H················a························P················1000000005H································变量名变量地址变量值在上图中可以看到,变量p在内存中占据2个字节。只不过它里面存放的是变量a的地址,这种关系表现为图7-5中的箭头,它表示p“指向”a。00000005H1指针变量p整型变量a,它的地址是00000005H图7-5指针变量p指向变量a7.2.2指针变量的赋值指针变量可以通过不同的方法获得一个地址值。不管是用什么方法,说白了就是用地址表达式给指针变量赋值,这个地址表达式可以是常量,可以是变量,也可以是函数的返回值。注意:指针具有双重含义,所以对指针变量赋值的地址表达式应该和定义指针变量的时候赋予的两方面的含义相符合,否则就会出错。1.通过取地址运算符“&”赋值地址运算符号“&”是单目运算符,运算对象放在地址运算符“&”的右边,用于求出运算对象的地址。通过地址运算“&”可以把一个变量的地址赋给指针变量。

floatf,*p;p=&f;

执行后把变量f的地址赋给指针变量p,指针变量p就指向了变量f。2.指针变量的初始化可以在定义变量时给指针变量赋初值,如floatf,*p=&f;,则把变量f的地址赋值给指针变量p,此语句相当于floatf,*p;p=&f;这两条语句。3.通过其他指针变量赋值可以通过赋值运算符,把一个指针变量的地址值赋给另一个指针变量,这样两个指针变量均指向同一变量。例如,有如下程序段:

inti,*p1=&i,*p2;p2=p1;

执行后指针变量p1与p2都指向整型变量i。注意,当把一个指针变量的地址值赋给另一个指针变量时,赋值号两边指针变量所指的数据类型必须相同。例如:

inti,*pi=&i;float*pf;

则语句pf=pi;是非法的,因为pf只能指向实型变量,而不能指向整型变量。4.用NULL给指针变量赋空值除了给指针变量赋地址值外,还可以给指针变量赋空值,如:

p=NULL;

NULL是在stdio.h头文件中定义的预定义标识符,因此在使用NULL的时候,应该在程序中加上文件包含“stdio.h”。在stdio.h头文件中NULL被定义成符号常量,与整数0对应。执行以上的赋值语句后,p为空指针(和指向void空类型的指针是不同的),在C语言中当指针值为NULL时,指针不指向任何有效数据,因此在程序中为了防止错误地使用指针来存取数据,常常在指针未使用之前,先赋初值为NULL。由于NULL与整数0想对应,所以下面三条语句等价:

p=NULL;或p=0;或p=’\0’;但通常都使用p=NULL;的形式,因为这条语句的可读性好。NULL可以赋值给指向任何类型的指针变量。5.通过调用标准库函数赋值可以调用库函数malloc、calloc等在内存中开辟动态存储单元,并把所开辟的动态存储单元的地址赋给指针变量。由于这两个函数返回的是“void*”无类型指针类型,因此将它们的返回值赋值给指针变量的时候要进行强制类型转换。注:这个动态存储单元到底如何“动态”以及malloc、calloc函数的用法将在链表部分进行讲述。7.2.3void指针前面讲过指针有两个方面的含义,两个含义必须同时存在指针才可以正常工作。ANSIC中有void类型,如果指针指向void类型,那么该指针就不指向任何实际的数据类型了。因此在实际需要的时候通过给它补充该含义,可以使它可以重新指向实际的数据类型。

void*p;这个p是个指针变量,但是并不明确它要指向什么类型的变量。

inta=1;p=(int*)&a;现在希望p存放int变量a的地址,也就是说想用p指向int变量a,那么把地址值和p要跨度两个方面的含义都赋予指针才完整。那么这样的void指针究竟有什么利用价值呢?它不指向任何实际的数据类型,也就是说它蜕化掉了指针“跨度”的含义,也就是说它指向何种类型的数据类型是不明确的,但是可以把它想象成指向一个抽象的数据类型。在将它的值赋给另一个指针变量的时候,要进行强制类型转换使之适合于被赋值的变量之数据类型。ANSIC标准规定用动态存储分配函数时返回void指针。这部分的内容在讲述动态建立链表的时候将深入学习,这里暂不作讨论。抽象数据类型在《数据结构》中也是一种重要的概念。7.3指针变量的使用指针变量的使用包括通过指针变量访问变量和移动指针。7.3.1指针运算符指针变量有两种运算:“&”、“*”。

1.取地址运算符“&”

这个运算符前面已经用到。例如赋值语句:

px=&x;

就是通过取地址运算符“&”,把变量x的地址赋给指针变量的,也就是使px指向x。2.指针运算符“*”(根据地址取变量值)对于图7-4中所示的程序。px指向x后,就可以通过px间接访问它所指向的变量x了。*px就等价于x,所以,以下两条赋值语句:*px=10;x=10;

是等价的,都是将10赋给x。同样,下两条语句:

printf(“x=%d”,x);

printf(“x=%d”,*px);

直接和间接方式输出变量x的值,因此,输出结果都是10。3.指针的算术运算指针可以加上或减去一个整数(常量或变量),达到改变地址值也就是对指针进行移动的目的。例如:

y是个整数,p是一个指针变量,如果它所指向的数据类型在内存中占据x个字节的存储空间,则p+y表示在p的地址值基础上前进x*y个字节。减法运算原理相似,是在p的地址值基础上后退。7.3.2变量的存取方式(1)直接访问C语言作为一种高级语言为了方便程序员使用,使用变量名访问变量,用变量名对变量进行访问叫做直接访问。

(2)间接访问变量名本质上还是要转换为地址而后访问变量。指针变量的值就是地址,所以可以通过指针变量来“间接”访问它所指向的变量。用指针变量对其所指向的变量进行访问的方式叫做间接访问。7.3.3停下来思考一下论语有云:“学而不思则罔,思而不学则殆”。一路学到这里可能有多人会问,前面说指针好,那指针到底好在哪里呢?下面我们就停下来思考一下,对前面的知识梳理一下,对后边的内容也做一个交代,以期起到承上启下的作用。首先,在7.1.1预备知识中已经分析过变量的直接访问方式的诸多优点,那么通过指针变量来实现对变量的间接访问不就是画蛇添足了吗?其实我们在函数一章中学习过变量的性质后很容易回答这个问题。下面我们先来看一个以前考虑过的实际问题:背景:王老师任教计算机a班和b班,a班有一个同学叫做小明,而b班没有。今天王老师去b班上课,在b班的课堂上王老师突然想找小明。(这在C程序中好比在一个函数中却想访问或操纵另一个函数中的局部变量)思考:王老师是不能用“小明”这个名字来访问该同学的,这是因为小明这个名字根本不存在于b班。这就好比小明是定义在a班的范围内的一个局部变量,而现在是在b班的范围内,当然不能通过小王来访问所对应的同学了,因为我们已经知道局部变量在此函数之外是无效或说是不可见的。更术语地讲可以说小明这个名字的“名字空间”是a班,在a班的范围内,王老师引用小明,会找到对应的同学,在b班则不行。所以王老师对班长说“帮我把小明同学找来”,是达不到目的的。可以想到全校的同学都有唯一的学号,如果王老师此时用小明的学号,是可以访问小明的,比如王老师对班长说“帮我把9941083同学找来一下”是可以达到访问小明的目的的。因为该学号客观存在而且每个人都不一样。通过指针变量对它所指向的变量进行间接访问事实上和王老师通过学号达到访问另一个范围内的同学之情况是相似的。换句话说,利用指针变量对变量进行间接访问,可以跨越名字空间访问变量。这听起来好象显的很神秘,其实是很自然的事情,因为在内存中变量的地址是唯一的,正如学生的学号一样,只要该变量仍然客观存在着就行。在下一节我们就可以看到这个特性能给我们带来什么便捷。当然了,指针的灵活之处还远不止于此,我们将会慢慢学来。到现在您还跟得上进度吗?7.3.4指针变量作为函数参数大家已经知道,C语言中函数参数的传递方式是“值传递”。指针变量作为函数的实参时,由于指针变量的值实际上是它所指向的变量的地址,所以传递的是指针所指向的变量的地址,与此相对应函数的形参也应是指针变量。前面的程序例6-18中的change函数并不能交换a和b的值,下面我们通过一个程序看看使用指针变量来间接访问变量能带来什么好处。例7-1编制函数change,交换两个变量的值。voidchange(int*m,int*n){

inttemp;temp=*m;*m=*n;*n=temp;}voidmain(){

inta=1,b=2;

change(&a,&b);

printf("a=%d,b=%d",a,b);}图7-6程序的内存示意图10010210010221释放maab12100102n204206temp随机值208b100102mn100102204206temp1208指向值地址temp=*m;*m=*n;*n=temp;ab2100102mn204206temp2208释放释放change返回调用change(&a,&b);inta=1,b=2;1运行后观察显示结果可以发现变量a和b被成功地交换了。图7.6展示了程序运行过程中各个变量存储单元中的变化。其实原因很简单,因为在程序6_3.c中对变量直接访问,main函数中的a和b与change函数中的m和n只不过值相同罢了,除了值的单向传递他们之间没有任何关系,因此对m和n的改变不能反映在a和b上。而程序7_1.c中,变量m和n中存放的是变量a和b的地址,它们有“指向”的关系,那么通过*m和*n就找到并可以访问变量a和b了。从宏观上来看,变量a、b是定义在main函数中的局部变量,只有在main函数中可以访问它们,在change函数中则不能通过变量名a、b达到访问它们的目的,正如我们学过的变量之有效性范围。可是main函数在调用change函数的时候把变量a、b在内存中的地址以函数实参的形式“告诉”给了change函数,因此在change函数中就可以通过指针变量来“指向”变量a和b从而达到访问它们的目的了。7.4指针与数组

C语言中指针与数组有着极为密切的联系。引用数组元素可以用下标法,也可以用指针法。两者相比而言,下标法易于理解,适合于初学者;而指针表示法有利于提高程序的执行效率。7.4.1数组和数组元素的指针数组的指针是指数组在内存中的地址,数组元素的指针是数组元素在内存中的地址。我们已经知道数组名就是数组的起始地址,其实C语言规定它是下标为0的元素的起始地址,而不是数组的指针。尽管如果仅从首地址值的角度考虑,数组名、数组的指针,以及下标为0的数组元素的指针都相同。但是如果考虑到指针的完整定义,则其实数组名仅仅是下标为0的元素的指针。另外,请注意它们都是常量。1.数组元素的指针例如,intdata[6];

则C语言规定:数组名data是指针常量,它代表的是数组第一个元素data[0]的指针。前一节我们已经讨论过指针的算术运算,现在容易知道data+i其实也就是data[i]的首地址(i=0,1,2,···,5),即data+i与&data[i]等价。与简单变量类似,数组元素data[i]的首地址&data[i]就称为data[i]的指针。所以data+i也是data[i]的指针,简称为data+i指向data[i]。因而引用数组元素时,可用*data、*(data+1)、····*(data+5)的方式,如图7-7(a)所示。图7-7用指针引用数组元素datadata+1data+2data+3data+4data+5data[0]data[1]data[2]data[3]data[4]data[5](a)pp+1p+2p+3p+4p+5data[0]data[1]data[2]data[3]data[4]data[5](b)所以,以下两个循环输出语句完全等价:

for(i=0;i<6;i++)printf(“%4d”,data[i]);

for(i=0;i<6;i++)printf(“%4d”,*(data+i));2.数组的指针那么我们怎么获得数组的指针呢?其实在下标为0的数组元素的指针上(也就是数组名)作个类型转换即可。例如对上述的数组intdata[6]来说,((int*)[6])data就是数组data的指针。一般来说在二维和多维数组中,我们经常会用到数组的指针和指向数组的指针变量,对于它们来说,可能其值和某数组元素的首地址相等,但是其含义是不同的,例如((int*)[6])data+1则表示指针前进6*2=12个字节,而不是前进2个字节,很明显它和数组元素的指针在跨度含义上是不同的。分析:对于强制类型转换((int*)[6])data,考虑到运算符的优先级和结合性,容易知道data被转换成了指向拥有6个元素的整型数组的指针。7.4.2指向数组和数组元素的指针变量1.指向数组元素的指针变量指向某数组元素的指针变量就是指向该数组元素的指针变量。定义一个指向数组元素的指针变量的方法,与以前介绍的指向变量的指针变量相同。而且由于数组的元素就是某种类型的变量,因此指向数组元素的指针变量在定义形式上就是指向该类型变量的指针变量,只不过需要利用数组元素的地址对其赋值罢了。例如:intdata[6];/*定义data为包含6个整型数据的数组*/int*p;/*定义p为指向整型变量的指针变量*/则语句:p=&data[0];(或p=data;)就把data[0]元素的地址赋给指针变量p,即p指向data数组的第0号元素(也可说p指向data数组)。同样,语句p=&data[i];就使p指向data数组的第i号元素。如果p的初值为&data[0],则p+i就是data[i]的地址&data[i](i=0,1,2,···,5)。如果p指向数组中的一个元素,则p+1就指向同一数组的下一个元素。p+1所代表的地址实际上是p+1×d,d是一个数组元素所占字节数(对整数型,d=2;对实型,d=4;对字符型,d=1)如图7-7(b)所示。2.指向一维数组的指针变量指向数组的指针变量要略显复杂一些,但如果我们把握住指针的本质,很容易想到它具有的两个属性分别是:其值是数组的首地址,亦即下标为0的数组元素的地址。它是指向数组的,而不是数组元素。因此它的跨度是整个数组而不是一个数组元素。下面定义一个指向数组的指针变量。

int(*p)[6]

intdata[5][6];p=data[2];p++应该记住,此时p只能指向一个包含6个元素的一维数组。p的值就是该一维数组的起始地址(亦即下标为0的数组元素的地址),p不能指向一维数组中的某一元素。指向一维数组的指针变量一般用于对二维数组或多维数组的操作,请务必主意指针变量的类型。3.指向数组元素的指针变量,在使用中应注意的问题(1)指针变量可以通过本身值的改变(如p++)来指向数组中的不同元素。但是数组名是地址常量不能改变本身的值,如果写data++则是错误的;(2)应注意下面的几种指针运算形式:*p++等价于*(p++),作用是先得到p所指向的变量的值(即*p),然后再使p加1。*(p++)与*(++p)不同,前者是先取得*p的值,后使p加1;后者是先使p加1,再取*p的值。若p初值为&data[0],输出*(p++)时,得data[0]的值,而输出*(++p),则得到data[1]的值。总结以上两点可知,如果p当前指向data数组中第i个元素,则:*(p++)相当于data[i++],先对p进行“*”运算,再使p自增。*(p--)相当于data[i--],先对p进行“*”运算,再使p自减。*(++p)相当于data[++i],先使p自增,再对p进行“*”运算。data[0]p1data[1]data[2]data[3]data[4]data[5]p2data数组图7-8指针算术、关系运算10021010*(--p)相当于data[--i],先使p自减,再对p进行“*”运算。

(3)(*p)++表示p所指向的元素值加1,注意是元素值加1,而不是指针值加1。比如,如果p所指向的元素为data[3],且data[3]的值为9,则(*p)++表示将data[3]单元中的值加1,变成10,而p仍指向元素data[3],也就是说,p中的地址值并没有改变。

(4)p+n和p-n:将指针从当前位置前进或回退n个元素,而不是n个字节。显然,p++、p--(或++p、--p)是p+n(p-n)的特例(n=1)。

(5)p2-p1表示两指针之间的数组元素个数,而不是指针的地址之差,即图11-8中,p2-p1为4。

(6)两指针之间可以进行关系运算,如果p1指向data[i],p2指向data[j],并且i<j,则p1<p2为“真”,反之亦然,即图11-8中,p1<p2为真。data[0]p1data[1]data[2]data[3]data[4]data[5]p2data数组图7-8指针算术、关系运算100210107.4.3数组元素的引用数组元素的引用,既可用下标法,也可以用指针法。下标法简单直观;而指针法能使得目标程序简短(特别是采用指针变量的自增自减运算时)、运算速度快。例如,若有如下定义

intdata[6];

int*p=data;

则指针和数组之间有如下恒等式:

data+i==&data[i]==p+i(i=0,1,···,5)

data[i]==*(data+i)==*(p+i)==p[i](i=0,1,···,5)所以,引用数组第i个元素,有以下访问方式:(1)下标法数组名下标法:data[i]指针变量下标法:p[i](2)指针法数组名指针法:*(data+i)指针变量指针法:*(p+i)例如,下面4条语句的作用都是将20赋给data[5]元素。

data[5]=20;*(data+5)=20;*(p+5)=20;p[5]=20;例7-2用下标法和指针法引用数组元素。#include<stdio.h>main(){

intdata[6]={0,3,6,9,12,15};

int*p=data,i;

for(i=0;i<6;i++)

printf(i==5?"%d\n":"%d",data[i]);/*数组名下标法*/

for(i=0;i<6;i++)

printf(i==5?"%d\n":"%d",*(data+i));/*数组名指针法*/

for(i=0;i<6;i++)

printf(i==5?"%d\n":"%d",p[i]);/*指针变量下标法*/

for(i=0;i<6;i++)

printf(i==5?"%d\n":"%d",*(p+i));/*指针变量指针法*/}运行结果为:03691215036912150369121503691215

说明:(1)程序中用下标法和指针法的四种方式引用数组元素,结果完全一样,说明它们是完全等价的。printf()语句中的格式字符串是一个条件表达式,选择两种输出格式之一,输出最后一个元素时同时输出回车换行,输出其他元素时同时输出空格而不是回车换行。(2)这4种方法,前面两种的执行效率是一样的。编译程序是将data[i]转换为*(data+i)处理的;而后两种方法比前两种效率高,用指针变量直接指向元素,不必每次都重新计算地址,并且象p++这样的自加操作是比较快的。7.4.4数组名作为函数参数当数组名作为函数参数时,在函数调用时,实际传递给函数的是该数组的起始地址,即指针值。所以,实参可以是数组名或指向数组的指针变量。而被调用函数的形参,既可以说明为数组也可以说明为指针。由于数组名就是数组的首地址,因此,函数的实参和形参都可以使用指向数组的指针变量或数组名,于是函数实参和形参的配合上有四种等价形式,这4种等价形式在本质上是一种,即指针变量做函数参数。(1)实参和形参都用数组名voiddata_put(int

str[],intn){inti;for(i=0;i<n;i++)printf("\n%d",str[i]);}main(){inta[6]={1,2,3,4,5,6};data_put(a,6);}(2)实参用数组名,形参用指针voiddata_put(int*str,intn){inti;for(i=0;i<n;i++)printf("\n%d",*(str+i));}main(){inta[6]={1,2,3,4,5,6};data_put(a,6);}(3)实参用指针,形参用数组名利用下标引用指针变量所指数组元素voiddata_put(int

str[],intn){inti;for(i=0;i<n;i++)printf("\n%d",str[i]);}main(){inta[6]={1,2,3,4,5,6};Int*p=a;data_put(p,6);}(4)实参和形参都用指针voiddata_put(int*str,intn){inti;for(i=0;i<n;i++)printf("\n%d",*(str+i));}main(){inta[6]={1,2,3,4,5,6};int*p=a;data_put(p,6);}说明当数组名作为函数参数时,形参无论用数组名还是用指针,在函数体中对数组元素的存取操作,既可以用指针法也可以用下标发。这留给读者验证。7.4.5字符串的指针和指向字符串的指针变量

C语言中没有字符串类型,字符串是以字符数组的形式给出的,而数组可以用指针进行访问,所以,字符串也可以用指针进行访问。1.字符串的表示和引用(1)用字符数组存放一个字符串#include<stdio.h>voidmain(){charstring[]="Thisisastring";/*字符数组存放字符串*/

printf("\n%s",string);/*整体引用输出*/

printf("\n");

for(i=0;*(string+i)!='\0';i++)

printf("%c",*(string+i));/*逐个引用*/}程序的执行结果如下:ThisisastringThisisastring(2)用字符指针指向一个字符串#include<stdio.h>voidmain(){

inti;char*p="Thisisastring";/*字符数组存放字符串*/

printf("%s\n",p);/*整体引用输出*/

for(i=0;p[i]!='\0';i++)

printf("%c",p[i]);/*逐个引用*/

printf("\n");for(;*p!='\0';p++)printf("%c",*p);}程序的执行结果如下:ThisisastringThisisastringThisisastring例7-3:用字符指针指向一个字符串说明:语句:char*p=”Thisisastring”;等价于下面两行:char*p;p=“Thisisastring”;

C语言对字符串常量是按字符数组处理的,在定义字符串常量“Thisisastring”时,在内存开辟了一个字符数组来存放它,并把首地址赋给字符指针p,如图7-9所示。Thisisastring\0图7-9字符串常量在内存中的存放p例7_4用指针方法,求字符串长度。main(){char*p,str[80];

intn;

printf("输入字符串:\n");

gets(str);p=str;while(*p!='\0')p++;n=p-str;

printf("字符串:%s的长度=%d\n",str,n);}2.字符串指针作函数参数当数组名作为函数参数时,在函数调用时,实际传递给函数的是该数组的起始地址,即指针值。这样,在函数形参说明中,就可以将数组形参说明为指针,并可以在函数体中通过指针存取或改变数组中的元素。例7_5编写一个合并两个字符串的函数下面给出三种方法。(1)将形参说明为数组,利用下标引用数组元素voidmystrcat(charstr1[],charstr2[]){

inti=0,j=0;while(str1[i]!='\0')i++;/*找到字符串str1的结束符*/while(str2[j]!='\0'){str1[i]=str2[j];i++;j++;}/*把字符串str2的内容复制到字符串str1中*/str1[i]='\0';/*添加字符串str1的结束符*/}(2)将形参说明为指针,利用指针引用数组元素voidmystrcat(char*str1,char*str2){while(*str1!='\0')str1++;while(*str2!='\0'){*str1=*str2;str1++;str2++;}*str1='\0';}(3)将形参说明为指针,利用下标引用指针变量所指数组元素voidmystrcat(char*str1,char*str2){

inti=0,j=0;while(str1[i]!='\0')i++;/*找到字符串str1的结束符*/while(str2[j]!='\0'){str1[i]=str2[j];i++;j++;}/*把字符串str2的内容复制到字符串str1中*/str1[i]='\0';/*添加字符串str1的结束符*/}主函数#include"stdio.h"main(){charsource[80]="StringOne",object[20]="stringtwo";

mystrcat(source,object);printf("str1+str2=%s",s

ource);}程序运行结果是:str1+str2=StringOnestringtwo7.4.6指针数组1.指针数组的定义与应用如果一个数组的每个元素都是指针类型的数据,则这种数组称为指针数组。指针数组定义的一般形式为:类型标识符*数组名[常量表达式];

例如:

char*p[10];表示p是一个指针数组,包括10个元素,每个元素都是字符型指针。在定义指针数组的同时也可以为其初始化。如:

char*name[]={“zhang

shan”,“Lishi”,“Wangwu”};

由初始表中的初值个数可以看出,name[]指针数组中共有3个元素,每个元素都是一个指针,如图7-10所示。ZhangshanLishiWnagsu图7-10指针数组name[0]name[1]name[2]其中name[0]指向字符串“Zhanshan”,name[1]指向字符串“Lishi”,name[2]指向字符串“Wangwu”。因此,语句:

printf(“%s,%s,%s\n”,name[0],name[1],name[2]);

将显示字符串:Zhanshan,Lishi,Wangwu

在程序设计中,经常使用指针数组显示菜单信息。下面的例子就是这方面的实际应用。[例7-5]利用指针数组显示菜单信息FileEditSearchOption/*7_5.c*/#include"stdio.h"main(){char*menu[]={"File","Edit","Search","Option"};

inti;

for(i=0;i<4;i++)printf("%s",menu[i]);}如果利用C语言库函数中的光标定位函数,则上述菜单信息可以显示在屏幕的任意位置上,有关这方面的内容,请查阅图形和用户界面方面的技术。2.指针数组作为mian()函数的形参指针数组的一个重要应用就是作为main()函数的形参,在前面的程序中,main()函数是无参函数。实际上,main()可带两个参数,其一般形式为:

main(int

argc,char*argv[])/*这里是形参罢了,名字是可以随便取的,比如int

x,char*y[]*/第一个参数是整型,第二个参数是作为指向字符的指针数组来处理的,那么这两个参数如何得到具体的值呢?我们知道,在DOS命令提示符下,可以键入一个可执行文件名,还可以带参数。如DOS命令:copyoldfile

newfile,将实现oldfile复制成newfile的功能。假如copy是用C语言实现的,则相应的argc表示是参数的个数(含可执行程序名),故argc的值为3,而指针数组argv中的元素argv[0]、argv[1]、argv[2]分别指向三个字符串“copy”、“oldfile”和“newfile”,如图11-11所示。copy\0wldfile\0newfile\0图7-11指针数组的应用:命令行参数argv[0]argv[1]argv[2]argv/*echo.c*/#include<stdio.h>voidmain(int

argc,char*argv[]){

inti;

for(i=1;i<argc;i++)printf("%s",argv[i]);

printf("\n");}例如,下面程序echo.c将其所带的参数显示出来,如在DOS状态下键入echooldfile

newfile,将回显oldfile

newfile。注意,若参数(含执行文件名)共有n个,则最后一个参数由指针argv[]指向。#include<stdio.h>voidUser(void){···/*提示用户正确的操作信息语句*/}例7-6编写带有帮助说明的程序,也就是要求当输入执行文件名,后跟“/?”时,将提示命令行的操作方法。voidmain(int

argc,char*argv[]){

inti;

if(argc==2)if(strcmp(argv[1],"/?")==0){user();return;}···/*其他代码*/}7.4.7指针与二维数组前面讲过,数组的指针是数组在内存中的起始地址,数组元素的指针是数组元素在内存中的起始地址;指向的数组的指针变量是指向该数组的指针变量,指向某数组元素的指针变量就是指向该数组元素的指针变量。对于多维数组虽然也是如此,但情况要复杂一些。实际上在程序中很少有用到超过二维的数组,因此在这里只讨论二维数组与指针的关系。1.二维数组和数组元素的地址

C语言的二维数组由若干个一维数组构成,即二维数组的每一个元素是一个一维数组。例如定义以下二维数组:

inta[3][4]={{1,3,5,7,},{9,11,13,15},{17,19,21,23,}};

a是一个数组名。a数组包含3行,即3个元素:a[0]、a[1]、a[2]。而每一个元素又是一个一维数组,每个一维数组又包含4个元素(即4个列元素),例如,a[0]所代表的一维数组又包含4个元素:a[0][0]、a[0][1]、a[0][2]、a[0][3],见图7-12。可以认为二维数组是“数组的数组”,即二维数组a是由3个一维数组所组成的。a[0]a[1]a[2]1917311195132171523图7-12二维数组是“数组的数组”从二维数组的角度来看,a代表二维数组首元素的地址,现在的首元素不是一个简单的整型元素,而是由4个整型元素所组成的一维数组,因此a代表的是首行(即第0行)的首地址。a+1代表第1行的首地址。如果二维数组的首行的首地址为2000,则在TurboC中,a+1为2008,因为第0行有4个整型数据,因此a+1的含义是a[1]的地址,即a+4×2=2008。a+2代表a[2]的首地址,它的值是2016,见图7-13。a数组a(2000)a+1a+2(2008)(2016)a[0]a[1]a[2]图7-13二维数组各行

a[0]、a[1]、a[2]既然是一维数组名,而C语言又规定了数组名代表数组首元素地址,因此a[0]代表一维数组a[0]中第0列元素的地址,即&a[0][0]。a[1]的值是&a[1][0],a[2]的值是&a[2][0]。请考虑0行1列元素的地址怎么表示?a[0]为一维数组名,该一维数组中序号为1的元素的地址显然应该用a[0]+1来表示,见图7-14。此时“a[0]+1”中的1代表1个列元素的字节数,即2个字节。今a[0]的值是2000,a[0]+1的值是2002(而不是2008)。这是因为现在是在一维数组范围内讨论问题的,正如有一个一维数组x,x+1是其第1个元素x[1]的地址一样。a[0]+0、a[0]+1、a[0]+2、a[0]+3分别是a[0][0]、a[0][1]、a[0][2]、a[0][3]元素的地址(即使&a[0][0]、&a[0][1]、&a[0][2]、&a[0][3])。aa+1a+2图7-14二维数组各元素2000120023200452008920101120121320161720181920202120067201415202223a[0]a[0]+1a[0]+2a[0]+3前面已经学过,a[0]和*(a+0)等价,a[1]和*(a+1)等价,a[i]和*(a+i)等价。因此,a[0]+1和*(a+0)+1都是&a[0][1](即图7-14中的2002)。a[1]+2和*(a+1)+2的值都是&a[1][2](即图7-14中的2012)。请注意不要将*(a+1)+2错写成*(a+1+2),后者变成*(a+3)了,相当于a[3]。进一步分析,欲得到a[0][1]的值,用地址法怎么表示呢?既然a[0]+1和*(a+0)+1是a[0][1]的地址,那么,*(a[0]+1)就是a[0][1]的值。同理,*(*(a+0)+1)或*(*a+1)也是a[0][1]的值。*(a[i]+j)或*(*(a+i)+j)是a[i][j]的值。请务必记住*(a+i)和a[i]是等价的。2.指向二维数组及其元素的指针变量在了解上面的概念后,可以用指针变量指向二维数组或二维数组的元素。例7-7用指针变量输出二维数组元素的值。/*7-7.c*/#include<stdio.h>voidmain(){

inta[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};

int*p=a;

inti;

for(i=0;i<=11;i++){if(i%4==0)printf(“\n”);printf("%4d",*(p+i));}

printf("\n");}程序运行结果是:1357911131517192123这个p是指向数组元素的指针变量,数组元素是的类型是int,因此p的定义形式是int*p=a;而且p+1是向后移动2个字节。例7-8用指针变量输出二维数组元素的值。#include<stdio.h>voidmain(){

inta[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};

inti=0,j=0;

int(*p)[4]=a;

for(i=0;i<3;i++)

for(j=0;j<4;j++){printf("%4d",*(*(p+i)+j));/*这里用p[i][j]也可以*/}

printf("\n");}程序运行结果是:1357911131517192123这个p是指向数组元素的指针变量,数组元素是的类型是int,因此p的定义形式是int*p=a;而且p+1是向后移动2个字节。请注意这里变量p的定义“int(*p)[4]=a;”,由于p先和运算符“*”结合,因此p是指向数组的指针变量。而如果是“int*p[4]”则由于p先与方括号结合,则p是个包含两个某种元素的数组,然后再与*结合,表示p是由两个指针构成的数组,而且指针是指向int类型的数据的,这样的p就成了一个指针数组了。要区分这两种形式,避免混淆。注:对于例7-8,为什么要把p定义成int(*p)[4]=a;而不能定义为int**p(见下节7.5.3的“3.思考”部分)在这里不作讨论。7.4.8数组的存储结构之本质前面我们对数组所作的讨论是针对数组的逻辑结构,而事实上,如果我们能从数组的存储结构上进行把握,那么多维数组可以彻底简化成简单的一维数组(而不是前面说过的“把二维数组看作一维数组”)。逻辑结构是针对逻辑、概念上的讨论;存储结构是针对其在内存中的储存方式进行的讨论。例7-9对二维数组元素的一种简单访问形式。/*7-9.c*/voidmain(){

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

int*p=&a[0][0];

printf("%d",*(p+8));}运行结果为:

9可见该程序能够通过*(p+8)访问二维数组的元素a[2][2]。很明显,此时的访问更象对一维数组元素的引用方式。那么为什么可以这样呢?事实上,无论是一维数组、二维数组还是多维数组,其数组元素事实上在内存中都是线性、连续地存放的,所谓的“维度”不过是逻辑上的概念,不会应为存在着“维度”而改变这种存储结构上的特征。因此,指针变量p指向了数组元素a[0][0],则只需通过对p进行简单的移动即可指向数组元素a[2][2],进而达到访问该数组元素的效果。在这个过程中,任何维度的数组没有任何本质上的区别。图7-15二维数组的存储结构和逻辑结构123456789a[0][0]a[0][1]a[0][2]a[1][0]a[1][1]a[1][2]a[2][0]a[2][1]a[2][2]第一行a[0]第二行a[1]第三行a[2]存储结构逻辑结构例7-10对二维数组元素的简单访问。/*7-10.c*/voidmain(){

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

intj;

int*p=&a[1][1];

for(j=0;j<4;j+=2)

printf("%d",p[j]);}运行结果为:

57思考:对于m行n列的二维数组a[m][n],如果知道了其某元素a[i][j]的地址,那么该数组任意元素a[x][y]的地址能用一个什么样的通用表达式来代表呢?(注意:m和n是整常量,且0≤i≤m-1,0≤x≤m-1,0≤j≤n-1,0≤y≤n-1)请大家自己思考!总结:在这里我们把握住了数组的存储结构而避开其逻辑结构,这使得我们可以用一种简单的方式方便地访问多维数组的元素。事实上,逻辑结构和存储结构统称为数据结构。《数据结构》作为计算机专业的核心课程我们以后将会学到,在这里只是简单地提出,使得大家能有个提前的认识。通过本节的学习大家也应该看到:针对同一个问题,抓住其不同方面的特性,有时能取得一些意想不到的便利和效果。7.5指向指针的指针指向指针变量的指针,简称为指向指针的指针;指向指针变量的指针变量简称为指向指针的指针变量。7.5.1指向指针的指针指向指针变量的指针,简称为指向指针的指针。在本章开头已经提到了“间接访问”变量的方式。利用指针变量访问另一个变量就是“间接访问”。如果在一个指针变量中存放一个目标变量的地址,这就是“单级间址”,见图7-16(a)。指向指针的指针用的是“二级间址”方法,见图7-16(b)。从理论上说,间址方法可以延伸到更多的级,见图7-16(c)。但实际上在程序中很少有超过二级间址的。级数越多,越难理解,容易产生混乱,出错机会也多。图7-16通过指针变量存取变量的值地址值指针变量变量(a)一级间址地址1地址2指针变量1指针变量2(b)二级间址值变量地址n值变量指针变量n…指针变量指针变量2地址1地址2(c)n级间址7.5.2定义指向指针变量的指针变量指向指针变量的指针变量又简称为指向指针的指针变量。指向指针的指针变量定义的形式为:类型名**指针变量明;此处,指针变量名是“指向指针的指针变量”的变量名,类型名是该指针变量经过二级间址后所存取变量的数据类型。由于运算符“*”的结合性是“从右到左”,因此“**指针变量名”等价于“*(*指针变量名)”,表示该指针变量的值存放的是另一个指针变量的地址,要经过两次间接存取后才能存取到变量的值。例如语句:

char**p;

定义p为指向指针的指针变量,它要经过两次间接存取后才能存取到变量的值,该变量的数据类型为double。7.5.3指向指针的指针变量的应用

1.指向一个指针变量,间接存取变量的值可以把一个指针变量的地址赋给指向指针的指针变量,然后通过二级间址方法存取变量的值。例7-11通过二级间址方法存取变量的值。/*7-11.c*/main(){doubled=123.456,*p,**pp;pp=&p;p=&d;

printf("d=%8.3f,",**pp);**pp+=543.21;

printf("d=%8.3f\n",d);}运行结果为d=123.456,d=666.666上述指针变量pp指向指针变量p,而指针变量p又指向双精度实型变量d,如图7-17所示,图中假设指针变量p的地址是1500,变量d的地址是3500。此时*pp表示指针变量p的值(即变量d的地址),因此表达式**pp与变量d等价。1500指向指针的指针变量pp3500指针变量p123.45变量d1500(地址)3500(地址)图7-17指向指针指针指向指针变量2.指向指针数组,存取指针数组元素所指内容可以把一个指针数组的首地址赋给指向指针的指针变量,例如:例7-12有三个等级分,由键盘输入1,屏幕显示“pass”,输入2显示“good”,输入3显示“excellent”。/*7_12.c*/main(){

intgrade;char*ps[]={"pass","good","excellent"},**pp;pp=ps;

printf("请输入等级分(1~3):");

scanf("%d",&grade);

printf("%s\n",*(pp+grade-1));}运行结果:请输入等级(1~3):2↙good上述程序中pp指向数组

温馨提示

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

评论

0/150

提交评论