第 10章 指 针
10.1 地址和指针的概念
10.2 变量的指针和指向变量的指针变量
10.3 数组的指针和指向数组的指针变量
10.4 字符串的指针和指向字符串的指针变量
10.5 函数的指针和指向函数的指针变量
10.6 返回指针值的函数
10.7 指针数组和指向指针的指针
10.8 有关指针的数据类型和指针运算的小结习题指针是 C语言中的一个重要的概念,也是 C语言的一个重要特色。正确而灵活地运用它,可以有效地表示复杂的数据结构 ;能动态分配内存 ;能方便地使用字符串 ;有效而方便地使用数组 ;在调用函数时能得到多于 1个的值 ;能直接处理内存地址等,这对设计系统软件是很必要的。掌握指针的应用,可以使程序简洁、紧凑、高效。每一个学习和使用 C语言的人,都应当深入地学习和掌握指针。可以说,不掌握指针就是没有掌握 C
的精华。
指针的概念比较复杂,使用也比较灵活,因此初学时常会出错,务请在学习本章内容时十分小心,
多思考、多比较、多上机,在实践中掌握它。我们在叙述时也力图用通俗易懂的方法使读者易于理解。
10.1 地址和指针的概念为了说清楚什么是指针,
必须弄清楚数据在内存中是如何存储的,
又是如何读取的。
如果在程序中定义了一个变量,在编译时就给这个变量分配内存单元。系统根据程序中定义的变量类型,
分配一定长度的空间。
例如,一般微机使用的 C系统为整图 10.1
型变量分配 2个字节,对实型变量分配 4个字节,
对字符型变量分配 1个字节。内存区的每一个字节有一个编号,这就是“地址”,它相当于旅馆中的房间号。在地址所标志的内存单元中存放数据,
这相当于旅馆中各个房间中居住旅客一样。请务必弄清楚一个内存单元的地址与内存单元的内容这两个概念的区别,如图 10.1所示。假设程序已定义了 3个整型变量 i,j,k,编译时系统分配 2000
和 2001两个字节给变量 i,2002,2003字节给 j,
2004,2005给 k。 在程序中一般是通过变量名来对内存单元进行存取操作的。其实程序经过编译以后己经将变量名转换为变量的地址,对变量值的存取都是通过地址进行的。例如,printf(“%D”,i)的执行是这样的:根据变量名与地址的对应关系 (这个对应关系是在编译时确定的 ),找到变量 i的地址
2000,然后从由 2000开始的两个字节中取出数据
(即变量的值 3),把它输出。输入时如果用
scanf("%D",&i),在执行时,就把从键盘输入的值送到地址为 2000开始的整型存储单元中。如果有语句,k=i+j”,则从 2000,2001字节取出 i的值 (3),再从 2002,2003字节取出 j的值 (6),将它们相加后再将其和 (9)送到 k所占用的 2004,2005字节单元中。这种按变量地址存取变量值的方式称为
“直接访问”方式。
还可以采用另一种称之为“间接访问”的方式,将变量 i的地址存放在另一个变量中。按 C语言的规定,可以在程序中定义整型变量、实型变量、字符变量等,也可以定义这样一种特殊的变量,它是存放地址的。假设我们定义了一个变量 i-pointer,
用来存放整型变量的地址,它被分配为 3010、
3011字节。可以通过下面语句将 i的刂?2000)存放到 i-pointer中。
i-pointer=&i;
这时,i-pointer的值就是 2000,即变量 i所占用单元的起始地址。要存取变量 i的值,也可以采用间接方式:先找到存放,i的地址”的变量,从中取出 i
的地址 (2000),然后到 2000,2001字节取出 i的值
(3)。
打个比方,为了开一个 A抽屉,有两种办法,一种是将 A钥匙带在身上,需要时直接找出该钥匙打开抽屉,取出所需的东西。另一种办法是:为安全起见,将该 A钥匙放到另一抽屉 B中锁起来。如果需要打开 A抽屉,就需要先找出 B钥匙,打开 B抽屉,
取出 A钥匙,再打开 A抽屉,取出 A抽屉中之物,
这就是“间接访问”。图 10.2是直接访问和间接访问的示意图。
图 10.2
为了表示将数值 3送到变量中,可以有两种表达方法:
(1) 将 3送到变量 i所标志的单元中。见图 10.2上。
(2) 将 3送到变量 i-pointer所“指向”的单元 (即 i所标志的单元 )中。见图 10.2下。
所谓“指向”就是通过地址来体现的。 i-pointer中的值为 2000,它是变量 i的地址,这样就在 i-pointer和变量 i之间建立起一种联系,即通过 i-pointer能知道 i
的地址,从而找到变量 i的内存单元。图 10.2中以箭头表示这种“指向”关系。
由于通过地址能找到所需的变量单元,我们可以说,
地址“指向”该变量单元 (如同说,房间号“指向
“某一房间一样 )。因此在 C语言中,将地址形象化地称为
“指针”。意思是通过它能找到以它为地址的内存单元 (例如根据地址 2000就能找到变量 i的存储单元,从而读取其中的值 )。一个变量的地址称为该变量的“指针”。例如,地址 2000是变量 i的指针。
如果有一个变量专门用来存放另一变量的地址 (即指针 ),则它称为“指针变量”。上述的 i-pointer
就是一个指针变量。指针变量的值 (即指针变量中存放的值 )是指针 (地址 )。请区分“指针”和“指针变量”这两个概念。例如,可以说变量 i的指针是 2000,而不能说 i的指针变量是 2000。
10.2 变量的指针和指向变量的指针变量如前所述,变量的指针就是变量的地址。存放变量地址的变量是指针变量,用来指向另一个变量。
为了表示指针变量和它所指向的变量之间的联系,
在程序中用,*”符号表示“指向”,例如,i-
pointer代表指针变量,而 *i-pointer是 i-pointer所指向的变量,见图 10.3。
图 10.3
可以看到,*i-pointer也代表一个变量,它和变量 i是同一回事。下面两个语句作用相同:
① i=3;
② *i-pointer=3;
第②个语句的含意是将 3赋给指针变量 i-pointer所指向的变量。
10.2.1 定义一个指针变量
C语言规定所有变量在使用前必须定义,指定其类型,
并按此分配内存单元。指针变量不同于整型变量和其他类型的变量,它是用来专门存放地址的。必须将它定义为“指针类型”。先看一个具体例子:
int i,j;
int *pointer_1,*pointer_2;
第 1行定义了两个整型变量 i和 j,第 2行定义了两个指针变量,pointer_1和 pointer_2,它们是指向整型变量的指针变量。左端的 int是在定义指针变量时必须指定的“基类型”。 指针变量的基类型用来指定该指针变量可以指向的变量的类型。例如,
上面定义的指针变量 pointer_1和 pointer_2可以用来指向整型变量 i和 j,但不能指向实型变量 a和 B。
定义指针变量的一般形式为基类型 *指针变量名下面都是合法的定义:
float*pointer_3;(pointer-3是指向实型变量的指针变量 )
char*pointer_4; (pointer-4是指向字符型变量的指针变量 )
那么,怎样使一个指针变量指向另一个变量呢?下面用赋值语句使一个指针变量指向一个整型变量:
pointer_1=&i;
pointer_2=&j;
将变量 i的地址存放到指针变量 pointer_1中,因此
pointer_1就“指向”了变量 i。
图 10.4
同样,将变量 j的地址存放到指针变量 pointer_2中,
因此 pointer_2就“指向”了变量 j。见图 10.4。
在定义指针变量时要注意两点:
(1) 指针变量前面的,*”,表示该变量的类型为指针型变量。注意:指针变量名是 pointer_1、
pointer_2,而不是 *pointer_1,*pointer_2。这是与以前所介绍的定义变量的形式不同的。
(2) 在定义指针变量时必须指定基类型。有的读者认为既然指针变量是存放地址的,那么只需要指定其为“指针型变量”即可,为什么还要指定基类型呢?我们知道整型数据和实型数据在内存中所占的字节数是不相同的 (前者为 2字节,后者为 4字节 ),在本章的稍后将要介绍指针的移动和指针的运算 (加、减 ),例如“使指针移动 1个位置” 或“使指针值加 1”,这个,1” 代表什么呢?如果指针是指向一个整型变量的,那么“使指针移动 1个位置”
意味着移动 2个字节,“使指针加 1” 意味着使地址值加 2个字节。如果指针是指向一个实型变量的,
则增加的不是 2而是 4。因此必须规定指针变量所指向的变量的类型,即基类型。一个指针变量只能指向同一个类型的变量。不能忽而指向一个整型变量,忽而指向一个实型变量。上面的定义中,
表示 pointer_1和 pointer_2只能指向整型变量。
对上述指针变量的定义也可以这样理解:
*pointer_1和 *pointer_2是整型变量,如同:,int
a,B;”定义了 a和 B是整型变量一样。而
*pointer_1和 *pointer_2是 pointer_1和 pointer_2所指向的变量,pointer_1和 pointer_2是指针变量。
需要特别注意的是,只有整型变量的地址才能放到指向整型变量的指针变量中。
10.2.2 指针变量的引用请牢记,指针变量中只能存放地址 (指针 ),不要将一个整型量 (或任何其他非地址类型的数据 )赋给一个指针变量。下面的赋值是不合法的:
pointer-1=100; (pointer-1为指针变量,100为整数 )
有两个有关的运算符:
(1) &,取地址运算符。
(2) *,指针运算符 (或称“间接访问”运算符 )。
例如,&a为变量 a的地址,*p为指针变量 p所指向的存储单元。
例 10.1通过指针变量访问整型变量。
main()
{ int a,B;
int pointer-1,*pointer-2;
a=100;B=10;
pointer-1=&a;/*把变量 a的地址赋给 pointer-1*/
pointer-2=&B; /*把变量 B的地址赋给 pointer-2*/
printf("%D,%D\n",a,B);
printf("%D,%D\n",*pointer-1,*pointer-2);

运行结果为:
100,10
100,10
对程序的说明:
(1) 在开头处虽然定义了两个指针变量 pointer-1和
pointer-2,但它们并未指向任何一个整型变量。
只是提供两个指针变量,规定它们可以指向整型变量。至于指向哪一个整型变量,要在程序语句中指定。程序第 5,6行的作用就是使 pointer-1指向 a,pointer-2指向 B,见图 10.5。此时 pointer-1
的值为 &a(即 a的地址 ),pointer-2的值为 &B。
图 10.5
(2) 最后一行的 pointer-1和 *pointer-2就是变量 a和 B。
最后两个 printf函数作用是相同的。
(3) pointer-1和 *pointer-2,请区分它们的不同含义。程序第 3行的 *pointer-1和
*pointer-2表示定义两个指针变量 pointer-1、
pointer-2。它们前面的,*”只是表示该变量是指针变量。程序最后一行 printf函数中的 *pointer-1
和 *pointer-2则代表变量,即 pointer-1和 pointer-2
所指向的变量。
(4) 第 5,6行,pointer-1=&a;”和,pointer-2=&B;”
是将 a和 B的地址分别赋给 pointer-1和 pointer-2。
注意不应写成:,*pointer-1=&a;”和,*pointer-
2=&B;”。因为 a的地址是赋给指针变量 pointer-1,
而不是赋给 *pointer-1(即变量 a)。请对照图 10.5分析。
下面对,&”和,*”运算符再做些说明:
(1) 如果已执行了,pointer-1=&a;”语句,若有
&*pointer-1
它的含义是什么?,&”和,*”两个运算符的优先级别相同,但按自右而左方向结合,因此先进行
*pointer-1的运算,它就是变量 a,再执行 &运算。
因此,&*pointer-1与 &a相同,即变量 a的地址。
如果有 pointer-2=&*pointer-1;
它的作用是将 &a(a的地址 )赋给 pointer-2,如果
pointer-2原来指向 B,经过重新赋值后它已不再指向 B了,而也指向了 a。见图 10.6。图 10.6(a)是原来的情况,图 10.6(B)是执行上述赋值语句后的情况。
图 10.6
(2) *&a的含义是什么?先进行 &a运算,得 a的地址,
再进行 *运算。即 &a所指向的变量,*&a和
*pointer-1的作用是一样的 (假设已执行了
,pointer-1=&a”),它们等价于变量 a。即 *&a与 a
等价,见图 10.7。
图 10.7
(3) (*pointer-1)++相当于 a++。注意括号是必要的,
如果没有括号,就成为了 *pointer-1++,和 *为同一优先级别,而结合方向为自右而左,因此它相当于 *(pointer-1++)。由于 ++在 pointer-1的右侧,
是“后加”,因此先对 pointer-1的原值进行 *运算,
得到 a的值,然后使 pointer-1的值改变,这样
pointer-1不再指向 a了。
下面举一个指针变量应用的例子。
例 10.2输入 a和 B两个整数,按先大后小的顺序输出 a
和 B。
main()

int*p1,*p2,*p,a,B;
scanf("%D,%D",&a,&B);
p1=&a;p2=&B;
if(a< B)
{ p=p1;p1=p2;p2=p;}
printf("\na=%D,B=%D\n\n",a,B);
printf("max=%D,min=%D\n",*p1,
*p2);

运行情况如下:
5,9
a=5,B=9
max=9,min=5
当输入 a=5,B=9时,由于 a< B,将 p1和 p2交换。交换前的情况见图 10.8(a),交换后见图 10.8(B)。
图 10.8
请注意,a和 B并未交换,它们仍保持原值,但 p1和
p2的值改变了。 p1的值原为 &a,后来变成 &B,
p2原值为 &B,后来变成 &a。这样在输出 *p1和
*p2时,实际上是输出变量 B和 a的值,所以先输出
9,然后输出 5。
这个问题的算法是不交换整型变量的值,而是交换两个指针变量的值 (即 a和 B的地址 )。
10.2.3 指针变量作为函数参数函数的参数不仅可以是整型、实型、字符型等数据,
还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。下面通过一个例子来说明。
例 10.3题目同例 10.2,即对输入的两个整数按大小顺序输出。
今用函数处理,而且用指针类型的数据作函数参数。
程序如下:
Swap(int *p1,int *p2)
{ int temp;
temp=*p1;
*p1=*p2;
*p2=temp;

main()

int a,B;
int*pointer-1,*pointer-2;
scanf("%D,%D",&a,&B);
pointer-1=&a;pointer-2=&B;
if(a< B) Swap(pointer-1,pointer-2);
printf("\n%D,%D\n",a,B);

运行情况如下:
5,9
9,5
对程序的说明:
Swap是用户定义的函数,它的作用是交换两个变量
(a和 B)的值。 Swap函数的两个形参 p1,p2是指针变量。程序运行时,先执行 main函数,
输入 a和 B的值 (今输入 5和 9)。然后将 a和 B的地址分别赋给指针变量 pointer-1和 pointer-2,使 pointer-1
指向 a,pointer-2指向 B,见图 10.9(a)。接着执行 if
语句,由于 a< B,因此执行 Swap函数。注意实参
pointer-1和 pointer-2是指针变量,在函数调用时,
将实参变量的值传送给形参变量。采取的依然是
“值传递”方式。因此虚实结合后形参 p1的值为 &a,
p2的值为 &B。见图 10.9(B)。这时 p1和 pointer-1都指向变量 a,p2和 pointer-2都指向 B。接着执行 Swap
函数的函数体,使 *p1和 *p2的值互换,也就是使 a
和 B的值互换。互换后的情况见图 10.9(C)。函数调用结束后,p1和 p2不复存在 (已释放 ),情况如图
10.9(D)所示。最后在 main函数中输出的 a和 B的值已是经过交换的值
(a=9,B=5)。
图 10.9
请注意交换 *p1和 *p2的值是如何实现的。如果写成以下这样就有问题了:
Swap(int *p1,int *p2)
{ int temp;
*temp=*p1; /*此语句有问题 */
p1=*p2;
p2=*temp;

p1就是 a,是整型变量。而 *temp是指针变量 temp所指向的变量。但 temp中并无确定的地址值,它的值是不可预见的。 *temp所指向的单元也是不可预见的。因此,对 *temp赋值可能会破坏系统的正常工作状况。应该将 *p1的值赋给一个整型变量,
如程序所示那样,用整型变量 temp作为临时辅助变量实现 *p1和 *p2的交换。
注意,本例采取的方法是:交换 a和 B的值,而 p1和
p2的值不变。这恰和例 10.2相反。
可以看到,在执行 Swap函数后,变量 a和 B的值改变了。请仔细分析,这个改变是怎么实现的。这个改变不是通过将形参值传回实参来实现的。请读者考虑一下能否通过下面的函数实现 a和 B互换。
Swap(int x,int y)
{ int temp;
temp=x;
x=y;
y=temp;

如果在 main函数中用,Swap(a,B);”调用 Swap函数,
会有什么结果呢?如图 10.10所示。在函数调用时,
a的值传送给 x,B的值传送给 y。执行完 Swap函数后,x和 y的值是互换了,但 main函数中的 a和 B并未互换。也就是说由于“单向传送”的“值传递”
方式,形参值的改变无法传给实参。
为了使在函数中改变了的变量值能被 main函数所用,
不能采取上述把要改变值的图 10.10变量作为参数的办法,而应该用指针变量作为函数参数,在函数执行过程中使指针变量所指向的变量值发生变化,函数调用结束后,这些变量值的变化依然保留下来,
这样就实现了“通过调用函数使变量的值发生变化,
在主调函数 (如 main函数 )中使用这些改变了的值”
的目的。
如果想通过函数调用得到 n个要改变的值,可以:①在主调函数中设 n个变量,用 n个指针变量指向它们 ;②
然后将指针变量作实参,将这 n个变量的地址传给所调用的函数的形参 ;③ 通过形参指针变量,改变该 n
个变量的值 ;④ 主调函数中就可以使用这些改变了值的变量。请读者按此思路仔细理解例 10.3
程序。
请注意,不能企图通过改变指针形参的值而使指针实参的值改变。请看下面的程序:
Swap(int *p1,int *p2)
{ int p;
p=p1;
p1=p2;
p2=p;

main()

Swap(int *p1,int *p2)
{ int p;
p=p1;
p1=p2;
p2=p;

main()

int a,B;
int pointer-1,*pointer-2;
scanf("%D,%D",&a,&B);
pointer-1=&a;
pointer-2=&B;
if(a< B) Swap(pointer-1,pointer-2);
printf("\n%D,%D\n",*pointer-1,
*pointer-2);

作者的意图是:交换 pointer-1和 pointer-2的值,使
pointer-1指向值大的变量。其设想是:①先使
pointer-1指向 a,pointer-2指向 B,见图 10.11(a)。
②调用 Swap函数,将 pointer-1的值传给 p1,
pointer-2传给 p2,见图 10.11(B)。③在 Swap函数中使 p1与 p2的值交换,见图 10.11(C)。④形参 p1 2
将地址传回实参 pointer-1和 pointer-2,使 pointer-
1指向 B,pointer-2指向 a,见图 10.11(D)。然后输出 *pointer-1和 *pointer-2,想得到输出,9,5”。
图 10.11
但是这是办不到的,程序实际输出为,5,9”。问题出在第④步。 C语言中实参变量和形参变量之间的数据传递是单向的“值传递”方式。指针变量作函数参数也要遵循这一规则。调用函数不可能改变实参指针变量的值,但可以改变实参指针变量所指变量的值。我们知道,函数的调用可以 (而且只可以 )得到一个返回值 (即函数值 ),而运用指针变量作参数,可以得到多个变化了的值。难以做到这一点的。 例 10.4输入 a,B,C 3个整数,
按大小顺序输出。
Swap(int *pt1,int *pt2)
{ int temp;
temp=*pt1;
pt1=*pt2;
pt2=temp;

exChanGe(int *q1,int *q2,int *q3)
{ if(*q1< *q2) Swap(q1,q2);
if(*q1< *q3) Swap(q1,q3);
if(*q2< *q3) Swap(q2,q3);

main()

int a,B,C,*p1,*p2,*p3;
scanf("%D,%D,%D",&a,&B,&C);
p1=&a;p2=&B;p3=&C;
exChanGe(p1,p2,p3);
printf("\n%D,%D,%D\n",a,B,C);

运行情况如下:
9,0,10
10,9,0
10.3 数组的指针和指向数组的指针变量一个变量有地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组和数组元素 (把数组起始地址或某一元素的地址放到一个指针变量中 )。所谓数组的指针是指数组的起始地址,组元素的指针是数组元素的地址。
引用数组元素可以用下标法 (如 a[3]),也可以用指针法,即通过指向数组元素的指针找到所需的元素。
使用指针法能使目标程序质量高 (占内存少,运行速度快 )。
10.3.1 指向数组元素的指针定义一个指向数组元素的指针变量的方法,与以前介绍的指向变量的指针变量相同。
例如:
int a[10]; /*定义 a为包含 10个整型数据的数组 */
int p; /*定义 p为指向整型变量的指针变量 */
图 10.12
应当注意,如果数组为 int型,则指针变量亦应指向
int型。下面是对该指针元素赋值:
p=&a[0];
把 a[0]元素的地址赋给指针变量 p。也就是说,p指向 a数组的第 0号元素,见图 10.12。
C语言规定数组名代表数组的首地址,也就是第 0号元素的地址。因此,下面两个语句等价:
p=&a[0];
p=a;
注意数组 a不代表整个数组,上述,p=a;”的作用是
“把 a数组的首地址赋给指针变量 p”,而不是“把数组 a各元素的值赋给 p”。
在定义指针变量时可以赋给初值:
int p=&a[0];
它等效于
int p;
p=&a [0]; /*注意,不是 *p=&a [0];*/
当然定义时也可以写成
int p=a;
它的作用是将 a的首地址 (即 a[0]的地址 )赋给指针变量 p(而不是赋给 *p)。
10.3.2 通过指针引用数组元素假设 p已定义为指针变量,并已给它赋了一个地址,
使它指向某一个数组元素。如果有以下赋值语句:
p=1;
表示对 p当前所指向的数组元素赋予一个值 (值为 1)。
按 C的规定:如果指针变量 p已指向数组中的一个元素,则 p+1指向同一数组中的下一个元素 (而不是将 p值简单地加 1)。例如,数组元素是实型,每个元素占 4个字节,则 p+1意味着使 p的值 (地址 )加
4个字节,以使它指向下一元素。 p+1所代表的地址实际上是 p+1× D,D是一个数组元素所占的字节数 (对整型,D=2;对实型,D=4;对字符型,D=1)。
图 10.13
如果 p的初值为 &a[0],则:
(1) p+i和 a+i就是 a[i]的地址,或者说,它们指向 a数组的第 i个元素,见图 10.13。这里需要说明的是 a
代表数组首地址,a+i也是地址,它的计算方法同
p+i,即它的实际地址为 a+i× D。例如,p+9和 a+9
的值是 &a[9],它指向 a[9]。
(2) *(p+i)或 *(a+i)是 p+i或 a+i所指向的数组元素,即
a[i]。例如,*(p+5) (a+5)就是 a[5]。即
*(p+5)=*(a+5)=a[5]。实际上,在编译时,对数组元素 a[i]就是处理成 *(a+i),即按数组首地址加上相对位移量得到要找的元素的地址,然后找出该单元中的内容。例如,若数组 a的首地址为 1000,
设数组为整型,则 a[3]的地址是这样计算出来的:
1000+3× 2=1006,然后从 1006地址所标志的整型单元取出元素的值,即 a[3]的值。可以看出,[ ]实际上是变址运算符,即将 a[i]按 a+i计算地址,然后找出此地址单元中的值。
(3) 指向数组的指针变量也可以带下标,如 p[i]
(p+i)等价。
根据以上叙述,引用一个数组元素,可以用:
(1) 下标法,如 a[i]形式 ;
(2) 指针法,如 *(a+i) (p+i)。其中 a是数组名,p
是指向数组的指针变量,其初值 p=a。
例 10.5输出数组中的全部元素。假设有一个 a数组,整型,有
10个元素。要输出各元素的值有三种方法:
(1) 下标法。
main()

int a[10];
int i;
for(i=0;i< 10;i++)
scanf("%D",&a[i]);
printf("\n");
for(i=0;i< 10;i++)
printf("%D",a[i]);

(2) 通过数组名计算数组元素地址,找出元素的值。
main()

int a[10];
int i;
for(i=0;i< 10;i++)
scanf("%D",&a[i]);
printf("\n");
for(i=0;i< 10;i++)
printf("%D",*(a+i));

(3) 用指针变量指向数组元素。
main()

int a[10];
int*p,i;
for(i=0;i< 10;i++)
scanf("%D",&a[i]);
printf("\n");
for(p=a;p< (a+10);p++)
printf("%D ",*p);

以上 3个程序的运行情况均如下:
1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0
对三种方法的比较:
(1) 例 10.5的第 (1)和 (2)种方法执行效率是相同的。 C
编译系统是将 a[i]转换为 *(a+i)处理的。即先计算元素地址。因此用第 (1)和 (2)种方法找数组元素费时较多。
(2) 第 (3)种方法比 (1)(2)法快,用指针变量直接指向元素,不必每次都重新计算地址,像 p++这样的自加操作是比较快的。这种有规律地改变地址值
(p++)能大大提高执行效率。
(3) 用下标法比较直观,能直接知道是第几个元素。
例如,a[5]是数组中序号为 5的元素 (注意序号从 0
算起 )。用地址法或指针变量的方法不直观,难以很快地判断出当前处理的是哪一个元素。例如,
例 10.5第 (3)种方法所用的程序,要仔细分析指针变量 p的当前指向,才能判断当前输出的是第几个元素。
在使用指针变量时,有几个问题要注意:
(1) 指针变量可以实现使本身的值改变。例如,上述第 (3)种方法是用指针变量 p来指向元素,用 p++使
p的值不断改变,这是合法的。如果不用 p而使 a变化 (例如,用 a++)行不行呢?假如将上述 (3)程序的最后两行改为
for(p=a;a< (p+10);a++)
printf("%D",*a);
是不行的。因为 a是数组名,它是数组首地址,它的值在程序运行期间是固定不变的,是常量。 a++是什么意思呢?这是无法实现的。
(2) 要注意指针变量的当前值。请看下面的程序。
例 10.6通过指针变量输出 a数组的 10个元素。
有人编写出以下程序:
main()

int p,i,a[10];
p=a;
for(i=0;i< 10;i++)
scanf("%D",p++);
printf("\n");
for(i=0;i< 10;i++,p++)
printf("%D" p);

这个程序乍看起来好像没有什么问题。有的人即使已被告知此程序有问题,还是找不出它有什么问题。我们先看一下运行情况:
1 2 3 4 5 6 7 8 9 0
22153 234 0 0 30036 25202 11631 8259 8237 28483
显然输出的数值并不是 a数组中各元素的值。原因是指针变量的初始值为 a数组首地址 (见图 10.14中的
① ),但经过第一个 for循环读入数据后,p已指向 a
数组的末尾 (见图 10.14中② )。因此,在执行第二个 for循环时,p的起始值不是 &a[0]了,而是 a+10。
因为执行循环时,每次要执行 p++,p指向的是 a数组下面的 10个元素,而这些存储单元中的值是不可预料的。图 10.14p=a;解决这个问题的办法,只要在第二个 for循环之前加一个赋值语句:使 p的初始值回到 &a[0],这样结果就对了。
main()

int p,i,a[10];
p=a;
for(i=0;i< 10;i++)
scanf("%D",p++);
printf("\n");
p=a;
for(i=0;i< 10;i++,p++)
printf("%D " p);
} 图 10.14
运行情况如下:
1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0
(3) 从上例可以看到,虽然定义数组时指定它包含 10个元素,
可以用 p指向当前的数组元素。但是实际上指针变量 p可以指向数组以后的内存单元。如果引用数组元素 a[10],C编译程序并不认为非法,系统把它按 *(a+10)处理,即先找出
(a+10)的值 (是一个地址 ),然后找出它指向的单元的内容。
这样做虽然是合法的 (在编译时不出错 ),但应避免出现这样的情况,这会使程序得不到预期的结果。这种错误比较隐蔽,初学者往往难以发现。在使用指针变量指向数组元素时,应切实保证指向数组中有效的元素。
(4) 注意指针变量的运算。如果先使 p指向数组 a(即 p=a),则:
① p++(或 p+=1),使 p指向下一元素,即 a[1]。若再执行 *p,取出下一个元素 a[1]值。
② p++,由于 ++和 *同优先级,结合方向为自右而左,因此它等价于 *(p++)。作用是先得到 p指向的变量的值 (即 *p),然后再使 p+1=>p。
例 10.6最后一个程序中最后一个 for语句:
for(i=0;i< 10;i++,p++)
printf("%D",*p);
可以改写为
for(i=0;i< 10,i++)
printf("%D",*p++);
作用完全一样。它们的作用都是先输出 *p的值,然后使 p值加 1。这样下一次循环时,p就是下一个元素的值。
③ (p++)与 *(++p)作用不同。前者是先取 *p值,后使 p加 1。后者是先使 p加 1,再取 *p。若 p初值为
a(即 &a[0]),输出 *(p++)时,得 a[0]的值,而输出
*(++p),则得到 a[1]的值。
④ (*p)++表示 p所指向的元素值加 1,即 (a[0])++,
如果 a[0]=3,则 (a[0])++的值为 4。注意:是元素值加 1,而不是指针值加 1。
⑤ 如果 p当前指向 a数组中第 i个元素,则:
(p--)相当于 a[i--],先对 p进行,*”运算,再使 p自减。
(++p)相当于 a[++i],先使 p自加,再作 *运算。
(--p)相当于 a[--i],先使 p自减,再作 *运算。
将 ++和 --运算符用于指针变量十分有效,可以使指针变量自动向前或向后移动,指向下一个或上一个数组元素。例如,
想输出 a数组 100个元素,可以:
p=a; p=a
while(p<a+100) 或 while(p< a+100)
printf(“%D”,*p++); {printf("%D",*p);
p++;}
但如果不小心,很容易弄错。因此在用 *p++形式的运算时,
一定要十分小心,弄清楚先取 p值还是先使 p加 1。
10.3.3 用数组名作函数参数数组名可以用作函数的形参和实参。如:
main()f(int arr[ ],int n)
{ int array[10]; {


f(array,10); }


array为实参数组名,arr为形参数组名。在 7.7节已知,
当用数组名作参数时,如果形参数组中各元素的值发生变化,实参数组元素的值随之变化。这是为什么?在学习指针以后,对此问题就容易理解了。先看数组元素作实参时的情况。如果已定义一个函数,其原型为:
void Swap(int x,int y);
假设函数的作用是将两个形参 (x,y) 的值交换,今有以下的函数调用:
Swap(a[1],a[2]);
用数组元素 a[1],a[2]作实参的情况与用变量作实参时一样,是“值传递”方式,将 a[1]和 a[2]的值单向传递给 x,y。当 x和 y的值改变时,a[1]和 a[2]的值并不改变。
再看用数组名作函数参数的情况。前已介绍,实参数组名代表该数组首地址。而形参是用来接收从实参传递过来的数组首地址的。因此,形参应该是一个指针变量 (只有指针变量才能存放地址 )。实际上,C编译都是将形参数组作为指针变量来处理的。例如,上面给出的函数 f的形参是写成数组形式的:
f(int arr[ ],int n)
但在编译时是将 arr按指针变量处理的,相当于将函数 f
的首部写成
f(int *arr,int n)
以上两种写法是等价的。在调用该函数时,系统会建立一个指针变量 arr,用来存放从主调函数传递过 来的实参数组首地址。如果在 f函数中用 sizeof运算符测定 arr所占的字节数 (即 size of arr的值 ),结果为 2。这就证明了系统是把 arr作为指针变量来处理的。当 arr接收了实参数组的首地址后,arr就指向实参数组的开头,也就是指向 array[0]。因此,
*arr就是 array[0]的值。 arr+1指向 array[1],arr+2
指向 array[2],arr+3指向 array[3]。也就是说
*(arr+1),*(arr+2)*(arr+3)分别是 array[1],
array[2],array[3]的值。根据前面介绍过的知识,
*(arr+i)和 arr[i]是无条件等价的。因此,在调用函数期间,arr[0]和 *arr以及 array[0]都是数组 array
第 0个元素的值,依此类推,
arr[3],*(arr+3),array[3]都是 array数组第 3号元素的值,见图 10.15。这个道理与 10.2节 10.2.3段中的叙述是类似的。
图 10.15
常用这种方法通过调用一个函数来改变实参数组的值。
我们把用变量名作为函数参数和用数组名作为函数参数作一比较。
实参类型变量名数组名要求形参的类型变量名数组名或指针变量传递的信息变量的值数组的起始地址通过函数调用能否改变实参的值需要说明的是,C语言调用函数时虚实结合的方法都是采用“值传递”方式,当用变量名作为函数参数时传递的是变量的值,当用数组名作为函数参数时,
由于数组名代表的是数组起始地址,因此传递的值是数组首地址,所以要求形参为指针变量。
在用数组名作为函数实参时,既然实际上相应的形参是指针变量,为什么还允许使用形参数组的形式呢?这是因为在 C语言中用下标法和指针法都可以访问一个数组 (如果有一个数组 a,则 a[i]和 *(a+i)
无条件等价 ),用下标法表示比较直观,便于理解。
因此许多人愿意用数组名作形参,以便与实参数组对应。从应用的角度看,用户可以认为有一个形参数组,它从实参数组那里得到起始地址,因此形参数组与实参数组共占同一段内存单元,在调用函数期间,如果改变了形参数组的值,也就是改变了实参数组的值。当然在主调函数中可以利用这些已改变的值。对 C语言比较熟练的专业人员往往喜欢用指针变量作形参。
应该说明,实参数组后代表一个固定的地址,或者说是指针型常量,而形参数组并不是一个固定的地址值。作为指针变量,在函数调用开始时,它的值等于实参数组起始地址,但在函数执行期间,
它可以再被赋值。如:
f(arr[ ],int n)
{printf("%D\n",*arr);/*输出 array[0]的值,*/
arr=arr+3;
printf("%D\n",*arr);/*输出 array[3]的值,*/
*/
例 10.7将数组 a中 n个整数按相反顺序存放,见图
10.16示意。
解此题的算法为:将 a[0]与 a[n-1]对换,再将 a[1]与
a[n-2]对 ……,直到将图 10.16a[(n-1)/2]与 a[n-
int((n-1)/2)-1]对换。
今用循环处理此问题,设两个“位置指示变量” i和
j,i的初值为 0,j的初值为 n-1。将 a[i]与 a[j]交换,
然后使 i的值加 1,j的值减 1,再将 a[i]与 a[j]对换,
直到 i=(n-1)/2为止。
程序如下:
void inv(int x[ ],int n)/*形参 x是数组名 */

int temp,i,j,m=(n-1)/2;
for(i=0;i< =m;i++)
{ j=n-1-i;
temp=x[i];x[i]=x[j];x[j]=temp;}
return;

main()
{ int i,a[10]={ 3,7,9,11,0,6,7,5,4,
2} ;
printf("The oriGinal array,\n");
for(i=0;i< 10;i++)
printf("%D,",a[i]);
printf("\n");
inv(a,10);
printf("The array haS Been inverteD,\n");
for(i=0;i< 10;i++)
printf("%D,",a[i]);
printf("\n");

运行情况如下:
The oriGinal array:
3,7,9,11,0,6,7,5,4,2,
The array haS Been inverteD:
2,4,5,7,6,0,11,9,7,3,
图 10.16
主函数中数组名为 a,赋以各元素初值。函数 inv中的形参数组名为 x。在 inv函数中不必具体定义数组元素的个数,元素个数由实参传给形参 n(今实参值为 10)。这样做可以增加函数的灵活性。即不必要求函数 inv中的形参数组 x和 main函数中的实参数组 a长度相同。如果在 main函数中有函数调用语句,inv(a,10),表示要求对 a数组的前 10个元素实行题目要求的颠倒排列。
如果改为,inv(a,5),则表示要求将 a数组的前 5个元素实行颠倒排列,此时,函数 inv只处理 5个数组元素。
函数 inv中的 m是 i值的上限,当 i≤m时,循环继续执行 ;
当 i> m时,则结束循环过程。
例如,若 n=10,则 m=4,最后一次 a[i]与 a[j]的交换是
a[4]与 a[5]交换。
对这个程序可以作一些改动。
将函数 inv中的形参 x改成指针变量。实参为数组名 a,
即数组 a的首地址,将它传给形参指针变量 x,这时 x
就指向 a[0]。 x+m是 a[m]元素的地址。设 i和 j以及 p都是指针变量,用它们指向有关元素。 i的初值为 x,j
的初值为 x+n-1,见图 10.17。
使 *i与 *j交换就是使 a[i]与
a[j]交换。
图 10.17
程序如下:
void inv(int*x,int n)/*形参 x为指针变量 */

int p,temp,*i,*j,m=(n-1)/2;
i=x;j=x+n-1;p=x+m;
for(;i< =p;i++,j--)
{ temp=*i;*i=*j;*j=temp;}
return;

main()
{ int i,a[10]={ 3,7,9,111,0,6,7,5,4,
2} ;
printf("The oriGinal array,\n");
for(i=0;i< 10;i++)
printf("%D,",a[i]);
printf("\n");
inv(a,10);
printf("The array haS Been inverteD,\n");
for(i=0;i< 10;i++)
printf("%D,",a[i]);
printf("\n");

运行情况与前一程序相同。
例 10.8从 10个数中找出其中最大值和最小值。
本题不要求改变数组元素的值,只要求得到最大值和最小值。但是调用一个函数只能得到一个返回值,为了能得到两个结果值,今用全局变量在函数之间“传递”数据。程序如下:
int max,min;/*全局变量 */
void max-min-value(int array[ ],int n)
{ int p,*array-enD;
array-enD=array+n;
max=min=*array;
for(p=array+1;p< array-enD;p++)
if(*p> max)max=*p;
else if(*p< min)min=*p;
return;

main()
{ int i,numBer[10];
printf("enter 10 inteGer numBerS:\n");
for(i=0;i< 10;i++)
scanf("%D",&numBer[i]);
max-min-value(numBer,10);
printf("\nmax=%D,min=%D\n",max,
min);

运行结果如下:
enter 10 integer numbers:
-2 4 6 8 0 -3 45 67 89 100
max=100,min=-3
在函数 max-min-value中求出的最大值和最小值放在
max和 min中。由于它们是全局变量,因此在主函数中可以直接使用。
图 10.18
函数 max-min-value中的语句:
max=min=*array;
array是形参数组名,它接收从实参传来的数组
numBer的首地址。 array是形参数组的首地址,
*array相当于 *(array+0),即 array[0]。上述语句与下面语句等价:
max=min=array[0];
见图 10.18。在执行 for循环时,p的初值为 array+1,
也就是使 p指向 array[1]。以后每次执行 p++,使 p
指向下一个元素。每次将 *p和 max与 min比较,将大者放入 max,小者放入 min。
与上例相似,函数 max-min-value的形参 array可以改为指针变量类型。即将该函数首部改为 void
max-min-value(int *array,int n)效果相同。
实参也可以不用数组名,而用指针变量传递地址,形参仍用指针变量。程序可改为:
int max,min;
void max-min-value(int *array,int n)
{ int p,*array-end;
array-end=array+n;
max=min=*array;
for(p=array+1;p< array-enD;p++)
{ if(*p> max)max=*p;
else if(*p< min)min=*p;}
return;

main()
{ int i,number[10],*p;
p=numBer;/*使 p指向 number数组 */
printf("enter 10 integer numbers:\n");
for(i=0;i< 10;i++,p++)
scanf("%d",p);
printf("the 10integer numbers,\n");
for(p=number,i=0;i< 10;i++,p++)
printf("%d",*p);
p=number;
max-min-value(p,10);/*实参用指针变量 */
printf("\nmax=%d,min=%d\n",max,min);

归纳起来,如果有一个实参数组,想在函数中改变此数组的元素的值,实参与形参的表示形式有以下 4种情况:
图 10.19
(1) 形参和实参都用数组名,如:
main()
f(intx[ ],int n)
{ int a[10];
{


f(a,10);}


可以认为有一形参数组,与实参数组共用一段内存单元,
这种形式比较好理解,见图 10.19。例 10.7第一个程序即属此情况。
图 10.20
(2) 实参用数组名,形参用指针变量。如:
main()f(int *x,int n)
{ int a[10];
{


f(a,10);}


实参 a为数组名,形参 x为指向整型变量的指针变量,
函数开始执行时,x指向 a[0],即 x=&a[0],见图
10.20。通过 x值的改变,可以指向 a数组的任一元素。例 10.7的第二个程序就属此类。
(3) 实参形参都用指针变量。例如:
main()f(int *x,int n)
{ int a[10],*p;
{
p=a;

…}
f(p,10);


实参 p和形参 x都是指针变量。先使实参指针变量 p
指向数组 a,p的值是 &a[0]。
图 10.21
然后将 p的值传给形参指针变量 x,x的初始值也是
&a[0],见图 10.21。通过 x值的改变可以使 x指向数组 a的任一元素。例 10.8第二个程序就属此类。
(4) 实参为指针变量,形参为数组名。如:
main()f(int x
[ ],int n)
{ int a[10],*p;
{
p=a;

… }
f(p,10);


图 10.22
实参 p为指针变量,它使指针变量 p指向 a[0],即 p=a或
p=&a[0]。形参为数组名 x,从前面的介绍已知,实际上将 x作为指针变量处理,今将 a[0]的地址传给形参 x,使指针变量 x指向 a[0]。也可以理解为形参数组 x取得 a数组的首地址,x数组和 a数组共用同一段内存单元。见图 10.22。在函数执行过程中可以使 x[i]
值变化,它就是 a[i]。主函数可以使用变化了的数组元素值。例 10.7的程序可以改写为例 10.9。
例 10.9用实参指针变量改写例 10.7。
void inv(int *x,int n)
{ int p,m,temp,*i,*j;
m=(n-1)/2;
i=x;j=x+n-1;p=x+m;
for(;i< =p;i++,j--)
{ temp=*i;*i=*j;*j=temp;}
return;

main()
{ int i,arr[10],*p=arr;
printf("The original array,\n");
for(i=0;i< 10;i++,p++)
scanf("%d",p);
printf("\n");
p=arr;
inv(p,10);/*实参为指针变量 */
printf("The array has been inverted,\n");
for(p=arr;p< arr+10;p++)
printf("%d",*p);
printf("\n");

注意,上面的 main函数中的指针变量 p是有确定值的。如果在 main函数中不设数组,只设指针变量,
就会出错,假如把程序修改如下:
main()
{ int i arr;
printf("The oriGinal array,\n");
for(i=0;i< 10;i++)
scanf("%D",arr+i);
printf("\n");
inv(arr,10);/*实参为指针变量,但未被赋值 */
printf("The array haS Been inverteD,\n");
for(i=0;i< 10;i++)
printf("%D",*(arr+i));
printf("\n");

编译时出错,原因是指针变量 arr没有确定值,谈不上指向哪个变量。下面的使用是不正确的:
main()f(x[ ],int n)
{ int *p;
{
f(p,n);

… }

应注意,如果用指针变量作实参,必须先使指针变量有确定值,指向一个已定义的数组。
以上四种方法,实质上都是地址的传递。其中 (1)(4)
两种只是形式上不同,实际上都是使用指针变量。
例 10.10用选择法对 10个整数排序。
程序如下:
main()
{ int p,i,a[10];
p=a;
for(i=0;i< 10;i++)
scanf("%D",p++);
p=a;
Sort(p,10);
for(p=a,i=0;i< 10;i++)
{ printf("%D",*p);p++;}

sort(int x[ ],int n)
{ int i,j,k,t;
for(i=0;i< n-1;i++)
{ k=i;
for(j=i+1;j< n;j++)
if(x[j]> x[k]) k=j;
if(k! =i)
{ t=x[i];x[i]=x[k];x[k]=t;}


为了便于理解,函数 Sort中用数组名作为形参,用下标法引用形参数组元素,这样的程序很容易看懂。当然也可以改用指针变量,这时 Sort函数的首部可以改为
sort(int x,int n)
其他不改,程序运行结果不变。可以看到,即使在函数 Sort中将 x定义为指针变量,在函数中仍可用
x[i],x[k]这样的形式表示数组元素,它就是 x+i和
x+k所指的数组元素。它等价于
sort(int *x,int n)
{ int i,j,k,t;
for(i=0;i< n-1;i++)
{ k=i;
for(j=i+1;j< n;j++)
if(*(x+j)> *(x+k)) k=j;
if(k! =i)
{ t=*(x+i);*(x+i)=*(x+k);*(x+k)=t;



请读者自己理解消化程序。
10.3.4 指向多维数组的指针和指针变量用指针变量可以指向一维数组,也可以指向多维数组。但在概念上和使用上,多维数组的指针比一维数组的指针要复杂一些。
1,多维数组的地址为了说清楚多维数组的指针,先回顾一下多维数组的性质。今以二维数组为例,设有一个二维数组 a,
它有 3行 4列。
它的定义为
int a[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],见图 10.23。
图 10.23
从二维数组的角度来看,a代表整个二维数组的首地址,也就是第 0行的首地址。 a+1代表第 1行的首地址。如果二维数组的首地址为 2000,则 a+1为 2008,
因为第 0行有 4个整型数据,因此 a+1的含义是 a[1]
的地址,即 a+4× 2=2008。 a+2代表第 2行的首地址,
它的值是 2016,见图 10.24。
a[0],a[1],a[2]既然是一维数组名,而 C语言又规定了数组名代表数组的首地址,因此 a[0]代表第 0
行一维数组中第 0列元素的地址,即 &a[0][0]。 a[1]
的值是 &a[1][0],a[2]的值是 &a[2][0]。
图 10.24
请考虑第 0行第 1列元素的地址怎么表示?可以用
a[0]+1来表示,见图 10.25。此时,a[0]+1”中的 1代表 1个列元素的字节数,即 2个字节。今 a[0]的值是
2000,a[0]+1的值是 2002(而不是 2008)。这是因为现在是在一维数组范围内讨论问题的,正如有一个一维数组 x,x+1是其第 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图 10.25[0][2],&a[0][3])。前已述及,
a[0] (a+0)等价,a[1] (a+1)等价,a[i]和
*(a+i)等价。因此,a[0]+1和 (a+0)+1的值都是
&a[0][1] (即图 10.25中的 2002)。 a[1]+2和 *(a+1)+2
的值都是 &a[1][2](即图中的 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]是等价的。
有必要对 a[i]的性质作进一步说明。 a[i]从形式上看是 a数组中第 i个元素。如果 a是一维数组名,则 a[i]
代表 a数组第 i个元素所占的内存单元。 a[i]是有物理地址的,是占内存单元的。但如果 a是二维数组,
则 a[i]是代表一维数组名。 a[i]本身并不占它也不存放 a数组中各个元素的值。它只是一个地址 (如同一个一维数组名 x并不占内存单元而只代表地址一样 )。 a,a+i,a[i],*(a+i),*(a+i)+j、
a[i]+j都是地址。 *(a[i]+j),*(*(a+i)+j)是二维数组元素 a[i][j]的值。有些读者可能不理解为什么 a+1
和 *(a+1)都是 2008呢?他们想,a+1的值和 a+1的地址怎么都是一样的呢”?的确,二维数组中有些概念比较复杂难懂,要反复思考。首先说明,
a+1是地址 (指向第 1行首地址 ),而 *(a+1)并不是
,a+1单元的内容 (值 )”,因为 a+1并不是一个实际变量,也就谈不上它的内容。 *(a+1)就是 a[1],而
a[1]是一维数组名,所以也是地址。以上各种形式都是地址计算的不同表示。
为了说明这个容易搞混的问题,举一个日常生活中的例子来说明。有一个排,下设 3个班,每班有 10
名战士。规定排长只管理到班,班长管理战士。
在排长眼里只有第 0,1,2班 (为与 C语言中数组下标一致,假定班号也从 0开始 )。排长从第 0班的起始位置走到第 1班的起始位置,看来只走了一步,
但实际上它跳过了 10个战士。这相当于 a+1(见图
10.26)。为了找到某一班内某一个战士,必须给两个参数,即第 i班第 j个战士,先找到第 i班,然后由该班班长在本班范围内找第 j个战士。这个战士的位置就是 a[i]+j(这是一个地址 )。开始时班长面对第 0个战士。注意,排长和班长的初始位置是相同的 (如图 10.25中的 a和 a[0]都是 2000)。但它们的
“指向”是不同的。
排长“指向”班,他走一步就跳过 1个班,而班长
“指向”战士,走一步只是指向下一个战士。可以看到排长是“宏观管理”,只管班,在图 10.26
中是控制纵向,班长则是“微观管理”,管理到战士,在图上是控制横向。如果要找第 1班第 2个战士,则先由排长找到第 1班的班长,然后,由班长在本班范围内找到第 2个战士。二维数组 a相当于排长,每一行 (即一维数组 a[0],a[1],a[2])相当于班长,每一行中的元素 (如 a[1][2])相当于战士。
a+1与 a[0]+1是不同的,a+1是第 1行的首地址,
a+1指向第 1行 (相当于排长走到第 1班的开头 ),而
*(a+1)或 a[1]或 a[1]+0都指向第 1行第 0列元素 (相当于第 1班第 0个战士 ),二者地址虽相同,但含义不同了。前者是“纵向控制”,后者是“横向控制”。
a,a[0]的值虽然相同 (等于 2000),但是由于指针的类型不同 (a是指向一维数组,a[0]指向 a[0][0]元素 )因此,对这些指针进行加 1的运算,得到的结果是不同的。 请记住,二维数组名 (如 a)是指向行的。因此 a+1中的,1” 代表一行中全部元素所占的字节数
(图 10.25表示为 8个字节 )。一维数组名 (如 a[0],
a[1])是指向列元素的。 a[0]+1中的,1” 代表一个元素所占的字节数 (图 10.25表示为 2个字节 )。在行指针前面加一个 *,就转换为列指针。例如,a和
a+1是行指针,在它们前面加一个 *就是 *a和
*(a+1),它们就成为列指针,分别指向 a数组 0行 0
列的元素和 1行 0列的元素 。反之,在列指针前面加 &,就成为行指针。例如 a[0] 是指向 0行 0列元素的列指针,
在它前面加一个 &,得 &a[0],由于 a[0] 与 *(a+0)
等价,因此 &a[0] 与 &*a等价,也就是与 a等价,
它指向二维数组的 0行。
图 10.25
图 10.26
不要把 &a[i]简单地理解为 a[i]单元的物理地址,因为并不存在 a[i]这样一个变量。它只是一种地址的计算方法,能得到第 i行的首地址,&a[i]和 a[i]的值是一样的,但它们的含义是不同的。 &a[i]或 a+i指向行,而 a[i] (a+i)指向列。当列下标 j为 0时,&a[i]
和 a[i](即 a[i]+j)值相等,即它们具有同一地址值。
(a+i)只是 a[i]的另一种表示形式,不要简单地认为是
,a+i所指单元中的内容”。在一维数组中 a+i所指向的是一个数组元素的存储单元,它有具体值,上述说法是正确的。而对二维数组,a+i不是指向具体存储单元而指向行。在二维数组中,a+i=a[i]=
(a+i)=&a[i]=&a[i][0],即它们的地址值是相等的。
请读者仔细琢磨其概念。请分析下面的程序,以加深对上面叙述的理解。
例 10.11输出二维数组有关的值。
#define FORMAT "%D,%D\n"
main()
{ int a[3][4]={ 1,3,5,7,9,11,13,15,
17,19,21,23} ;
printf(FORMAT,a,*a);
printf(FORMAT,a[0],*(a+0));
printf(FORMAT,&a[0],&a[0][0]);
printf(FORMAT,a[1],a+1);
printf(FORMAT,&a[1][0],*(a+1)+0);
printf(FORMAT,a[2],*(a+2));
printf(FORMAT,&a[2],a+2);
printf(FORMAT,a[1][0],*(*(a+1)+0));

运行结果如下:
158,158(第 0行首地址和 0行 0列元素地址 )
158,158(0行 0列元素地址 )
158,158(0行首地址和 0行 0元素地址 )
166,166(1行 0列元素地址和 1行首地址 )
166,166(1行 0列元素地址 )
174,174(2行 0列元素地址 )
174,174(第 2行首地址 )
9,9(1行 0列元素的值 )
请注意,a是二维数组名,代表数组首地址,但是不能企图用 *a来得到 a[0][0]的值。 *a相当于 *(a+0),
即 a[0],它是第 0行地址 (本次程序运行时输出 a、
a[0]和 *a的值都是 158,都是地址。请注意:每次编译分配的地址是不同的 )。 a是指向一维数组的指针,可理解为行指针,*a是指向列元素的指针,
可理解为列指针,指向 0行 0列元素,**a是 0行 0列元素的值。同样,a+1指向第 1行首地址,但也不能企图用 *(a+1)得到 a[1][0]的值,而应该用 **(a+1)
求 a[1][0]元素的值。
2,指向多维数组的指针变量在了解上面的概念后,可以用指针变量指向多维数组及其元素。
(1) 指向数组元素的指针变量。
例 10.12用指针变量输出数组元素的值。
main()
{ int a[3][4]={ 1,3,5,7,9,11,13,15,
17,19,21,23} ;
int p;
for(p=a[0];p< a[0]+12;p++)
{ if((p-a[0])%4==0)printf("\n");
printf("%4D",*p);


运行结果如下:
1 3 5 7
9 11 13 15
17 19 21 23
p是一个指向整型变量的指针变量,它可以指向一般的整型变量,也可以指向整型的数组元素。每次使
p值加 1,以移向下一元素。 if语句的作用是使一行输出 4个数据,然后换行。如果读者对 p的值还缺乏具体概念的话,可以把 p的值 (即数组元素的地址 )输出。可将程序最后两个语句改为
printf("addr=%o,value=%4d\n",p,*p);这时输出如下:
addr=236,value=1
addr=240,value=3
addr=242,value=5
addr=244,value=7
addr=246,value=9
addr=250,value=11
addr=252,value=13
addr=254,value=15
addr=256,value=17
addr=260,value=19
addr=262,value=21
addr=264,value=23
注意地址是以八进制数表示的 (输出格式符为 %o)。
上例是顺序输出数组中各元素之值,比较简单。如果要输出某个指定的数组元素 (例如 a[1][2]),则应事先计算该元素在数组中的相对位置 (即相对于数组起始位置的相对位移量 )。计算 a[i][j]在数组中的相对位置的计算公式为 i*m+j其中 m为二维数组的列数 (二维数组大小为 n× m)。例如,对上述 3× 4的二维数组,它的第 2行第 3列元素 (a[2][3])对 a[0][0]
的相对位置为 2*4+3=11。如果开始时使指针变量
p指向 a(即 (a[0][0]),为了得到 a[2][3]的值,可以用
*(p+2*4+3)表示。 (p+11)是 a[2][3]的地址。 a[i][j]
的地址为 a[0]+i*m+j。下面来说明上述
(a[0]+i*m+j)中的 i*m+j公式的含义。从图 10.27可以看到在 a[i][j]元素之前有 i行元素 (每行有 m个元素 ),在 a[i][j]所在行,a[i][j]的前面还有 j个元素,
因此 a[i][j]之前共有 i× m+j个元素。例如,a[2][3]
的前面有两行 (共 2× 4=8个 )元素,在它本行内还有 3?+3=11个元素在它之前。
可用 p+11表示其相对位置。
图 10.27
可以看到,C语言规定数组下标从 0开始,对计算上述相对位置比较方便,只要知道 i和 j的值,就可以直接用 i× m+j公式计算出 a[i][j]相对于数组开头的相对位置。如果规定下标从 1开始 (如 FORTRAN
语言 ),
则为计算 a[i][j]的相对位置所用的公式就要改为 (i-
1)× m+(j-1)。这就增加了计算的工作量,而且不直观。
(2) 指向由 m个元素组成的一维数组的指针变量。
上例的指针变量 p是指向整型变量的,p+1所指向的元素是 p所指向的元素的下一元素。可以改用另一方法,使 p不是指向整型变量,而是指向一个包含
m个元素的一维数组。这时,如果 p先指向 a[0](即
p=&a[0]),则 p+1不是指向 a[0][1],而是指向 a[1],
p的增值以一维数组的长度为单位,见图 10.28。
图 10.28
例 10.13输出二维数组任一行任一列元素的值。
main()
{ int a[3][4]={ 1,3,5,7,9,11,13,15,
17,19,21,23} ;
int (*p)[4],i,j;
p=a;
scanf(" i=%d,j=%d",&i,&j);
printf("a[%d,%d]=%d\n",i,j,
*(*(p+i)+j));

运行情况如下:
i=1,j=2 (本行为键盘输入 )
a[1,2]=13
注意应输入,i=1,j=2”,以与 scanf函数中指定的字符串相对应。
程序第 3行,int(*p)[4]”表示 p是一个指针变量,它指向包含 4个元素的一维数组。注意 *p两侧的括号不可缺少,如果写成 *p[4],由于方括号 []运算级别高,因此 p先与 [4]结合,是数组,然后再与前面的 *结合,*p[4]是指针数组 (见 10.7节 )。有的读者感到,(*p)[4]”这种形式不好理解。可以对下面二者做比较:
① int a[4];(a有 4个元素,每个元素为整型 )
② int (*p)[4];
第②种形式表示 *p有 4个元素,每个元素为整型。
也就是 p所指的对象是有 4个整型元素的数组,即 p
是行指针,见图 10.29。应该记住,此时 p只能指向一个包含 4个元素的一维数组,p的值就是该一维数组的首地址。 p不能指向一维数组中的第 j个元素。
图 10.29
程序中的 p+i是二维数组 a的第 i行的地址 (由于 p是指向一维数组的指针变量,因此 p加 1,就指向下一个一维数组 )。见图 10.30。 *(p+2)+3是 a数组第 2行第 3列元素地址,这是指向列的指针,(p+2)+3)是
a[2][3]的值。有的读者可能会想,(p+2)是第 2行 0
列元素的地址,而 p+2是第 2行首地址,二者的值相同,(p+2)+3能否写成 (p+2)+3呢?显然不行。因为 (p+2)+3就成了 (p+5)了,是第 5行的首地址了。
(p+2)+3”,括弧中的 2是以一维数组的长度为单位的,即 p每加 1,地址就增加 8个字节 (4个元素,每个元素 2个字节 ),而 (p+2)+3括弧外的数字 3,
不是以 p所指向的一维数组为长度单位的。而是采用 p所指向的一维数组内部各元素的长度单位了,
加 3就是加 (3× 2)个字节。 p+2和 (p+2)具有相同的值,但 (p+2)+3 (p+2)+3的值就不相同了。这和上一节所叙述的概念是一致的。
图 10.30
3,多维数组的指针作函数参数一维数组的地址可以作为函数参数传递,多维数组的地址也可作函数参数传递。在用指针变量作形参以接受实参数组名传递来的地址时,有两种方法:① 用指向变量的指针变量 ;② 用指向一维数组的指针变量。
例 10.14有一个班,3个学生,各学 4门课,计算总平均分数,以及第 n个学生的成绩。
这个题目本来是很简单的。只是为了说明用多维数组指针作函数参数而举的例子。用函数 average求总平均成绩,用函数 search找出并输出第 i个学生的成绩。程序如下:
main()
{ void average(float *p,int n);
void search(float (*p)[4],int n);
float score[3][4]={{65,67,70,60},{80,
87,90,81},{90,99,100,98}};
average(*score,12); /*求 12个分数的平均分
*/
search(score,2); /*求第 2个学生成绩 */

void average(float *p,int n)
{ float p-end;
float sum=0,aver;
p-end=p+n-1;
for(;p< =p-end;p++)
sum=sum+(*p);
aver=sum/n;
printf("average=%5.2f\n",aver);

void search(float (*p)[4],int n)
{ int i;
printf("the score of No.%D are,\n",n);
for(i=0;i< 4;i++)
printf("%5.2f " (*(p+n)+i));

程序运行结果如下:
average=82.25
the score of No.2 are:
90.00 99.00 100.00 98.00
在函数 main中,先调用 average函数以求总平均值。在函数 average
中形参 p被声明为指向一个实型变量的指针变量。用 p指向二维数组的各个元素,p每加 1就改为指向下一个元素,见图 10.31。相应的实参用 *score,即 score[0],
它是一个地址,指向 score[0][0]
元素。用形参 n代表需要求平均值的元素的个数,实参 12表示要求 12个元素值的平均值。函数
average中的指针变量 p指向 score
数组的某一元素 (元素值为 图 10.31
一门课的成绩 )。 sum是累计总分,aver是平均值。
在函数中输出 aver的值,故函数无需返回值。
函数 search的形参 p不是指向一般实型变量的指针变量,而是指向包含 4个元素的一维数组的指针变量。
实参传给形参 n的值为 2,即找序号为 2的学生的成绩 (3个学生的序号分别为 0,1,2)。函数调用开始时,将实参 score的值 (代表该数组第 0行首地址 )传给 p,使 p也等于 score。 p+n是一维数组 score[n]的首地址,*(p+n)+i是 score[n][i]的地址,*(*(p+n)+i)
是 score[n][i]的值。现在 n=2,i由 0变到 3,for循环输出 score[2][0]到 score[2][3]的值。
例 10.15在上题基础上,查找有一门以上课程不及格的学生,打印出他们的全部课程的成绩。程序如下:
main()
{ void search(float (*p)[4],int n); /*函数声明 */
float score[3][4]={{65,57,70,60},{58,
87,90,81},{90,99,100,98}};
search(score,3);

void search(float (*p)[4],int n)
{ int i,j,flag;
for(j=0;j< n;j++)
{ flag=0;
for(i=0;i< 4;i++)
if(*(*(p+j)+i)< 60) flag=1;Break;}
if(flag==1)
{ printf("No.%D fails,his scores
are:\n",j+1);
for(i=0;i< 4;i++)
printf("%5.1f ",*(*(p+j)+i));
printf("\n");



程序运行结果如下:
No.1 fails,his scores are:
65.0 57.0 70.0 60.0
No.2 fails,his scores are:
58.0 87.0 90.0 81.0
在函数 search中,flag是作为标志不及格的变量。先使 flag=0,若发现某一学生有一门不及格,则使
flag=1。最后用 if语句检查 flag,如为 1,则表示该学生有不及格的纪录,输出其全部课程成绩。变量 j代表学生号,i代表课程号。
通过指针变量存取数组元素速度快,且程序简明。
用指针变量作形参,可以允许数组的行数不同。
因此数组与指针常常是紧密联系的,使用熟练的话可以使程序质量提高,且编写程序方便灵活。
10.4.1 字符串的表示形式在 C程序中,可以用两种方法访问一个字符串。
(1) 用字符数组存放一个字符串,然后输出该字符串。
例 10.16
main()
{ char string[]="I love China! ";
printf("%S\n",string);

10.4 字符串的指针和指向字符串的指针变量运行时输出:
I love China!
和前面介绍的数组属性一样,
string是数组名,它代表字符数组的首地址 (见图
10.32)。 string[4]代表数组中序号为 4的元素 (v),实际上 string[4]就是 *(string+4),
string+4是一个地址,它指向字符,v”。
图 10.32
(2) 用字符指针指向一个字符串。
可以不定义字符数组,而定义一个字符指针。用字符指针指向字符串中的字符。
例 10.17
main()
{ char string="I love China! ";
printf("%s\n",string);

在这里没有定义字符数组,在程序中定义了一个字符指针变量 string。给定一个字符串常量,I love
China!”,C语言对字符串常量是按字符数组处理的,
在内存开辟了一个字符数组用来存放字符串常量。程序在定义字符指针变量 string时把字符串首地址 (即存放字符串的字符数组的首地址 )赋给 string(见图 10.33)。
有人认为 string是一个字符串变量,以为是在定义时把,I love
China!”赋给该字符串变量,
这是不对的。定义 string的部分
char string="I love China! ";
等价于下面两行:
char string;
string="I love China! "; 图 10.33
可以看到 string被定义为一个指针变量,指向字符型数据,请注意它只能指向一个字符变量或其他字符类型数据,不能同时指向多个字符数据,更不是把,I love China!”这些字符存放到 string中
(指针变量只能存放地址 ),也不是把字符串赋给
*string。只是把,I love China!”的首地址赋给指针变量 string。不要认为上述定义行等价于
char string;
*string="I love China! ";
在输出时,用
printf("%sn",string);
%s示输出一个字符串,给出字符指针变量名 string,
则系统先输出它所指向的一个字符数据,然后自动使 string加 1,使之指向下一个字符,然后再输出一个字符 …… 如此直到遇到字符串结束标志
‘ \0?为止。注意,在内存中,字符串的最后被自动加了一个‘ \0?(如图 10.33所示 ),因此在输出时能确定字符串的终止位置。
通过字符数组名或字符指针变量可以输出一个字符串。而对一个数值型数组,是不能企图用数组名输出它的全部元素的。如:
int i[10]

printf("%D\n",i);
是不行的,只能逐个元素输出。
显然,用 %S可以对一个字符串进行整体的输入输出。
对字符串中字符的存取,可以用下标方法,也可以用指针方法。
例 10.18将字符串 a复制为字符串 b。
main()
{ char a[]="i am a boy.",b[20];
int i;
for(i=0;*(a+i)! =′\0′;i++)
*(b+i)=*(a+i);
*(b+i)=′\0′;
printf("string a is,%s\n",a);
printf("string b is,");
for(i=0;b[i]! =′\0′;i++)
printf("%c",b[i]);
printf("\n");

程序运行结果为:
string a is,I am a Boy.
string b is,I am a Boy.
程序中 a和 B都定义为字符数组,可以通过地址访问数组元素。在 for语句中,先检查 a[i]是否为‘ \0?(今 a[i]是以 *(a+i)形式表示的 )。如果不等于‘ \0?,表示字符串尚未处理完,就将 a[i]的值赋给 B[i],即复制一个字符。
在 for循环中将 a串全部复制给了 B串。最后还应将‘ \
0? (B+i)=′\0′;此时 i的值是字符串有效字符的个数 n加 1。第二个 for循环中用下标法表示一个数组元素 (即一个字符 )。
也可以设指针变量,用它的值的改变来指向字符串中的不同的字符。例 10.19用指针变量来处理例 10.18问题。
main()
{ char a[]="I am a Boy.",B[20],*p1,*p2;
int i;
p1=a;p2=B;
for(;*p1! =′\0′;p1++,p2++)
*p2=*p1;
*p2=′\0′;
printf("string a is,%S\n",a);
printf("string B is,");
for(i=0;B[i]! =′\0′;i++)
printf("%C",B[i]);
printf("\n");

p1,p2是指针变量,它指向字符型数据。先使 p1和 p2的值分别为字符串 a和 B的首地址。 *p1最初的值为‘ I?,
赋值语句,*p2=*p1;”的作用是将字符‘ I?(a串中第 1个字符 )赋给 p2所指向的元素,
即 B[1]。然后 p1和 p2分别加
1,指向其下面的一个元素,
直到 *p1的值为‘ \0?止。注意 p1和 p2的值是不断在改变的,见图 10.34的虚线和 p1′、
p2′。程序必须保证使 p1和
p2同步移动。
图 10.34
10.4.2 字符串指针作函数参数将一个字符串从一个函数传递到另一个函数,可以用地址传递的办法,即用字符数组名作参数或用指向字符串的指针变量作参数。在被调用的函数中可以改变字符串的内容,在主调函数中可以得到改变了的字符串。
例 10.20用函数调用实现字符串的复制。
(1) 用字符数组作参数。
void copy-string
(char from[ ],char to[ ])
{ int i=0;
while(from[i]! =′\0′)
{ to[i]=from[i];i++;}
to[i]=′\0′;

main()
{ char a[]="I am a teacher.";
char B[]="you are a student.";
printf("string a=%s\nstring b=%s\n",a,
b);
copy-string(a,b);
printf("\nstring a=%S\nstring b=%s\n",a,
b);

程序运行结果如下:
string-a=I am a teacher.
string-b=you are a Student.
string-a=I am a teacher.
string-b=I am a teacher.
a和 b是字符数组。初值如图 10.35(a)所示。 copy-
string函数的作用是将 from[i]赋给 to[i],直到
from[i]的值是‘ \0?为止。在调用 copy-string函数时,将 a和 b的首地址分别传递给形参数组 from和
to。因此 from[i]和 a[i]是同一个单元,to[i]和 B[i]是同一个单元。程序执行完以后,B数组的内容如图
10.35(B)所示。可以看到,由于 B数组原来的长度大于 a数组,因此在将 a数组复制到 b数组后,未能全部覆盖 b数组原有内容。 b数组最后 3个元素仍保留原状。在输出 B时由于按 %S(字符串 )输出,遇
‘ \0?即告结束,因此第一个 \0?后的字符不输出。
如果不采取 %S格式输出而用 %C逐个字符输出是可以输出后面这些字符的。
图 10.35
在 main函数中也可以不定义字符数组,而用字符型指针变量。 main函数可改写如下:
main()
{ char *a="I am a teacher.";
char *b="you are a student.";
printf("string a=%s\nstring b=%s\n",a,
b);
Copy-string(a,b);
printf("\nstring a=%s\nstring b=%s\n",a,
b);

与上面程序运行结果相同。
(2) 形参用字符指针变量。程序如下:
void copy-string(char *from,char *to)
{ for(;*from! =′\0′;from++,to++)
*to= from;
*to=′\0′;

main()
{ char*a="I am a teacher.";
char*b="you are a student.";
printf("\nstring a=%S\nstring b=%S\n",a,
b);
copy-string(a,b);
printf("\nstring a=%S\nstring b=%s\n",a,b);

形参 from和 to是字符指针变量。它们相当于例 10.19中的 p1和 p2。算法也与例 10.19完全相同。在调用
Copy-string时,将数组 a的首地址传给 from,把数组
B的首地址传给 to。在函数 Copy-string中的 for循环中,每次将 *from赋给 *to,第 1次就是将 a数组中第 1
个字符赋给 B数组的第 1个字符。在执行 from++和
to++以后,
from和 to就分别指向 a[1]和 b[1]。再执行 *to=*from,
就将 a[1]赋给 B[1]…… 最后将‘ \0?赋给 *to,注意此时 to指向哪个单元。
(3) 对 Copy-string函数还可作简化。
① 将 Copy-string函数改写为
void Copy-string(char *from,char *to)
{ while((*to=*from)! =′\0′)
{ to++;from++;}

请与上面一个程序对比。在本程序中将,*to=*from”
的操作放在 while语句的表达式中,而且把赋值运算和判断是否为‘ \0?的运算放在一个表达式中,先赋值后判断。在循环体中使 to和 from增值,指向下一个元素 …… 直到 *from的值为‘ \0?为止。
② Copy-string函数的函数体还可改为

while((*to++=*from++)! =′\0′);

把上面程序的 to++和 from++运算与 *to=*from合并,
它的执行过程是,先将 *from赋给 *to,然后使 to
和 from增值。显然这又简化了。
③ 函数体还可写成

while(*from! =′\0′)
*to++=*from++;
to=′\0′;

当 *from不为‘ \0?时,将 *from赋给 *to,然后使 to和
from增值。
字符可以用其 ASCII码来代替。例如,,Ch=?a?”可以用,Ch=97”代替,,while(Ch! =?a?)”可以用
,while(Ch! =97)”代替。因此,,while(*from!
=?\0?)”可以用,while(*from! =0)”代替 (?\0?的
ASCII代码为 0)。而关系表达式,*from! =0”又可简化为,*from”,这是因为若 *form的值不等于
0,则表达式,*from”为真,同时,*from! =0”
也为真。因此,while(*from! =0)”和
,while(*from)”是等价的。所以函数体可简化为
{ while(*from)
*to++=*from++;
*to=′\0′;

④ 上面的 while语句还可以进一步简化为下面的
while语句,while(*to++=*from++);它与下面语句等价:
while((*to++=*from++)! =′\0′);
将 *from赋给 *to,如果赋值后的 *to值等于‘ \0?,则循环终止 (?\0?已赋给 *to)。
⑤ 函数体中 while语句也可以改用 for语句:
for(;(*to++=*from++)! =0;);

for(;*to++=*from++;);
⑥ 也可用指针变量,函数 Copy-string可写为
void Copy-string(char from[
],char to[ ])
{ char p1,*p2;
p1=from;p2=to;
while((*p2++=*p1++)! =′\0′);

以上各种用法,变化多端,使用十分灵活,初看起来不太习惯,含义不直观。初学者会有些困难,
也容易出错。但对 C熟练之后,以上形式的使用是比较多的,读者应逐渐熟悉它,掌握它。
10.4.3 对使用字符指针变量和字符数组的讨论虽然用字符数组和字符指针变量都能实现字符串的存储和运算,但它们二者之间是有区别的,不应混为一谈,主要有以下几点:
(1) 字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址 (字符串的首地址 ),决不是将字符串放到字符指针变量中。
(2) 赋值方式。对字符数组只能对各个元素赋值,不能用以下办法对字符数组赋值。
char str[14];
str="I love China! ";
而对字符指针变量,可以采用下面方法赋值:
char a;
a="I love China! ";
但注意赋给 a的不是字符,而是字符串的首地址。
(3) 对字符指针变量赋初值:
char a="I love China! ";
等价于
char a;
a="I love China! ";
而对数组的初始化:
char str[14]={ "I love China! "} ;
不能等价于
char str[14];
str[]="I love China! ";
即数组可以在变量定义时整体赋初值,但不能在赋值语句中整体赋值。
(4) 如果定义了一个字符数组,在编译时为它分配内存单元,它有确定的地址。而定义一个字符指针变量时,给指针变量分配内存单元,在其中可以放一个地址值,也就是说,该指针变量可以指向一个字符型数据,但如果未对它赋予一个地址值,
则它并未具体指向一个确定的字符数据。如
char str[10];
scanf("%S",str);
是可以的。而常有人用下面的方法:
char a;
scanf("%S",a);
目的是输入一个字符串,虽然一般也能运行,但这种方法是危险的,不宜提倡。因为编译时虽然给指针变量 a分配了内存单元,a的地址 (即 &a)是已指定了,
但 a的值并未指定,在 a单元中是一个不可预料的值。
在执行 scanf函数时要求将一个字符串输入到 a所指向的一段内存单元 (即以 a的值 (地址 )开始的一段内存单元 )中。而 a的值如今却是不可预料的,它可能指向内存中空白的 (未用的 )用户存储区中 (这是好的情况 ),也有可能指向已存放指令或数据的有用内存段,这就会破坏了程序,甚至破坏了系统,会造成严重的后果。在程序规模小时,由于空白地带多,往往可以正常运行,而程序规模大时,出现上述“冲突”的可能性就大多了。应当这样:
char a,str[10];
a=str;
scanf("%S",a);
先使 a有确定值,也就是使 a指向一个数组的开头,
然后输入一个字符串,把它存放在以该地址开始的若干单元中。
(5) 指针变量的值是可以改变的,如:
例 10.21
main()
{ char a="I love China! ";
a=a+7;
printf("%S",a);

运行结果如下:
China!
指针变量 a的值可以变化,输出字符串时从 a当时所指向的单元开始输出各个字符,直到遇‘ \0?为止。
而数组名虽然代表地址,但它的值是不能改变的。
下面是错的:
char str[]={ "I loveChina! "} ;
str=str+7;
printf("%s",str);
需要说明,若定义了一个指针变量,并使它指向一个字符串,就可以用下标形式引用指针变量所指的字符串中的字符。如:
例 10.22
main()
{ char a="I love China!";
int i;
printf("The Sixth character is %C\n",a[5]);
for(i=0;a[i]! =′\0′;i++)
printf("%C",a[i]);

运行结果如下:
The sixth character is e
I love China!
程序中虽然并未定义数组 a,但字符串在内存中是以字符数组形式存放的。 a[5]按 *(a+5)执行,即从 a当前所指向的元素下移 5个元素位置,取出其单元中的值。
(6) 用指针变量指向一个格式字符串,可以用它代替
printf函数中的格式字符串。如:
char format;
format="a=%D,B=%f\n";
printf(format,a,B);
它相当于
printf("a=%D,B=%f\n",a,B);
因此只要改变指针变量 format所指向的字符串,就可以改变输入输出的格式。这种 printf函数称为可变格式输出函数。
也可以用字符数组实现。如:
char format[]="a=%D,B=%f\n";
printf(format,a,B);
但由于不能采用赋值语句对数组整体赋值,如:
char format[];
format="a=%D,B=%D\n";
因此用指针变量指向字符串的方式更为方便。
10.5.1 用函数指针变量调用函数可以用指针变量指向整型变量、字符串、数组,也可以指向一个函数。一个函数在编译时被分配给一个入口地址。这个入口地址就称为函数的指针。
可以用一个指针变量指向函数,然后通过该指针变量调用此函数。先通过一个简单的例子来回顾一下函数的调用情况。
例 10.23求 a和 b中的大者。先列出按一般方法的程序。
10.5 函数的指针和指向函数的指针变量
main()
{ int max(int,int);
int a,b,c;
scanf("%d,%d",&a,&b);
c=max(a,b);
printf("a=%d,b=%d,max=%d",a,b,
c);

max(int x,int y)
{ int z;
if(x> y)z=x;
else z=y;
return(z);

main函数中的,C=max(a,B);”包括了一次函数调用 (调用 max函数 )。每一个函数都占用一段内存单元,它们有一个起始地址。因此,可以用一个指针变量指向一个函数,通过指针变量来访问它指向的函数。
将 main函数改写为
main()
{ int max(int,int);
int(*p)();
int a,b,C;
p=max;
scanf("%D,%D",&a,&B);
C=(*p)(a,b);
printf("a=%d,b=%d,max=%d",a,b,
c);

其中 int(*p)()定义 p是一个指向函数的指针变量,此函数带回整型的返回值。注意 *p两侧的括弧不可省略,表示 p先与 *结合,是指针变量,然后再与后面的 ()结合,表示此指针变量指向函数,这个函数值 (即函数返回的值 )是整型的。如果写成,int
p()”,则由于 ()优先级高于 *,它就成了声明一个函数了 (这个函数的返回值是指向整型变量的指针 )。
赋值语句,p=max;”的作用是将函数 max的入口地址赋给指针变量 p。和数组名代表数组起始地址一样,函数名代表该函数的入口地址。这时,p就是指向函数 max的指针变量,也就是 p和 max都指向函数的开头,见图 10.36。调用 *p就是调用函数
max。请注意 p是指向函数的指针变量,它只能指向函数的入口处而不可能指向函数中间的某一条指令处,因此不能用 *(p+1)来表示函数的下一条指令。
图 10.36
在 main函数中有一个赋值语句
c=(*p)(a,b);
它包括函数的调用,和,C=max(a,B);”等价。这就是用指针形式实现函数的调用。以上用两种方法实现函数的调用,结果是一样的。
说明:
(1) 指向函数的指针变量的一般定义形式为数据类型
(*指针变量名 )();这里的“数据类型”是指函数返回值的类型。
(2) 函数的调用可以通过函数名调用,也可以通过函数指针调用 (即用指向函数的指针变量调用 )。
(3) (*p)()表示定义一个指向函数的指针变量,它不是固定指向哪一个函数的,而只是表示定义了这样一个类型的变量,它是专门用来存放函数的入口地址的。在程序中把哪一个函数的地址赋给它,它就指向哪一个函数。在一个程序中,一个指针变量可以先后指向不同的函数。
(4) 在给函数指针变量赋值时,只需给出函数名而不必给出参数,如,p=max;因为是将函数入口地址赋给 p,而不牵涉到实参与形参的结合问题。不能写成,p=max(a,B);”形式。
(5) 用函数指针变量调用函数时,只需将 (*p)代替函数名即可 (p为指针变量名 ),在 (*p)之后的括弧中根据需要写上实参。如下面语句表示:“调用由
p指向的函数,实参为 a,B。得到的函数值赋给
C。” C=(*p)(a,B);注意函数的返回值是什么类型?从上例对指针变量 p的定义可以知道,函数的返回值是整型的,因此将其值赋给整型变量 C是合法的。
(6) 对指向函数的指针变量,像 p+n,p++,p--等运算是无意义的。
10.5.2 用指向函数的指针作函数参数函数指针变量常用的用途之一是把指针作为参数传递到其他函数。这个问题是 C语言应用的一个比较深入的部分,在本书中只作简单的介绍,以便在今后用到时不致感到困惑。进一步的理解和掌握有待于读者今后深入的学习和提高。
以前介绍过,函数的参数可以是变量、指向变量的指针变量、数组名、指向数组的指针变量等。现在介绍指向函数的指针也可以作为参数,以便实现函数地址的传递,也就是将函数名传给形参。
下一章还要介绍用指向结构体的指针作函数参数。
它的原理可以简述如下:有一个函数 (假设函数名为 Sub),
它有两个形参 (x1和 x2),定义 x1和 x2为指向函数的指针变量。在调用函数 Sub时,实参用两个函数名 f1和 f2给形参传递函数地址。这样在函数 SuB中就可以调用 f1和 f2函数了。如:
实参函数名 f1 f2
↓ ↓
Sub(int (*x1)(int ),int (*x2)(int,int ))/*定义 x1,x2为函数指针变量,x1指向的 */
/*函数有一个整型形参,x2指向的函数有两个整型形参 */
{ int a,B,i=1,j=2;
a=(*x1)(i); /*调用 f1函数 */
B=(*x2)(i,j); /*调用 f2函数 */


其中 i和 j是函数 f1和 f2所要求的参数。函数 Sub的形参 x1,x2(指针变量 )在函数 Sub未被调用时并不占内存单元,也不指向任何函数。在 Sub被调用时,
把实参函数 f1和 f2的入口地址传给形参指针变量 x1
和 x2,使 x1和 x2指向函数 f1和 f2,见图 10.37。这时,在函数 Sub中,用 *x1和 *x2就可以调用函数 f1
和 f2。
(*x1)(i)就相当于 f1(i),(*x2)(i,j)就相当于 f2(i,j)。
图 10.37
有人可能会问,既然在 Sub函数中要调用 f1和 f2函数,
为什么不直接调用 f1和 f2而要用函数指针变量呢?
何必绕这样一个圈子呢?的确,如果只是用到 f1
和 f2,完全可以直接在 Sub函数中直接调用 f1和 f2,
而不必设指针变量 x1,x2。但是,如果在每次调用 Sub函数时,要调用的函数不是固定的,这次调用 f1和 f2,而下次要调用 f3和 f4,第三次要调用的是 f5和 f6。这时,用指针变量就比较方便了。只要在每次调用 Sub函数时给出不同的函数名作为实参即可,Sub函数不必作任何修改。这种方法是符合结构化程序设计方法原则的,是程序设计中常使用的。
下面通过一个简单的例子来说明这种方法的应用。
例 10.24设一个函数 process,在调用它的时候,每次实现不同的功能。输入 a和 B两个数,第一次调用
process时找出 a和 b中大者,第二次找出其中小者,
第三次求 a与 b之和。
程序如下:
main()
{ int max(int,int); /* 函数声明 */
int min(int,int); /* 函数声明 */
int add(int,int); /* 函数声明 */
int a,b;
printf("enter a and b,");
scanf("%d,%d",&a,&b);
printf("max=");
process(a,b,max);
printf("min=");
process(a,b,min);
printf("sum=");
process(a,b,add);

max(int x,int y) /* 函数定义 */
{ int Z;
if(x> y)Z=x;
else Z=y;
return(Z);

min(int x,int y) /* 函数定义 */
{ int Z;
if(x< y)Z=x;
else Z=y;
return(Z);

add(int x,int y) /* 函数定义 */
{ int Z;
Z=x+y;
return(Z);

process(int x,int y,int (*fun)(int,int)) /* 函数定义。
int (*fun)(int,int) 表示 fun是指向函数的指针,该函数是一个整型函数,有两个整型形参 */
{ int reSult;
result=(*fun)(x,y);
printf("%D\n",result);

运行情况如下:
enter a and B,2,6
max=6
min=2
sum=8
max,min和 add是已定义的 3个函数,分别用来实现求大数、求小数和求和的功能。在 main函数中第一次调用 process函数时,除了将 a和 B作为实参将两个数传给 process的形参 x,y外,还将函数名 max
作为实参将其入口地址传送给 process函数中的形参 fun(fun是指向函数的指针变量 ),见图 10.38(a)。
这时,process函数中的 (*fun)(x,y)相当于 max(x,
y),
执行 process可以输出 a和 B中大者。在 main函数第二次调用时,改以函数名 min作实参,此时
process函数的形参 fun指向函数 min,见图
10.38(B),在 process函数中的函数调用 (*fun)(x,y)
相当于 min(x,y)。同理,第三次调用 process函数时,情况见图 10.38(c)。 (*fun)(x,y)相当于 add(x,
y)。
图 10.38
从本例可以清楚地看到,不论调用 max,min或 add,
函数 process一点都没有改动,只是在调用 process函数时将实参函数名改变而已。这就增加了函数使用的灵活性。可以编一个通用的函数来实现各种专用的功能。需要注意的是,对作为实参的函数,应在主调函数中用函数原型作函数声明。例如,main函数中第 2行到第 4行的函数声明是不可少的。有的读者可能会问,过去不是曾说过,对本文件中的整型函数可以不加说明就可以调用吗?是的,但那只是限于函数调用的情况,函数调用时在函数名后面跟括弧和实参 (如 max(a,b)),编译时能根据此形式判断它为函数。而现在是只用函数名 (如 max)作实参,
后面没有括弧和参数,编译系统无法判断它是变量名还是函数名。故应事先作声明,声明 max、
min,add是函数名 (不是变量名 ),这样编译时将它们按函数名处理 (把函数入口地址作实参值 ),不致出错。
有了以上基础,就可以编写出较为复杂的程序。例如,编写一个求定积分的通用函数,用它分别求以下 5个函数的定积分,
可以看出,每次需要求定积分的函数是不一样的。
可以编写一个求定积分的通用函数 integral,它有
3个形参:下限 a、上限 B以及指向函数的指针变量
fun。函数原型可写为
float integral (float a,float b,float (*fun)(float));
分别定义 5个 C函数 f1,f2,f3,f4,f5,用来求上面 5个函数 1+x,2x+3,ex+1,(1+x)2,x3 。然后先后调用 integral函数 5次,每次调用时把 a,b以及 f1,f2,f3,f4,f5之一作为实参,把上限、下限以及有关函数的入口地址传送给形参 fun。在执行 inteGral函数过程中求出定积分的值。请读者根据以上思路,编写出完整的程序。
10.6 返回指针值的函数一个函数可以带回一个整型值、字符值、实型值等,
也可以带回指针型的数据,即地址。其概念与以前类似,只是带回的值的类型是指针类型而已。
这种带回指针值的函数,一般定义形式为类型名 *函数名 (参数表 );
例如:
int *a(int x,int y);
a是函数名,调用它以后能得到一个指向整型数据的指针 (地址 )。 x,y是函数 a的形参,为整型。请注意在 *a两侧没有括弧,在 a的两侧分别为 *运算符和 ()运算符。而 ()优先级高于 *,因此 a先与 ()结合。显然这是函数形式。这个函数前面有一个 *,
表示此函数是指针型函数 (函数值是指针 )。最前面的 int表示返回的指针指向整型变量。对初学 C语言的人来说,这种定义形式可能不大习惯,容易弄错,用时要十分小心。
例 10.25有若干个学生的成绩 (每个学生有 4门课程 ),
要求在用户输入学生序号以后,能输出该学生的全部成绩。用指针函数来实现。
程序如下:
main()
{ float score[ ][4]={{60,70,80,90},{56,
89,67,88},{34,78,90,66}};
float search(float (*pointer)[4],int n);
float p;
int i,m;
printf("enter the number of Student,");
scanf("%D",&m);
printf("The Scores of No.%D are,\n",m);
p=Search(Score,m);
for(i=0;i< 4;i++)
printf("%5.2f\t" (p+i));

float *Search(float (*pointer)[4],int n)
{ float *pt;
pt=*(pointer+n);
return(pt);

运行情况如下:
enter the number of student,1
The scores of No.1 are:
56.0089.0067.0088.00
注意:学生序号是从 0号算起的。函数 search被定义为指针型函数,它的形参 pointer是指向包含 4个元素的一维数组的指针变量。 pointer+1指向 score数组第 1行。见图 10.39。 *(pointer+1)指向第 1行第 0
列元素。也就是上一节介绍的指针从行控制转化为列控制了。 pt是指针变量,它指向敌捅淞?而不是指向一维数组 )。 main函数调用 search函数,将
score数组的首地址传给 pointer(注意 score也是指向行的指针,而不是指向列元素的指针 )。 m是要查找的学生序号。调用 search函数后,得到一个地址 (指向第 m个学生第 0门课程 ),赋给 p。然后将此学生的 4门课的成绩打印出来。 *(p+i)表示此学生第 i门课的成绩。
图 10.39
请注意,指针变量 p,pt和 pointer的区别。如果将
search函数中的语句
pt=*(pointer+n);
改为
pt=(*pointer+n);
运行结果为:
enter the number of student,1
The scores of No.1 are:
70.0080.0090.0056.00
得到的不是第一个学生的成绩,而是二维数组中
a[0][1]开始的 4个元素的值。为什么?请读者分析。
例 10.26对上例中的学生,找出其中有不及格课程的学生及其学生号。
程序如下:
main()
{ float score[ ][4]={{60,70,80,90},{50,
89,67,88},{34,78,90,66}};
float search(float (*pointer)[4]);
float p;
int i,j;
for(i=0;i< 3;i++)
{ p=search(score+i);
if(p==*(score+i))
{ printf("No.%D scores,",i);
for(j=0;j< 4;j++)
printf("%5.2f ",*(p+j));
printf("\n");}


float search(float (*pointer)[4])
{ int i;
float pt;
pt=*(pointer+1);
for(i=0;i< 4;i++)
if(*(*pointer+i)< 60) {pt=*pointer;Break;}
return(pt);

程序运行结果为
No.1 scores,50.00 89.00 67.00 88.00
No.2 scores,34.00 78.00 90.00 66.00
函数 search的作用是检查一个学生有无不及格的课程。在 search函数中 pointer是指针变量,指向一维数组 (有 4个元素 )。 pt为指向实型变量的指针变量。从实参传给形参 pointer的是 score+i,它是
score第 i行的首地址,见图 10.40(a)。
图 10.40
在 search函数中,先使 pt=*(pointer+1),即把 pt指向本行之末尾,见图 10.40(B)。这是为了区分有无不及格课程的标志。若经检查 4门课中有不及格的,就使 pt指向本行第 0列元素。若无不及格则保持 pt指向本行末尾 (亦即下一行第 0
列元素 )。将 pt返回 main函数。在 main函数中,把调用
search得到的函数值 (指针变量 pt的值 ),赋给 p。用 if语句判断 p是否等于 *(score+i),若相等,表示所查的学生有不及格课程 (p的值为 *(score+i),即 p指向第 i行的第 0列元素 )。
若无不及格,p的值是 *(score+i+1),因为在函数 search中已使它指向本行的末尾,也就是下一行开头。如果 p等于
*(score+i),就输出该学生 (有不及格课程的学生 )4门课成绩。
请读者仔细消化本例中指针变量的含义和用法。
10.7.1 指针数组的概念一个数组,其元素均为指针类型数据,称为指针数组,也就是说,指针数组中的每一个元素都相当于一个指针变量。一维指针数组的定义形式为类
[数组长度 ]
例如,int* p[4]由于 []比 *优先级高,因此 p先与 [4]
结合,形成 p[4]形式,这显然是数组形式,它有 4
个元素。然后再与 p前面的,*”结合,,*”表示此数组是指针类型的,每个数组元素 (相当于一个指针变量 )都可指向一个整型变量。
10.7 指针数组和指向指针的指针注意:不要写成 int(*p)[4],这是指向一维数组的指针变量。这在前面已介绍过了。为什么要用到指针数组呢?它比较适合于用来指向若干个字符串,使字符串处理更加方便灵活。
例如,图书馆有若干本书,想把书名放在一个数组中 (图 10.41(a)),然后要对这些书目进行排序和查询。按一般方法,字符串本身就是一个字符数组。
因此要设计一个二维的字符数组才能存放多个字符串。但在定义二维数组时,需要指定列数,也就是说二维数组中每一行中包含的元素个数 (即列数 )相等。而实际上各字符串 (书名 )长度一般是不相等的。如按最长的字符串来定义列数,则会浪费许多内存单元。见图 10.41(B)。
可以分别定义一些字符串,然后用指针数组中的元素分别指向各字符串,见图 10.41(C)。如果想对字符串排序,不必改动字符串的位置,只需改动指针数组中各元素的指向 (即改变各元素的值,这些值是各字符串的首地址 )。这样,各字符串的长度可以不同,而且移动指针变量的值 (地址 )要比移动字符串所花的时间少得多。
图 10.41
例 10.27将若干字符串按字母顺序 (由小到大 )输出。
main()
{ void Sort(char *name[ ],int n);
void print(char *name[ ],int n);
char *name[]={"Follow me","BASIC",
"Great Wall","FORTRAN","Computer
Design"};
int n=5;
Sort(name,n);
print(name,n);

void Sort(char *name[ ],int n)
{ char temp;
int i,j,k;
for(i=0;i< n-1;i++)
{ k=i;
for(j=i+1;j< n;j++)
if(strcmp(name[k],name[j])> 0) k=j;
if(k!=i)
{ temp=name[i]; name[i]=name[k];
name[k]=temp;}


void print(char *name[ ],int n)
{ int i;
for(i=0;i< n;i++)
printf("%S\n",name[i]);
}
运行结果为:
BASIC
Computer Design
FORTRAN
Follow me
Great W all
在 main函数中定义指针数组 name。它有 5个元素,
其初值分别是,Follow me”、,BASIC”、
,Great W all”、,FORTRAN”,,Computer
Design”的首地址。见图 10.41(C)。这些字符串是不等长的 (并不是按同一长度定义的 )。 Sort函数的作用是对字符串排序。 Sort函数的形参 name也是指针数组名,接受实参传过来的 name数组的首地址,因此形参 name数组和实参 name数组指的是同一数组。用选择法对字符串排序。 strcmp是字符串比较函数,name[k]和 name[j]是第 k个和第 j个字符串的起始地址。 strcmp(name[k],name[j])的值为:如果 name[k]所指的字符串大于 name[j]所指的字符串,则此函数值为正值 ;若相等,则函数值为 0;若小于,
则函数值为负值。 if语句的作用是将两个串中
“小”的那个串的序号 (i或 j之一 )保留在变量 k中。
当执行完内循环 for语句后,从第 i个串到第 n个串这么多字符串中,第 k个串最“小”。若 k≠i就表示最小的串不是第 i串。故将 name[i]和 name[k]对换,也就是将指向第 i个串的数组元素 (是指针型元素 )与指向第 k个串的数组元素对换。执行完 Sort函数后指针数组的情况如图 10.42所示。
图 10.42
print函数的作用是输出各字符串。 name[0]到
name[4]分别是各字符串 (按从小到大顺序排好序的各字符串 )的首地址 (按字符串从小到大顺序,
name[0]指向最小的串 ),用 "%s"格式符输出,就得到这些字符串。
print函数也可改写为以下形式:
void print(char *name[ ],int n)
{ int i=0;
char p;
p=name[0];
while(i< n)
{ p=*(name+i++);
printf("%S\n",p);}

其中,*(name+i++)”表示先求 *(name+i)的值,即
name[i](它是一个地址 ),然后使 i加 1。在输出时,
按字符串形式输出以 p地址开始的字符串。
请注意 Sort函数中的第一个 if语句中的逻辑表达式的正确用法。如果写成下面形式是不对的:
if(*name[k]> *name[j])k=j;
这样只比较 name[k]和 name[j]所指向的字符串中的第一个字符,字符串比较应当用 strcmp函数。
10.7.2 指向指针的指针在掌握了指针数组的概念的基础上,下面介绍指向指针数据的指针变量,简称为指向指针的指针。
从图 10.43可以看到,name是一个指针数组,它的每一个元素是一个指针型数据,其值为地址。
name是一个数组,它的每一元素都有相应的地址。
数组名 name代表该指针数组的首地址。 name+i是
name[i]的地址。 name+i就是指向指针型数据的指针 (地址 )。还可以设置一个指针变量 p,它指向指针数组的元素 (见图 10.44)。 p就是指向指针型数据的指针变量。
图 10.43
图 10.44
怎样定义一个指向指针数据的指针变量呢?如下:
char**p;
p的前面有两个 *号。 *运算符的结合性是从右到左,
因此 **p相当于 *(*p),显然 *p是指针变量的定义形式。如果没有最前面的 *,那就是定义了一个指向字符数据的指针变量。现在它前面又有一个 *号,
表示指针变量 p是指向一个字符指针变量 (即指向字符型数据的指针变量 )的。 *p就是 p所指向的另一个指针变量,如果有
p=name+2;
printf("%o\n",*p);
printf("%S\n",*p);
第一个 printf函数语句输出 name[2]的值 (它是一个地址 ),第二个 printf函数语句以字符串形式 (%S)输出字符串,Great
W all”。例 10.28使用指向指针的指针。
main()
{ char *name[]={“Follow me”,,BASIC”,,Great
Wall”,,FORTRAN”,,Computer Design"};
char *p;
int i;
for(i=0;i< 5;i++)
{ p=name+i;
printf(“%s\n",*p);


运行结果如下:
Follow me
BASIC
FORTRAN
Great W all
Computer Design
p是指向指针的指针变量,在第一次执行循环体时,赋值语句,p=name+i;” 使 p指向 name数组的
0号元素 name[0],*p是 name[0] 的值,即第一个字符串的起始地址,用 printf函数输出第一个字符串 (格式符为 %S)。依次输出 5个字符串。
指针数组的元素也可以不指向字符串,而指向整型数据或实型数据等,例如:
int a[5]={ 1,3,5,7,9,i} ;
int *num[5]
int *p;
for (i=0; i<5; i++)
num[i]=&a[i];
此时为了得到 a[2]中的数据,5”,可以先使
p=num+2,然后输出 **p。注意 *p是 p间接指向的对象的地址 num[2]。而 **p是 p间接指向的对象的值,即 *num[2],也就是 a[2]的值 5。见图 10.44。
例 10.29是一个指针数组的元素指向整型数据的简单例子。目的是为了说明它的用法。
main()
{ static int a[5]={ 1,3,5,7,9} ;
int *num[5]={ &a[0],&a[1],&a[2],
&a[3],&a[4]} ;
int p,i;
p=num;
for(i=0;i< 5;i++)
{ printf("%D\t",**p);p++;}

运行结果为:
13 5 7 9
请不要把第 2,3行错写为
int num[5]={ 1,3,5,7,9} ;
指针数组的元素只能存放地址。读者可在此例基础上实现对各数排序。在本章开头已经提到了“间接访问”
变量的方式。利用指针变量访问另一个变量就是“间接访问”。如果在一个指针变量中存放一个目标变量的地址,这就是“单级间址”,见图 10.45(a)。指向指针的指针用的是“二级间址”方法。见图 10.45(B)。
从理论上说,间址方法可以延伸到更多的级,见图
10.45(C)。但实际上在程序中很少有超过二级间址的。
级数愈多,愈难理解,容易产生混乱,出错机会也多。
图 10.45
10.7.3 指针数组作 main函数的形参指针数组的一个重要应用是作为 main函数的形参。
在以往的程序中,main函数的第一行一般写成以下形式:
main()
括弧中是空的。实际上,main函数可以有参数,例如:
main(argc,argv)
argc和 argv就是 main函数的形参。 main函数是由系统调用的。当处于操作命令状态下,输入 main所在的文件名 (经过编译、连接后得到的可执行文件名 ),系统就调用 main函数。那么,main函数的形参的值从何处得到呢?显然不可能在程序中得到。实际上实参是和命令一起给出的。也就是在一个命令行中包括命令名和需要传给 main函数的参数。命令行的一般形式为命令名参数 1参数
2…… 参数 n命令名和各参数之间用空格分隔。参数 1是 main所在的文件名,假设为 file1,今想将两个字符串,China”,,Beijing”作为传送给 main
函数的参数。参数可以写成以下形式:
file1 China Beijing
实际上,文件名应包括盘符、路径以及文件的扩展名,今为简化起见,用 file1来代替。
请注意以上参数与 main函数中形参的关系。
main函数中形参 argc是指命令行中参数的个数 (注意,
文件名也作为一个参数。例如,本例中,file1” 也是一个参数 ),现在,argc的值等于 3(有 3个命令行参数:
file1,China,Beijing)。 main函数的第二个形参 argv
是一个指向字符串的指针数组,也就是说,带参数的
main函数原型是 main(int argc,char *argv[ ]);命令行参数应当都是字符串 (例如,上面命令行中的,file1”、
,China”、,Beijing”都是字符串 ),这些字符串的首地址构成一个指针数组,见图 10.46。指针数组 argv中的元素 argv[0]指向字符串,file1”(或者说 argv[0]的值是字符串,file1”的首地址 ),argv[1]指向字符串
,China”,argv[2]指向字符串,Beijing”。
如果有以下一个 main函数,它所在的文件名为 file1:
图 10.46
main(int argc,char *argv[ ])
{ while(argc> 1)
{ ++argv;
printf("%s\n" argv);
--argc;


输入的命令行参数为
file1 China Beijing
则执行以上命令行将会输出以下信息:
China
Beijing
上面程序可以改写为
main(int argc,char *argv[ ])
{ while(argc--> 1)
printf("%S\n",*++argv);

其中 *++argv是先进行 ++argv的运算,使 argv指向下一个元素,然后进行 *的运算,找到 argv当前指向的字符串,输出该字符串。在开始时,argv指向字符串,file1”,++argv使之指向,China”,所以第一次输出的是,China”,第二次输出
,Beijing”。
许多系统提供了 echo命令,它的作用是实现“参数回送”,即将 echo后面的各参数 (各字符串 )在同一行上输出。它的 C程序如下:
C> type echo.c(这是一条 DOS命令,echo.c是一个 C
文件名 )
main(int argc,char *argv[ ])
{ while(--argc> 0)
printf("%S%C",*++argv,(argc>1)?′ ′:′\n′);

如果命令行输入:
C> echo Computer and C Language
在显示屏上输出:
Computer and C Language
这个程序与前面的差别在于:① 将 while语句中的 (argc--> 1)
改为 (--argc> 0),作用显然是一样的。② 当 argc> 1时,
在输出的两个字符串间输出一个空格,当 argc=1时输出一个换行。程序不输出命令名,echo”。
为便于理解,echo程序也可写成以下形式:
main(int argc,char *argv[ ])
{ int i;
for(i=1;i< argc;i++)
printf("%s%c",argv[i],(argc>1)?′ ′:′\n′);

main函数中的形参不一定命名为 argc和 argv,可以是任意的名字,只是人们习惯用 argc和 argv而已。
利用指针数组作 main函数的形参,可以向程序传送命令行参数 (这些参数是字符串 ),这些字符串的长度事先并不知道,而且各参数字符串的长度一般并不相同,命令行参数的数目也是可以任意的。
用指针数组能够较好地满足上述要求。
关于指向指针的指针,是 C语言中比较深入的概念,
在此只作简单的介绍,以为读者提供今后进一步学习的基础。
10.8 有关指针运算的小结
前面已经用到了有关指针的运算,为使读者有一系统完整的概念,现在作一小结。
10.8.1 指针运算小结
前面已用过一些指针运算 (如 p++,p+i等 ),今把全部的指针运算列出如下。
(1) 指针变量加 (减 )一个整数
例如,p++,p--,p+i,p-i,p+=i,p-=i等。
C语言规定,一个指针变量加 (减 )一个整数并不是简单地将指针变量的原值加 (减 )一个整数,而是将该指针变量的原值 (是一个地址 )和它指向的变量所占用的内存单元字节数相加 (减 )。如 p+i代表地址计算,p+C*i。 C为字节数,在大多数微机 C系统中,对整型数据 C=2,实型 C=4,字符型 C=1。这
(p+i)指向 p下面的第 i个元素,它才有实际意义。
(2) 指针变量赋值将一个变量地址赋给一个指针变量。如:
p=&a(将变量 a的地址赋给 p)
p=array; (将数组 array首地址赋给 p)
p=&array[i];(将数组 array第 i个元素的地址赋给 p)
p=max; (max为已定义的函数,将 max的入口地址赋给 p)
p1=p2; (p1和 p2都是指针变量,将 p2的值赋给 p1)
注意:不应把一个整数赋给指针变量。如:
p=1000;
有人以为这样可以将地址 1000赋给 p。但实际上是做不到的。只能将变量已分配的地址赋给指针变量。
同样也不应把指针变量 p的值 (地址 )赋给一个整型变量 i:
i=p;
(3) 指针变量可以有空值,即该指针变量不指向任何变量,可以这样表示,p=NULL;
实际上 NULL是整数 0,它使 p的存储单元中所有二进位均为 0,也就是使 p指向地址为 0的单元。系统保证使该单元不作它用 (不存放有效数据 ),即有效数据的指针不指向 0单元。实际上是先定义 NULL,即
#define NULL 0

p=NULL;
在 Studio.h头文件中就有以上的 NULL定义,它是一个符号常量。
用,p=NULL;”表示 p不指向任一有用单元。应注意,p
的值为 NULL与未对 p赋值是两个不同的概念。前者是有值的 (值为 0),不指向任何变量,后者虽未对 p赋值但并不等于 p无值,只是它的值是一个无法预料的值,
也就是 p可能指向一个事先未指定的单元。这种情况是很危险的。因此,在引用指针变量之前应对它赋值。
任何指针变量或地址都可以与 NULL作相等或不相等的比较,如
if(p==NULL)…
(4) 两个指针变量可以相减如果两个指针变量指向同一个数组的元素,则两个指针变量值之差是两个指针之间的元素个数,
见图 10.47。
图 10.47
假如 p1指向 a[1],p2指向 a[4],则 p2-p1=4-1=3。
但 p1+p2并无实际意义。
(5) 两个指针变量比较若两个指针指向同一个数组的元素,则可以进行比较。指向前面的元素的指针变量“小于”指向后面元素的指针变量。如图 10.47中,p1< p2,或者说,表达式,p1< p
2”的值为 1(真 ),而,p2< p1”的值为 0(假 )。注意,
如果 p1和 p2不指向同一数组则比较无意义。
10.8.3 void指针类型
ANSI新标准增加了一种,void”指针类型,即可定义一个指针变量,但不指定它是指向哪一种类型数据的。 ANSI C标准规定用动态存储分配函数时返回 void指针,它可以用来指向一个抽象的类型的数据,在将它的值赋给另一指针变量时要进行强制类型转换使之适合于被赋值的变量的类型。
例如,
char p1;
void p2;

p1=(char *)p2;
同样可以用 (void *)p1将 p1的值转换成 void *类型。如:
p2=(void *)p1;
也可以将一个函数定义为 void *类型,如,
void fun(char Ch1,char Ch2)
表示函数 fun返回的是一个地址,它指向“空类型”,
如需要引用此地址,也需要根据情况对之进行类型转换,如对函数调用得到的地址要进行以下转换:
p1=(char )fun(Ch1,Ch2);
在本章中介绍了指针的基本概念和初步应用。应该说明,
指针是 C语言中重要的概念,是 C的一个特色。使用指针的优点:①提高程序效率 ;② 在调用函数时变量改变了的值能够为主调函数使用,即可以从函数调用得到多个可改变的值 ;③ 可以实现动态存储分配。
但是同时应该看到,指针使用实在太灵活,对熟练的程序人员来说,可以利用它编写出颇有特色的、
质量优良的程序,实现许多用其他高级语言难以实现的功能,但也十分容易出错,而且这种错误往往难以发现。由于指针运用的错误甚至会引起使整个程序遭受破坏,比如由于未对指针淞縫赋值就向 *p赋值,这就可能破坏了有用的单元的内容。有人说指针是有利有弊的工具,甚至说它
“好坏参半”。的确,如果使用指针不当,特别是赋予它一个错误的值时,会成为一个极其隐蔽的、难以发现和排除的故障。因此,使用指针要十分小心谨慎,要多上机调试程序,以弄清一些细节,并积累经验。
习题本章习题均要求用指针方法处理。
10.1 输入 3个整数,按由小到大的顺序输出。
10.2 输入 3个字符串,按由小到大的顺序输出。
10.3 输入 10个整数,将其中最小的数与第一个数对换,把最大的数与最后一个数对换。写 3个函数:
①输入 10个数 ;② 进行处理 ;③ 输出 10个数。
10.4 有 n个整数,使前面各数顺序向后移 m个位置,
最后 m个数变成最前面 m个数,见图 10.48。写一函数实现以上功能,在主函数中输入 n个整数和输出调整后的 n个数。
图 10.48
10.5 有一字符串,包含 n个字符。写一函数,将此字符串中从第 m个字符开始的全部字符复制成为另一个字符串。
10.6 输入一行文字,找出其中大写字母、小写字母、
空格、数字以及其他字符各有多少?
10.7 写一函数,将一个 3× 3的矩阵转置。
10.8 将一个 5× 5的矩阵中最大的元素放在中心,4
个角分别放 4个最小的元素 (顺序为从左到右,从上到下顺序依次从小到大存放 ),写一函数实现之。
用 main函数调用。
10.9 在主函数中输入 10个等长的字符串。用另一函数对它们排序。然后在主函数输出这 10个已排好序的字符串。
10.10 用指针数组处理上一题目,字符串不等长。
10.11 将 n个数按输入时顺序的逆序排列,用函数实现。
10.12 有一个班 4个学生,5门课。①求第一门课的平均分 ;② 找出有 2门以上课程不及格的学生,输出他们的学号和全部课程成绩和平均成绩 ;③ 找出平均成绩在 90分以上或全部课程成绩在 85分以上的学生。分别编 3个函数实现以上 3个要求。
10.13 输入一个字符串,内有数字和非数字字符,
如 a123x456 17960? 302taB5876将其中连续的数字作为一个整数,依次存放到一数组 a中。例如,
123放在 a[0],456放在 a[1]… 统计共有多少个整数,
并输出这些数。
10.14 写一函数,实现两个字符串的比较。即自己写一个 strcmp函数,函数原型为 int strcmp(char
*p1,char *p2);设 p1指向字符串 S1,p2指向字符串 S2。要求当 S1=S2时,返回值为 0,若 S1≠S2,
返回它们二者第一个不同字符的 ASCII码差值 (如
,BOY”与,BAD”,第二个字母不同,,O”与
,A”之差为 79-65=14)。如果 S1> S2,则输出正值,
如 S1< S2,则输出负值。
10.15 编一程序,打入月份号,输出该月的英文月名。例如,输入,3”,则输出,March”,要求用指针数组处理。
10.16 用指向指针的指针的方法对 5个字符串排序并输出。
10.17 用指向指针的指针的方法对 n个整数排序并输出。要求将排序单独写成一个函数。 5个整数和 n
在主函数中输入。最后在主函数中输出。