1、第第3章章 指针高级应用指针高级应用3.1 指针3.2 指针与数组3.3 指针与字符串3.4 指针与函数3.5 指针与结构体3.6 链表3.7 综合实例习题3实验3指针是C语言的一大特色,指针的灵活运用,可使程序简捷、明快,大大提高程序的运行速度。但由于指针是指向内存地址的,如果使用不当,比如指针指向了内存的系统存储区,可能会因为进行了错误的写操作而导致系统区被破坏而死机,因此使用指针要非常谨慎。本章首先对指针进行详细讲解,其次结合结构体指针,详细讲述了链表的一系列操作。在模块化程序设计中,一个C源文件通常由多个函数组成,这些函数之间互相调用,完成一个复杂的功能。因此,函数之间的参数传递非常重
2、要。本章主要就指针与函数问题进一步与大家探讨。3.1 指指 针针在计算机中,所有的要处理的数据都是存放在内存中的。一般把内存的一个字节称为一个内存单元。不同的数据类型所占用的内存单元数不等,如一个整型变量占2个单元(16位机或某些C编译环境下),字符变量占1个单元等。任何变量都在计算机内存中占有一块内存区域,变量的值就存放在这块内存区域之中(寄存器变量不在内存中,而是在CPU的寄存器中)。比如在图3.1中,整型变量i占2000和2001两个内存单元,因此i的地址是2000。图3.1 变量i占用内存示意图CPU为了正确地访问这些内存单元,必须为每个内存单元编上号。内存单元的地址从0开始编码,最大
3、地址码取决于内存大小。比如256 B大小的内存,内存单元编码范围为028-1。根据一个内存单元的编码即可准确地找到该内存单元,内存单元的编号也叫做地址,通常也把这个地址称为指针。如果想知道变量的地址,可采用&运算,如:int a=6;printf(%d,&a);/*&a 就是变量a的地址*/注意,这个地址并不是始终不变的,这是由机器和操作系统来安排的,我们无法预先知道。3.1.1 指针变量指针变量我们已经知道,变量在计算机内是占有一块存储区域的,变量的值就存放在这块区域之中。在计算机内部,访问这块区域的方法有两种:(1)通过访问或修改相应的变量来访问或修改这块区域的内容。(2)先求出变量的地址
4、,然后再通过地址对它进行访问,这就是这里所要论述的指针及其指针变量。变量的地址其实就是一个数字,但我们无法用一个整数来表示,因为该数字的取值范围与内存的大小有关。因此为了表示该地址,引入了指针变量的概念。指针:一个变量的地址称为指针,可以说指针就是地址。指针变量:一个变量,其值是另外一个变量的地址。1.指针变量的定义指针变量的定义指针变量的定义如下:类型 *指针变量名;/*指针变量的类型由其所指变量的类型决定*/如:int i=10,j=11,k=12,*p=&i;此时内存如图3.2所示。注意,i、j、k三个变量的地址在实际内存中不一定连续,为了节省空间在图中画成连续的。图3.2 指针与变量在
5、内存中的关系图3.2中,指针变量p存储的是整型变量i的地址,我们一般称为“p指向i”。指针变量有三个属性:(1)指针变量的类型。指针变量的类型取决于该指针指向的变量的类型。如p指向的变量i是整型,则p的类型也是整型。(2)指针变量所占内存单元数。指针变量本身也是变量,因此也需要占一定的内存单元,只不过它是一种特殊的变量而已,它只能存储其他变量的地址。指针变量到底占多少内存单元呢?如果指针变量占两个内存单元,则称为“近指针”,用near表示。如果该变量在内存中占4个内存单元,则称为“远指针”,用far表示。如果未指定near或far,则缺省是near。(3)指针变量的值,即该指针变量指向哪一个变
6、量。如p的值是2000。注意区分指针变量本身的值与指针变量所指的值。图3.2中,指针变量p本身的值是一个地址2000,它所指向的值是10。2.指针变量的引用指针变量的引用有两个运算符可以引用指针变量:(1)&:取地址运算符,一般用来给指针变量赋值,如 p=&i;。注意,&只能对左值进行运算。所谓左值,是指具有内存单元的数据。如整型变量k是左值,但5就不是左值,因为5是常量。(2)*:引用目标运算符,用于访问指针变量所指向的内存单元。例如:int i10,j,*p;p=&i;/*指针变量p指向变量i*/j=*p;/*将p所指变量的值赋给j*/3.指针变量的赋值指针变量的赋值指针变量中只能存放地址
7、,可采用&给指针变量赋值。虽然地址也是一个整数,但不能将一个整数直接赋给一个指针变量,如:int*p1=&j;/*正确*/int*p2=1000;/*警告,虽然不是错误,但请不要这样做,危险!*/指针变量赋值后,它指向某个内存单元。之后还可以通过赋值运算,让该指针变量指向其他内存单元。这就是指针变量灵活的地方。如:char*p3,str10;p3=&str0;p3=&str1;【程序程序3.1】分析下列程序的执行结果,仔细体会其区别所在。#include main()intx,y,*p,*q,t,*r;x=100;y=200;p=&x;q=&y;printf(x=%d y=%d*p=%d*q=
8、%dn,x,y,*p,*q);t=x;x=y;y=t;printf(x=%d y=%d*p=%d*q=%dn,x,y,*p,*q);x=100;y=200;p=&x;q=&y;t=*p;*p=*q;*q=t;printf(x=%d y=%d*p=%d*q=%dn,x,y,*p,*q);x=100;y=200;p=&x;q=&y;r=p;p=q;q=r;printf(x=%d y=%d*p=%d*q=%dn,x,y,*p,*q);程序的执行结果为:x=100 y=200*p=100*q=200 x=200 y=100*p=200*q=100 x=200 y=100*p=200*q=100 x=1
9、00 y=200*p=200*q=100三次交换中各变量的存储内容变化情况如图3.3所示。图3.3 变量p、q、x、y存储内容的变化 从图中我们可以看出,第一次交换与第二次交换有相同的结果,都是对变量x、y的交换;而第三次交换则是对指针变量p、q值的交换,x、y的值不变。4.注意事项注意事项(1)一个指针变量只能指向同一类型的变量。如:int a,*p;float f;p=&a;/*正确*/p=&f;/*错误*/(2)定义指针变量后,在还未规定它指向哪一个变量之前,不应该对指针所指单元进行运算。只有在程序中用&赋值语句具体规定后,才能用*运算符访问所指向的变量。如:char*q,c;c=*q;
10、/*错误*/(3)注意*运算符的用法。如:int a;int*p=&a;/*定义指针变量时指定初值,是为p指定初值*/*p=100;/*给指针p所指向的变量赋值,这里是给变量a赋值*/(4)运算符*与&优先级相同,仅次于()运算符,按自右向左的方向结合。(5)注意区别以下概念:指针变量与指针变量所指的变量;指针变量的值和指针变量所指向的变量的值。例如,在图3.2中,指针变量是p,它所指的变量是i;指针变量的值是2000,是一个地址,它所指向的变量的值是10。3.1.2 指针的基本运算指针的基本运算与指针相关的运算除取地址与引用目标运算外,还有相关指针的比较运算、指针与整数的加减运算及相关指针的
11、减法运算。这些运算都是针对指向集合型数据中数据元素的指针应用的。集合型数据是顺序存储在一个连续的存储空间中,并具有相同数据类型的数据元素组成的一个数据集合。集合型数据的典型例子就是数组,对集合型数据中任一数据元素的访问可以通过该集合型数据的内存区域的首地址和被访问元素的序号来完成。我们已经知道通过数组名及数组元素下标可以访问数组中的每一个元素,下面我们很快可以看到如何通过数组的首地址及元素的下标来访问数组元素,而数组元素的下标实际上就是每一个数组元素在数组中的位置序号。相关指针是指向同一集合型数据中数据元素的指针。1.指针的比较运算指针的比较运算相关指针的比较运算是比较指向同一集合型数据中数据
12、元素的两个指针之间的前后次序关系的运算,包括所有的关系运算、=、=、!=。设p、q分别为指向同一数组中某两个元素的指针,若p q为真,则表示p所指向的数组元素在q所指向的数组元素之前;而p=q为真,则表示p、q指向相同的数组元素。两个相关指针用比较运算符连接的表达式同其他关系运算表达式一样,都是关系运算表达式,在C语言中都是整型表达式,关系成立时表达式计算结果为1,否则为0。2.指针与整数的加减运算指针与整数的加减运算指针与整数的加减运算是C程序中最常见的一种指针算术运算。设p为指向一集合型数据中某一数据元素的指针,n为一整数,则pn为一指针表达式,其运算结果为相对于指针p所指向的数据元素位置
13、的第后n个或前n个数据元素的首地址。例如:int a100,*p;p=&a50;p+20、p-20、p+x*12都是正确的指针表达式。这里,p+20的值为数组元素a70的首地址,p-20的值为数组元素a30的首地址,而p+x*12的值则取决于整型表达式x*12的计算结果。所以,C编译程序在处理一个指针型数据加(减)一个整数这种指针表达式时,并不是简单地将指针加(减)这个整数,而是要将指针加上(减去)这整数个目标数据元素所占据的内存单元数目作为表达式的运算结果。例如,指向char类型目标的指针和指向double类型目标的指针,在程序中有同样的加(减)整数n表达式计算,而在编译程序实际处理时,表达
14、式计算结果分别为加(减)整数n和整数n*8,从这里我们也可以看出,不同类型的指针变量虽然占据相同数目的内存单元,但其参与表达式运算时一定具有不同的运算方式,从而具有不同的运算结果,否则我们也就没有必要去区分它们的数据类型了。由于pn为一指针表达式,因此其运算结果可以赋予同p指向相同目标类型的指针变量,而在实际编程中最常用到的是指针的增量运算,如p+、p-、+p、-p等。3.相关指针的减法运算相关指针的减法运算指针类型数据的另外一种运算是相关指针的减法运算。设p、q为相关指针,p-q为一整型表达式,其绝对值表示p与q所指对象之间元素的个数,若p在q之后,则值为正整数,反之为负,若p与q指向同一对
15、象,则值为0。这里同样要特别注意,p-q表示p与q之间数据元素的个数,而不是内存单元的个数,它实际上等于内存单元的个数除以每个元素所占据的内存单元数。下面我们通过例子来看一下指针的这几种运算。【程序程序3.2】用指针的方法来访问数组中的元素。#include main()static inta10=1,2,3,4,5,6,7,8,9,10;int i,*p,*q;p=&a0;q=&a10;for(i=0;i q-p;i+)printf(%d,a i);for(i=0;p+i q;i+)printf(%d,*(p+i);for(;p q;p+)printf(%d,*p);思考:存在a10吗?a1
16、0的内容是什么?q=&a10写法对吗?【程序程序3.3】用指针的方法实现将数组中的元素颠倒存放。#include main()static inta10=1,2,3,4,5,6,7,8,9,10;int i,t,*p,*q;for(i=0;i 10;i+)printf(%d,a i );p=&a0;q=&a9;while(p q)t=*p;*p=*q;*q=t;p+;q-;printf(n);for(i=0;i 10;i+)printf(%d,a i );3.2 指指针针与与数数组组同其他基本数据类型一样,指针类型的数据有变量也有常量。在C语言中指针类型的常量基本上有三种形式:数组名、字符串常
17、量、强制成指针类型的无符号整数。指针可以指向数组或某个数组元素,当一个指针指向数组后,对数组元素的访问,既可以使用数组下标,也可以使用指针。3.2.1 指针与一维数组指针与一维数组在C语言中指针与数组有着密切的关系,数组名实际上就是指针类型的数据,是指针常量。例如下面的定义中:int a100,*p;a和p都是指针类型的数据,但是a是常量,p是变量,也就是说我们可以为变量p赋值,但决不能为常量a赋值,表达式a+、a=是错误的,正如10+、5=是错误的一样。a和p的另外一个区别在于,当它们被定义之后,a指向一个能够存放100个int类型的数据的内存区域,而p在没有赋值之前则没有指向任何有意义的空
18、间。除此以外,a与p完全相同。数组既然同指针是完全相同的数据类型,它们之间的运算就应该是通用的。实际上数组中通过下标访问元素的运算完全等价于指针的引用目标运算,即“”运算实际上是通过指针数据访问目标的另一种运算。我们前面讲过指针表达式与整数的加减运算,实际上有以下的等价关系成立:(指针表达式)n =*(指针表达式+n)所以,我们完全可以像访问数组元素那样去访问指针所指向的目标,同样也可以像引用指针目标那样去访问数组元素。另外有一点需要说明的是,当函数中有指针类型的形式参数时,在定义时既可以定义成指针形式的参数类型,也可以定义成数组形式的参数类型,两者完全相同,都是指针类型的形式参数。访问数组有
19、5种方法,总结如下:(1)数组名下标。例如:main()int a10;int i;for(i=0;i10;i+)scanf(%d,&ai);printf(n);for(i=0;i10;i+)printf(%d,ai);(2)数组名+下标。例如:main()int a10;int i;for(i=0;i10;i+)scanf(%d,a+i);printf(n);for(i=0;i10;i+)printf(%d,*(a+i);(3)指针法。例如:main()int a10;int*p=a;for(p=a;p(a+10);p+)scanf(%d,p);printf(n);for(p=a;p(a+1
20、0);p+)printf(%d,*p);(4)指针+下标。例如:main()int a10;int*p=a,i;for(i=0;i10;i+)scanf(%d,p+i);printf(n);for(i=0;i10;i+)printf(%d,*(p+i);(5)指针下标。例如:main()int a10;int*p=a,i;for(i=0;i10;i+)scanf(%d,&pi);printf(n);for(i=0;i10;i+)printf(%d,pi);使用指针指向一维数组,应注意以下事项:(1)若指针p指向数组a,虽然p+i与a+i、*(p+i)与*(a+i)意义相同,但仍应注意p与a的区
21、别:a代表数组的首地址,是不变的;p是一个指针变量,可以指向数组中的任何元素,如:for(p=a;a(p+10);a+)/*a代表数组的首地址,是不变的,a+不合法*/printf(%d,*a)(2)指针变量可以指向数组中的任何元素,注意指针变量的当前值。(3)使用指针时,应特别注意避免指针访问越界。因编译器不能发现该问题,所以避免指针访问越界是程序员自己的责任。【程序程序3.4】有n个人围成一圈,顺序排号。从第一个人开始报数(从1到3报数),凡报到3退出圈子,问最后留下的是原来第几号的那位,请用指针完成。#include stdio.h#define N 10main()int i,pers
22、onN,count,num;int*p=person;for(i=0;pperson+N;i+,p+)*p=i+1;/*为每个人编号,从1开始*/num=0;/*num用来记录已经出圈的人数*/for(p=person;num=person+N)p=person;/*循环,围成一个圈*/count=1;while(count=person+N)p=person;if(*p!=0)count+;printf(person%d come out.n,*p);num+;*p=0;/*出圈的人编号变为0*/*寻找最后一个留下的人*/for(p=person;pperson+N;p+)if(*p!=0)
23、printf(The last person is person%d,p-person+1);通过程序3.4,体会指针与一维数组的关系,以及使用指针时如何判断到达数组末尾。3.2.2 指针与二维数组指针与二维数组C语言按照行序为主序存储二维数组,也就是说先存储第0行数据,再存储第1行数据,依次类推,如图3.4所示。因此,利用指针变量的指向可以改变的优势,能够定义一个指针变量,让其依次指向二维数组的各个元素,对其进行操作。图3.4 二维数组存储方式【程序程序3.5】利用指针变量对二维数组进行操作。#include stdio.h#define Row 2#define Col 3void mai
24、n()int aRowCol,i,j,*p;for(i=0;iRow;i+)for(j=0;jCol;j+)aij=i*Col+j+1;p=&a00;for(;p=&aRow-1Col-1;p+)/*考虑能否写成 p&aRowCol*/printf(%d,*p);执行结果:1 2 3 4 5 6程序3.5中,p=&a00;是否可写成p=a;呢?下面我们进一步分析指针和二维数组的关系。为了说明问题,我们定义以下二维数组:int a34=0,1,2,3,4,5,6,7,8,9,10,11;a为二维数组名,此数组有3行4列,共12个元素。也可这样来理解,数组a由三个元素组成:a0,a1,a2。而它的
25、每个元素又是一个一维数组,且都含有4个元素(相当于4列)。例如,a0所代表的一维数组所包含的4个元素为a00,a01,a02,a03。如图3.5所示。图3.5 二维数组的指针注意:a0、a1、a2实际上不存在,为了便于理解,图3.5中将其画出。数组名a:表示整个二维数组的首地址,也是元素a00的地址,同时代表第一行元素的首地址。a+1:表示第二行元素的首地址,也是元素a10的地址。a+2:表示第三行元素的首地址,也是元素a20的地址。如果此二维数组的首地址为1000,由于第0行有4个整型元素,因此a+1为1008,a+2也就为1016,如图3.6所示。图3.6 二维数组指针的地址由于把a0、a
26、1、a2看成一维数组,它们代表各自数组的首地址,即:a0&a00(表示“等价”)a1&a10 a2&a20 另外,根据一维数组的表示方法,有:a0+1&a01 /*表示一维数组中第二个元素的地址*/a0+2&a02 a1+1&a11;也就是说ai+j&aij。在二维数组中,我们还可用指针的形式来表示各元素的地址。a0与*(a+0)等价,a1与*(a+1)等价,因此ai+j就与*(a+i)+j等价,它表示数组元素aij的地址。综上所述,在二维数组中,有:aij*(ai+j)*(*(a+i)+j)&aij ai+j*(a+i)+j【程序程序3.6】用指针变量输出二维数组中的值。main()stat
27、ic int a34=1,3,5,7,9,11,13,15,17,19,21,23;int*p;for(p=a0;pa0+12;p+)if(p-a0)%4=0)printf(n);printf(%4d,*p);C语言中,二维数组在内存是按行序顺序存储的。程序3.6用指针顺序访问二维数组的元素。若需访问二维数组anm(n行m列)的某个元素aij,计算该元素的相对位置公式为:i*m+j(i,j=0,1,2,)这种方法相当于把二维数组转化为一维数组来使用。此时有:&aij&a00+4*i+j a0+4*i+j /不可以写成a+4*i+j另外,要补充说明一下,如果编写一个程序输出打印a和*a,可以发现
28、它们的值是相同的,这是为什么呢?我们可以这样来理解:首先,为了说明问题,我们把二维数组人为地看成是由三个数组元素a0,a1,a2组成的,将a0,a1,a2看成是数组名,它们又分别是由4个元素组成的一维数组。因此,a表示数组第0行的地址,而*a即为a0,它是数组名,当然还是地址,也就是数组第0行第0列元素的地址。虽然用指针可以访问二维数组,但程序会变得比较难以理解。比如:访问第i行,第j列的元素,如果用指针,可能写成:p=ai+j /*或p=*(a+i)+j*/然后用p对其进行操作,这样做没有直接用下标aij直观。因此,在不要求编译速度的情况下,一般建议直接用下标对数组元素进行操作,这样做不但简
29、单、可读性好,而且不易出错。3.2.3 指向一个由指向一个由n个元素所组成的数组指针个元素所组成的数组指针在TC中,可定义如下的指针变量:int(*p)3;即指针p为指向一个由3个元素所组成的整型数组指针。在该定义中,圆括号是不能少的,否则它是指针数组,这将在后面介绍。这种数组的指针不同于前面介绍的整型指针。当整型指针指向一个整型数组的元素时,进行指针(地址)加1运算,表示指向数组的下一个元素,此时地址值增加了2(因为放大因子为2)。而如上所定义的指向一个由3个元素组成的数组指针,进行地址加1运算时,其地址值增加了6(放大因子为23=6)。这种数组指针在Turbo C中用得较少,但在处理二维数
30、组时还是很方便的。例如:int a34,(*p)4;p=a;开始时p指向二维数组第0行,当进行p+1运算时,根据地址运算规则,此时放大因子为42=8,所以此时正好指向二维数组的第1行。和二维数组元素地址计算的规则一样,*p+1指向a01,*(p+i)+j则指向数组元素aij。【程序程序3.7】数组指针示例。int a3 4=1,3,5,7,9,11,13,15,17,19,21,23;main()int i,(*b)4;b=a+1;/*b指向二维数组的第1行,此时*b0或*b是a10*/for(i=1;i=4;b=b0+2,i+)/*修改b的指向,每次增加2*/printf(%dt,*b0);
31、printf(n);for(i=0;i2;i+)b=a+i;/*修改b的指向,每次跳过二维数组的一行*/printf(%dt,*(bi+1);printf(n);程序运行结果如下:9 13 17 21 3 11 193.2.4 指针数组指针数组数组元素可以是基本类型,也可以是指针。用来存储指针型数据的数组就称为指针数组。指针数组中每个元素都是指向同一数据类型的指针。指针数组的定义形式如下:类型*数组名整常量表达式;例如:char*str10;int*a20;/*切记:stri,ai中存放的均是地址。*/同前面所引入的概念一致,这里的数组名仍然是一指针常量,它所指向的目标是指针型数据,而这个指针
32、型数据的目标又是指向其他基本类型数据的指针,所以指针数组名是指向指针类型数据的指针,我们称它为指针的指针。指针数组用途比较多,在此我们以处理多个字符串为例,说明其方便之处。由于指针数组的每一个元素实际上都是指向另一个数据的指针,因此,可以将不同长度的字符串首地址分别放入指针数组的每一个元素中,然后再对这些字符串一一处理。例如,在一个简单的图书管理系统中,为了存储5本书,假设书名最长为128个字符。如果没有指针数组,需要采用二维数组来存放书名:char name5128;当书名小于128个字符时,会造成空间浪费,如图3.6(a)所示。而有了指针数组后,可以这样定义:char*book_name5
33、;书名有多长,就申请多大的空间来存储,以便充分利用空间,如图3.7(b)所示。图3.7 字符二维数组与指针数组【程序程序3.8】指针数组。#include string.hmain()char*book_name5,temp128;for(i=0;i5;i+)scanf(%s,temp);/*将书名先存放到一个临时数组中*/len=strlen(temp);/*计算书名的大小*/*申请len+1长度大小的空间,为0预留空间*/book_namei=(char*)malloc(sizeof(char)*(len+1);strcpy(book_namei,temp);malloc函数的详细用法请参
34、见3.6节。3.2.5 指针的指针指针的指针指针数组名是指针常量,那么我们如何定义指针的指针类型的变量呢?可以按照下面的形式来定义:类型区分符*变量名;根据我们前面所学的知识,当我们需要通过函数参数返回指针型数据时,我们要传入该指针型数据的地址,实际上就是指针的指针,而在函数中我们要将返回的结果存入该指针所指向的目标,这是对指针的指针类型数据的一个典型应用。另外,指针的指针类型数据也是处理字符串集合的一个基本存储结构,处理时我们需要将每一个字符串的首地址存储在指针数组中的每个元素中,然后通过这个指针数组就可以访问到所有的字符串。下面我们通过一个例子来看一下这方面的应用。【程序程序3.9】输入1
35、0个字符串,按字典序对字符串排序并输出。#include void sort(char *Str,int n)inti,j;char*t;for(i=0;i n-1;i+)for(j=0;j 0)t=Str j;Str j =Str j+1;Str j+1 =t;main()char*str10;inti;for(i=0;i10;i+)scanf(%s,str i);sort(str,10);for(i=0;i10;i+)printf(%sn,str i);本程序中用到了冒泡排序算法,另外还用到了C标准库函数中的字符串比较函数strcmp,该函数根据参数中给定的两个字符串,按照字典序,通过返回
36、整型值-1、0、+1来表示这两个字符串之间的小于、等于、大于关系。请思考:stri中存放的是什么?输入的字符串存储在什么地方?这样做合理吗?如果不合理,应该如何改进?3.3 指针与字符串指针与字符串C语言本身没有字符串数据类型,要实现字符串,有两种方式:字符数组与字符指针。3.3.1 字符数组与字符串的区别字符数组与字符串的区别看下面的例子:char str5=c,h,i,n,a;char name=china;其中,str与name都是字符数组,str的大小是5,但name的大小是6,它们的内存形式如下:此时,name可以称做字符串,因为它是以0结尾的。只要以0结尾的我们就可以称其为字符串。
37、3.3.2 实现字符串实现字符串1.采用字符数组实现字符串采用字符数组实现字符串字符串变量可以用字符数组来实现,在声明的时候进行初始化。比如:char str16=china;/*等价于char str1 =c,h,i,n,a;*/编译器把字符串china中的字符复制到字符数组str1中,同时在最后放置一个0,以使str1能作为字符串使用。其内存形式如下:china0那么,当字符串没有放满字符数组时,空余的单元放什么呢?比如:char str210=china;此时,编译器会自动添加0,即str2的内存形式如下:chinA00000如果字符串长度大于字符数组长度,又如何存储呢?比如:char
38、str35=I love C!;此时str3的内存形式如下:Ilov可以看出,str3已经不能称其为字符串了,只能是字符数组,而且有部分字符丢失。因此,在用字符数组实现字符串时,一定要注意字符数组的大小,还要记得为0留个空间。一种可取的方式是不指定字符数组的大小,而是让编译器自己计算。比如:char str4=I love C!;此时编译器自动计算字符串长度,为str4分配10个空间。其内存形式为:Ilovec!0注意事项:(1)不要误认为是字符数组,其长度就可以改变,因为一旦程序编译后,字符数组的长度就不能变动了。(2)由于数组可以用下标访问,也可以用指针访问,因此,对于str4数组,str
39、4表示一个元素,其值是字符v,也可以用*(str4+4)来访问,str4+4是指向字符v的指针。(3)数组名是常量,因此,初始化后不能再对其进行赋值。比如:char str520=I love C!;str5=I love China!;/*错误,给一个常量赋值显然是错误的。*/可采用字符串拷贝方式给一个字符数组重新赋值。如:strcpy(str5,I love China!);此时一定要注意字符数组的大小要能容纳新的字符串。【程序程序3.10】分析下面程序的输出结果。main()char str5=c,h,i,n,a;printf(%sn,str);2.用字符指针实现字符串。【程序程序3.1
40、1】用字符指针实现字符串(如图3.8所示)。main()char*pstr=I love China!;printf(%sn,pstr);图3.8 用字符指针实现的字符串pstr是一个指针变量,I love China!是一个字符串常量。语句:char*pstr=I love China!;等价于char*pstr;pstr=I love China!;即把字符串常量的首地址赋给指针pstr,而不能理解为把字符串常量赋给指针变量,即*pstr=I love China!;/*错误*/注意事项:(1)pstr是指针变量,存放一个内存地址,因此可以给它赋一个地址。如:pstr=abcde;/*将字
41、符串常量的首地址赋给s*/(2)指针变量在没有明确的指向之前,不要对其进行操作,以免造成严重后果。如:char*pstr1;scanf(%s,pstr1);/*不出错,会有警告,尽量避免*/pstr10=a;/*警告*/字符数组与字符指针变量的比较如表3.1所示。表表3.1 字符数组与字符指针变量的比较字符数组与字符指针变量的比较3.3.3 字符串的输入字符串的输入/输出输出1.字符串的输出字符串的输出使用printf()和puts()可以轻松地完成字符串的输出。如:char str=I love China!;/*或char*str=I love China!*/printf(%s,str)
42、;/*str为字符串首地址,遇到0停止*/puts较printf简单一些,只需要一个参数,即字符串首地址。另一个差别是使用puts将字符串输出后会自动添加一个换行符,而printf是要显式添加的。2.字符串的输入字符串的输入与字符串输入相关的有scanf()和gets(),两者差别较大。char str10;scanf(%s,str);用scanf()输入字符串时,不需要在str前添加&符号,因为str是数组名,编译器会自动将其按地址对待。需要强调的是,scanf()会跳过空格字符,然后读入字符,直到遇到一个空格字符或换行符,并自动在字符串末尾添加0。也就是说,通过scanf()读入的字符串是
43、不会含有空格字符和换行符的。为了能读入空格字符,需要使用gets函数。gets函数不会跳过一开始的空格字符,并且直到遇到一个换行符时才停止输入。下面的例子可以说明两者的区别。char str100;printf(enter a string:n);scanf(%s,str);puts(str);执行时,如果在提示语句后面输入:Im a chinese!那么输出结果是:Im如果将scanf换为:gets(str);同样的输入,得到的执行结果是:Im a chinese!需要指出的是:二者都不对长度进行检查,当输入的字符串长度大于数组长度时,会导致程序结果执行的异常,因此,需要程序员在编写程序时考
44、虑长度的限制。3.逐个字符的字符串输入逐个字符的字符串输入可以看出,scanf()和gets()都不够灵活,因此,我们往往需要编写自己的字符串输入函数。一般通过逐个读入字符来读入字符串。在编写自己的字符串输入函数之前,请仔细考虑以下几个问题:(1)开始的空格字符是否需要跳过?(2)什么时候中止字符串读写?(3)如果字符串太长无法存储,应该怎么办?下面给出一个输入字符构成字符串的函数。该函数跳过开始的空格,但读入中间的空格;遇到字符#结束;当字符串太长时,忽略多余的字符,并返回成功读入字符的个数,也就是字符串的长度。【程序程序3.12】读入字符构成字符串。int readStr(char str
45、,int n)char ch;int i=0;ch=getchar();while(ch=)ch=getchar();/*忽略开始的空格字符*/while(ch!=#&in-1)stri+=ch;ch=getchar();stri=0;/*最后一定要自己加上0表示字符串尾*/return i;main()char ss20;readStr(ss,20);puts(ss);有关更多字符串操作的函数请查看附录C。3.4 指指针针与与函函数数3.4.1 指针作为函数参数指针作为函数参数指针在函数中最常见的用途是作为函数的形式参数,并在函数被调用后通过参数返值。根据前面我们所学的知识,C语言中调用函数
46、时,实参替换形参的过程是一个单向的传值过程,在编译技术中称为值传递方式,也就是将形参的值传递给实参。值传递方式的优点是实现简单,且在实参中可以使用表达式,而最大的缺点是被调用函数不能通过参数向调用函数返值。与值传递方式相对应的是另一种称为地址传递的参数替代方式,它将形参的地址传递给实参,它的优缺点正好同值传递方式相反。大部分高级语言采用后一种方式,也有的语言如Pascal语言采用两种方式共存的方法,但在C语言中由于设有专门的地址类型的数据,因此为了简化语言的实现,只采用第一种方式,若需要通过参数带回值,则可以为函数显式地定义指针类型的参数,并在调用该函数时将希望存储返回值的目标单元的地址通过指
47、针参数传入函数,而被调用函数则可以通过传入的指针参数间接地引用它所指向的调用函数中的目标单元,从而实现向调用函数指定的目标变量返值的过程。1.基本数据类型指针作为函数参数基本数据类型指针作为函数参数下面的例子较好地说明了这一方法的原理。设有一函数定义如下:swap(int x,int y)intt;t=x;x=y;y=t;当我们用实参a、b两个变量调用该函数swap(a,b)后会发现,a、b的值同调用前相比没有任何变化,swap函数没有实现交换的功能,这正是值传递方式所造成的结果。实际上swap函数确实执行交换了,只不过它交换的不是调用函数的a、b变量,而是它自己的x、y变量,x、y变量是sw
48、ap函数的形式参数,也是它的局部动态量,当swap执行结束后它们就不复存在了,所以swap函数没有为调用函数留下任何痕迹。为了实现对调用该函数a、b变量的交换,我们可以将swap函数的定义改进如下:swap(int *x,int *y)intt;t=*x;*x=*y;*y=t;在调用该函数时需传入a、b变量的地址而不是a、b变量本身,亦即swap(&a,&b),执行后会发现a、b的值被交换了。此时执行的swap函数调用,其参数的传递仍然是值传递方式,只不过此时传入的值不再是a、b变量自己的值,而是它们的地址,在swap函数中通过对该地址所指向目标的引用,可以改变a、b变量的值,从而实现被调用函
49、数将值返回到调用函数所指定的变量中去。在通过函数参数返值时,一定要遵循调用者传入实参变量的地址,被调用者引用该地址所指向的目标这一规律。例如对上例中swap函数的定义改变如下:swap(int *x,int *y)int*t;t=x;x=y;y=t;其他方面不作任何改变,调用函数时仍然传入a、b变量的地址,亦即swap(&a,&b),执行后会发现swap没有交换a、b变量的值。仔细分析后会发现,此时虽然传入的仍是调用函数的目标变量a、b的地址,但在被调用函数swap中并没有对该地址所指向的目标进行引用,而是直接对其局部动态量x、y变量交换。同第一个例子相同,当swap函数执行结束后,x、y变量
50、就不复存在了,所以swap函数没有为调用函数留下任何痕迹。通俗地说,参数传递时,如果传指针,只能改变指针所指单元的内容,不能改变指针的指向,即指针变量本身的值。也就是说,调用子函数前,指针指向谁,调用后,指针仍旧指向谁。【程序程序3.13】用函数参数返回指定数组中最大、最小元素的值及其位置。main()static inta10=1,2,3,4,5,6,7,8,9,10;intmax,min,max_pos,min_pos;cal_mm(a,10,&max,&max_pos,&min,&min_pos);cal_mm(int x,int n,int*a,int*apos,int*b,int*b