第八章:善于利用指针
8.1 指针是什么
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。打个比方,一个房间的门口挂了一个房间号2008,这个2008就是房间的地址,或者说,2008“指向”该房间。因此,将地址形象化地称为“指针”。意思是通过它能找到以它为地址的内存单元。
说明:对计算机存储单元的访问比旅馆要复杂一些,在C语言中,数据是分类型的,对不同类型的数据,在内存中分配的存储单元大小(字节数)和存储方式是不同的(如整数以补码形式存放,实数以指数形式存放)。如果只是指定了地址1010,希望从该单元中调出数据,这是做不到的,虽然能找到所指定的存储单元,但是,无法确定是从1个字节中取信息(字符数据),还是从2个字节取信息(短整型),抑或是从4个字节取信息(整型)。也没有说明按何种存储方式存取数据(整数和单精度实数都是4个字节,但存储方式是不同的)。因此,为了有效地存取一个数据,除了需要位置信息外,还需要有该数据的类型信息(如果没有该数据的类型信息,只有位置信息是无法对该数据进行存取的)。C语言中的地址包括位置信息(内存编号,或称纯地址)和它所指向的数据的类型信息,或者说它是“带类型的地址”。如&a,一般称它为“变量a的地址”,确切地说,它是“整型变量a的地址”。后面提到的“地址”,都是这个意思。
请务必弄清楚存储单元的地址和存储单元的内容这两个概念的区别,假设程序已定义了3个整型变量i,j,k,在程序编译时,系统可能分配地址为2000~ 2003的4个字节给变量i,2004~ 2007的4个字节给 j,2008~2011的4个字节给k(不同的编译系统在不同次的编译中,分配给变量的存储单元的地址是不相同的)见图8.1。在程序中一般是通过变量名来引用变量的值,例如:
printf("%d\n",i);
由于在编译时,系统已为变量i分配了按整型存储方式的4个字节,并建立了变量名和地址的对应表,因此在执行上面语句时,首先通过变量名找到相应的地址,从该4个字节中按照整型数据的存储方式读出整型变量i的值,然后按十进制整数格式输出。
假如有输入语句:
scanf("%d",&i);
在执行时,把键盘输入的值送到地址为2000开始的整型存储单元中。如果有语句
k=i十j;
则从2000~ 2003字节取出i的值(3),再从2004~2007字节取出j的值(6) ,将它们相加后再将其和(9)送到k所占用的2008~2011字节单元中。
这种直接按变量名进行的访问,称为“直接访问”方式。
还可以采用另一种称为“间接访问”的方式,即将变量i的地址存放在另一变量中,然后通过该变量来找到变量i的地址,从而访问i变量。
在C语言程序中,可以定义整型变量,浮点型(实型)变量、字符变量等,也可以定义一种特殊的变量,用它存放地址。假设定义了一个变量 i_pointer(变量名可任意取),用来存放整型变量的地址。可以通过下面语句将i的地址(2000)存放到i_pointer中。
i_pointer = &i; //将i的地址存放到i_pointer中
这时,i_pointcr的值就是2000(即变量i所占用单元的起始地址)。
图8.2(a)表示直接访问,根据变量名直接向变量;赋值,由于变是名与变量的地址有一对应的关系,因此就按此地址直接对变量i的存储单元进行访问(如把数值3存放到变量i的存储单元中)。
图8.2(b)表示间接访问,先找到存放变量i地址的变量i_pointer,从其中得到变量i的地址(2000),从而找到变量i的存储单元,然后对它进行存取访问。
为了表示将数值3送到变量中,可以有两种表达方法:
(1)将3直接送到变量i所标识的单元中,例如“i一3;”。
(2)将3送到变量i_pointer所指向的单元(即变量i的存储单元),例如“* i_pointer=3;
”,其中* i_pointer表示i_pointer指向的对象。
指向就是通过地址来体现的。假设i_pointer中的值是变量i的地址(2000),这样就在i_pointer和变量i之间建立起一种联系,即通过i_pointer能知道i的地址,从而找到变量i的内存单元。图8.2中以单箭头表示这种“指向”关系。
由于通过地址能找到所需的变量单元,因此说,地址指向该变量单元(如同说,一个房间号“指向”某一房间一样)。将地址形象化地称为“指针”。意思是通过它能找到以它为地址的内存单元(如同根据地址2000就能找到变量i的存储单元一样)。
如果有一个变量专门用来存放另一变量的地址(即指针),则它称为**“指针变量”**。上述的i_pointer就是一个指针变量。指针变量就是地址变量,用来存放地址,指针变量的值是地址(即指针)。
注意:区分“指针”和“指针变量”这两个概念。例如,可以说变量i的指针是2000,而不能说i的指针变量是2000。指针是一个地址,指针变量是存放地址的变量。
8.2 指针变量
8.2.1 例子
略
8.2.2 怎样定义指针变量
一般形式:
类型名 *指针变量名
如:
int *pointer_1, *pointer_2;
左端的int是在定义指针变量时必须指定的**“基类型”**。指针变量的基类型用来指定此指针变量可以指向的变量的类型。例如,上面定义的、基类型为int的指针变量 pointer_1和pointer_2,可以用来指向整型的变量i和j,但不能指向浮点型变量a和 b。
可以在定义指针变量时,同时对它初始化:
int *pointer_1=&a, *pointer_2=&b;
说明:在定义指针变量时要注意:
(1)指针变量前面的“*”表示该变量为指针型变量。指针变量名是 pointer_1 和pointer_2,而不是*pointer_l和*pointer_2。这是与定义整型或实型变量的形式不同的。上面程序第5,6行不应写成“* pointer_1=&-a;”和“* pointer_2=&.b;”。因为a的地址是赋给指针变量pointer_1,而不是赋给* pointer_1(即变量a)。
(2)在定义指针变量时必须指定基类型。一个变量的指针的含义包括两个方面,一是以存储单元编号表示的纯地址(如编号为2000的字节),一是它指向的存储单元的数据类型(如int,char ,float等)。在说明变量类型时不能一般地说“a是一个指针变量”,而应完整地说:“a是指向整型数据的指针变量,b是指向单精度型数据的指针变量,c是指向字符型数据的指针变量”。
(3)如何表示指针类型。指向整型数据的指针类型表示为“int*”,读作“指向int的指针"或简称“int 指针”。可以有int* ,char* , float*等指针类型,如上面定义的指针变量pointer_3的类型是“float* ” , pointer_4的类型是“char* ”。int* ,float*,char* 是3种不同的类型,不能混淆。
(4)指针变量中只能存放地址(指针),不要将一个整数赋给一个指针变量。如:
* pointer_l=100;// pointer_1是指针变量,100是整数,不合法
原意是想将地址100赋给指针变量pointer_l,但是系统无法辨别它是地址,从形式上看100是整常数,而整常数只能赋给整型变量,而不能赋给指针变量,判为非法。在程序中是不能用一个数值代表地址的,地址只能用地址符“&.”得到并赋给一个指针变量,如“p=&a;
”。
8.2.3 怎样引用指针变量
在引用指针变量时,可能有3种情况:
(1)给指针变量赋值。如:
p=&a; //把a的地址赋给指针变量p
指针变量p的值是变量a的地址,p指向a。
(2)引用指针变量指向的变量。
如果已执行“p=&a;”,即指针变量p指向了整型变量a,则printf("%d” * p);
。其作用是以整数形式输出指针变量p所指向的变量的值,即变量a的值。
如果有以下赋值语句:
*p=1;
表示将整数1赋给p当前所指向的变量,如果p指向变量a,则相当于把1赋给a,即“a=1;”。
(3)引用指针变量的值。如:printf("%o”,p);
作用是以八进制数形式输出指针变量p的值,如果p指向了a,就是输出了a的地址,即&a。
注意:要熟练掌握两个有关的运算符
(1)&取地址运算符。&.a是变量a的地址。
(2)* 指针运算符(或称“间接访问”运算符),* p代表指针变量p指向的对象。下面是一个指针变量应用的例子。
8.2.4 指针变量作为函数参数
函数的参数不仅可以是整型、浮点型、字符型等数据,还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。
下面通过一个例子来说明。
例:对输入的两个整数按大小顺序输出。现用函数处理,而且用指针类型的数据作函数参数。
#include<stdio.h>
int main(){
void swap(int * pl,int * p2);//对swap函数的声明
int a,b;
int * pointcr_l,* pointcr_2;//定义两个int *型的指针变量
printf("please enter a and b:");
scanf("%d,%d",&a,&b); //输入两个整数
pointer_l=&a; //使pointer_l指向a
pointer_2=&b; //使pointer_2指向b
if (a<b)swap(pointer_l,pointer__2);//如果a<b,调用swap函数
printf("max= %d, min=%d\n",a,b); //输出结果
return 0;
void swap(int *pl , int *p2)//定义swap函数
{int temp;
temp=*pl;//使*pl 和* p2互换
*pl=*p2;
*p2=temp;}
如果想通过函数调用得到n个要改变的值,可以这样做:
①在主调函数中设n个变量,用n个指针变量指向它们;
②设计一个函数,有n个指针形参。在这个函数中改变这n个形参的值;
③在主调函数中调用这个函数,在调用时将这n个指针变量作实参,将它们的值,也就是相关变量的地址传给该函数的形参;
④在执行该函数的过程中,通过形参指针变量,改变它们所指向的n个变量的值;
⑤主调函数中就可以使用这些改变了值的变量。
8.3 通过指针引用数组
8.3.1 数组元素的指针
所谓数组元素的指针就是数组元素的地址
引用数组元素可以用下标法(如 a[3]),也可以用指针法,即通过指向数组元素的指针找到所需的元素。使用指针法能使目标程序质量高(占内存少,运行速度快)。
在C语言中,数组名(不包括形参数组名)代表数组中首元素(即序号为0的元素)的地址。因此,下面两个语句等价:
p=&a[0]; //p的值是a[0]的地址
p=a; //p的值是数组a首元素(即a[0])的地址
==注意:程序中的数组名不代表整个数组,只代表数组首元素的地址。上述p=a;
的作用是“把 a数组的首元素的地址赋给指针变量p”,而不是“把数组a各元素的值赋给p”。
8.3.2 在引用数组元素时指针的运算
在指针已指向一个数组元素时,可以对指针进行以下运算:加一个整数(用+或+=),如 p+1;
减一个整数(用-或-=),如 p-1;
自加运算,如 p++,++p;
自减运算,如 p–,–p。
两个指针相减,如 p1-p2(只有p1和 p2都指向同一数组中的元素时才有意义)。分别说明如下:
(1)如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素,p-1指向同一数组中的上一个元素。注意:执行p+1时并不是将p的值(地址)简单地加1,而是加上一个数组元素所占用的字节数。例如,数组元素是float型,每个元素占4个字节,则p+1意味着使p的值(是地址)加4个字节,以使它指向下一元素。p+1所代表的地址实际上是p+1×d,d是一个数组元素所占的字节数(在Visual C++中,对int型,d=4;对float和 long 型,d=4;对char型,d=1)。若p的值是2000,则p+1的值不是2001,而是2004。
(2)如果p的初值为&a[0],则p+i和 a+i就是数组元素a[i]的地址,或者说,它们指向a数组序号为i的元素。这里需要注意的是a代表数组首元素的地址,a+1也是地址,它的计算方法同p+1,即它的实际地址为a+1×d。例如,p+9和a+9的值是&a[9],它指向a[9]。
(3) *(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,设数组为float型,则a[3]的地址是这样计算的:1000+3×4=1012,然后从1012地址所指向的float型单元取出元素的值,即 a[3]的值。
说明:[ ]实际上是变址运算符,即将a[i]按a+i计算地址。然后找出此地址单元中的值。
(4)如果指针变量p1和 p2都指向同一数组中的元素,如执行p2-p1,结果是p2-p1的值(两个地址之差)除以数组元素的长度。假设,p2指向实型数组元素a[5],p2的值为2020;p1指向a[3],其值为2012,则 p2-p1的结果是(2020-2012)/4=2。这个结果是有意义的,表示p2所指的元素与p1所指的元素之间差2个元素。这样,人们就不需要具体地知道p1和p2的值,然后去计算它们的相对位置,而是直接用p2-p1就可知道它们所指元素的相对距离。
注意:两个地址不能相加,如 p1+p2是无实际意义的。
8.3.3 通过指针引用数组元素
根据以上叙述,引用一个数组元素,可以用下面两种方法:
(1)下标法,如 a[i]
形式;
(2)指针法,如*(a+i)
或*(p+i)
。其中a是数组名,p是指向数组元素的指针变量,其初值p=a。
3种方法的比较:
- 下标法和指针法的第一种执行效率是相同的。C编译系统是将
a[i]
转换为*(a+i)
处理的,即先计算元素地址。因此这两种方法找数组元素费时较多。 - 指针法的第二种比上述两种方法快,用指针变量直接指向元素,不必每次都重新计算地址,像p++这样的自加操作是比较快的。这种有规律地改变地址值(p++)能大大提高执行效率。
- 用下标法比较直观,能直接知道是第几个元素。例如,
a[5]
是数组中序号为5的元素(注意序号从0算起)。用地址法或指针变量的方法不直观,难以很快地判断出当前处理的是哪一个元素。要仔细分析指针变量p的当前指向,才能判断当前输出的是第几个元素。有经验的专业人员往往喜欢用第(3)种形式,用p++进行控制,程序简洁、高效。初学者在开始时可用第(1)种形式,直观、不易出错。
8.3.4 用数组名作函数参数
实参用数组名a,形参可用数组名,也可用指针变量名。
8.3.5 通过指针引用多维数组
1.多维数组元素的地址
由于分配内存情况不同,所显示的地址可能是不同的。但是上面显示的地址是有共同规律的。
2.指向多维数组元素的指针变量
(1)指向数组元素的指针变量
例:有一个3×4的二维数组,要求用指向元素的指针变量输出二维数组各元素的值
#inlcude<stdio.h>
int main(){
int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
int *p; //p是int*型指针变量
for (p=a[0];p<<a[0]+12;p++){ //使p依次指向下一个元素
if((p-a[0])%4==0)printf("\n"); //p移动4次后换行
printf("%4d", *p);} //输出p指向的元素的值
printf("\n");
return 0;
(2)指向由m个元素组成的一维数组的指针变量
例:输出二维数组任一行任一列元素的值
#include<stdio.h>
int main(){
int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
int (*p)[4],i,j; //指针变量p指向包含4个整型元素的一维数组
p=a; //p指向二维数组的0行
printf("please enter row and colum:");
scanf("%d,%d",&i,&j); //输入要求输出的元素的行列号
printf("a[%d,%d]=%d\n",i,j,*(*(p+i)+j)); //输出a[i][j]的值
return 0;
注意:程序第4行中“int (*p)[4]”
表示定义p为一个指针变量,它指向包含4个整型元素的一维数组。注意,*p
两侧的括号不可缺少,如果写成* p[4]
,由于方括号[门运算级别高,因此p先a[4]结合,p[4]是定义数组的形式,然后再与前面的*
结合,* p[4]
就是指针数组(见8.7节)。
要注意指针变量的类型,从int (*p)[4];
可以看到,p的类型不是int *
型,而是int(*)[4]
型,p被定义为指向一维整型数组的指针变量,一维数组有4个元素,因此p的基类型是一维数组,其长度是16字节。*(p+2)+3
括号中的2是以p的基类型(一维整型数组)的长度为单位的,即 p每加1,地址就增加16个字节(4个元素,每个元素4个字节),而“*(p+2)+3
”括号外的数字3,不是以p的基类型的长度为单位的。由于经过*(p+2)
的运算,得到a[2]
,即&a[2][0]
,它已经转化为指向列元素的指针了,因此加3是以元素的长度为单位的,加3就是加(3×4)个字节。虽然p+2
和*(p+2)
具有相同的值,但由于它们所指向的对象的长度不同,因此(p+2)+3
和* (p+2)+3
的值就不相同了。这和上一节所叙述的概念是一致的。
3.用指向数组的指针作函数参数
一维数组名可以作为函数参数,多维数组名也可作函数参数。用指针变量作形参,以接受实参数组名传递来的地址。可以有两种方法:①用指向变量的指针变量;②用指向一维数组的指针变量。
例:有一个班,3个学生,各学4门课,计算总平均分数以及第n个学生的成绩。
#inlcude<stdio.h>
int 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的学生的成绩
return 0;}
void average(float *p, int n){
float *p_end;
float sum=0,aver;
p_end = p+n-1 //n的值为12时,p_end的值是p+11,指向最后一个元素
for(;p<=p_end;p++){
sum=sum +(*p);
aver=sum/n;
printf("average=%5.2f\n",aver);}
void search(float (*p)[4],int n){ //p是指向具有4个元素的一维数组的指针
int i;
printf("The score of No.%d are:\n",n);
for (i=0;i<4;i++){
printf("%5.2f ",*(*(p+n)+i));
printf("\n");}
例:在上述例子的基础上,查找有一门以上课程不及格的学生,输出他们的全部课程的成绩
#inlcude<stdio.h>
int main(){
void search(float (*p)[4], int n);
float score[3][4]={{65,67,70,60},{80,87,90,81},{90, 99,100,98}};
search(score,3); //求序号为2的学生的成绩
return 0;}
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;
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");}
}
}
}
8.4 通过指针引用字符串
在前面几章中已大量地使用了字符串,如在 printf函数中输出一个字符串。这些字符串都是以直接形式(字面形式)给出的,在一对双撇号中包含若干个合法的字符。在本节中将介绍使用字符串的更加灵活方便的方法——通过指针引用字符串。
8.4.1 字符串的引用方式
(1)用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明“%s”输出该字符串。
(2)用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
#include<stdio.h>
int main(){
char *string="I love China!"; //定义字符指针变量string并初始化
printf("%s\n",string); //输出字符串
return 0;
注意:string 被定义为一个指针变量,基类型为字符型。请注意它只能指向一个字符类型数据,而不能同时指向多个字符数据,更不是把"I love China!"这些字符存放到string 中(指针变量只能存放地址),也不是把字符串赋给*string。只是把"I love China !"的第1个字符的地址赋给指针变量string。
%s是输出字符串时所用的格式符,在输出项中给出字符指针变量名string,则系统会输出string所指向的字符串第1个字符,然后自动使string 加1,使之指向下一个字符,再输出该字符……如此直到遇到字符串结束标志’\0’为止。注意,在内存中,字符串的最后被自动加了一个\0’,因此在输出时能确定输出的字符到何时结束。可以看到,用%s可以对一个字符串进行整体的输入输出。
8.4.2 字符指针作函数参数
函数的形参和实参可以分别用字符数组名或字符指针变量。
8.4.3 使用字符指针变量和字符数组的比较
(1)字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第1个字符的地址),绝不是将字符串放到字符指针变量中。
(2)赋值方式。可以对字符指针变量赋值,但不能对数组名赋值。
(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)存储单元的内容。编译时为字符数组分配若干存储单元,以存放各元素的值,而对字符指针变量,只分配一个存储单元(Visual C++为指针变量分配4个字节)。
如果定义了字符数组,但未对它赋值,这时数组中的元素的值是不可预料的。可以引用(如输出)这些值,结果显然是无意义的,但不会造成严重的后果,容易发现和改正。
如果定义了字符指针变量,应当及时把一个字符变量(或字符数组元素)的地址赋给它,使它指向一个字符型数据,如果未对它赋予一个地址值,它并未具体指向一个确定的对象。此时如果向该指针变量所指向的对象输入数据,可能会出现严重的后果。
char *a;
scanf("%s",a) //错误
char *a,str[10];
a=str;
scanf("%s",a); //正确
(5)指针变量的值是可以改变的,而字符数组名代表一个固定的值(数组首元素的地址),不能改变。
(6)字符数组中各元素的值是可以改变的(可以对它们再赋值),但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们再赋值)。如:
char a[]="House";
//字符数组a初始化
char * b="House";
//字符指针变量b指向字符串常量的第一个字符
a[2]='r';
//合法,r取代a数组元素a[2]的原值u
b[2]='r';
//非法,字符串常量不能改变
(7)引用数组元素。对字符数组可以用下标法(用数组名和下标)引用一个数组元素(如a[5]
),也可以用地址法(如*(a十5)
)引用数组元素a[5]
。如果定义了字符指针变量p并使它指向数组a的首元素,则可以用指针变量带下标的形式引用数组元素(如 p[5])
,后样,可以用地址法(如*(p+5)
)引用数组元素a[5]
。
但是,如果指针变量没有指向数组,则无法用p[5]
或*(p+5)
这样的形式引用数组中的元素。这时若输出p[5]
或*(p+5)
,系统将输出指针变量p所指的字符后面5个字节的内容。显然这是没有意义的,应当避免出现这种情况。
(8)用指针变量指向一个格式字符串,可以用它代替 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=%fn";
printf(format,a,b);
但使用字符数组时,只能采用在定义数组时初始化或逐个对元素赋值的方法,而不能用赋值语句对数组整体赋值,例如:
char format[];
format="a=%d,b= %dn";
//非法
因此,用指针变量指向字符串的方式更为方便。
8.5 指向函数的指针
8.5.1 什么是函数的指针
函数名代表函数的起始地址。函数名就是函数的指针。
可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。例如:
int (*p)(int,int);
定义p是一个指向函数的指针变量,它可以指向函数类型为整型且有两个整型参数的函数。此时,指针变量p的类型用int(*)(int,int)表示。
8.5.2 用函数指针变量调用函数
例:用函数求整数a和b中的大者
(1)通过函数名调用函数
#include<stdio.h>
int main(){
int max(int,int);
int a,b,c;
printf("please enter a and b:");
scanf("%d,%d,%d",&a,&b);
c=max(a,b);
printf("a=%d\nb=%d\nmax=%d\n",a,b,c);
return0;
}
int max(int x,int y){
int z;
z = x>y?x:y;
return z;
}
(2)通过指针变量调用它所指向的函数
#include<stdio.h>
int main(){
int max(int,int);
int (*p)(int,int);
int a,b,c;
p=max;
printf("please enter a and b:");
scanf("%d,%d,%d",&a,&b);
c=(*p)(a,b);
printf("a=%d\nb=%d\nmax=%d\n",a,b,c);
return0;
}
int max(int x,int y){
int z;
z = x>y?x:y;
return z;
}
8.5.3 怎样定义和使用指向函数的指针变量
一般形式:
类型名 (* 指针变量名)(函数参数表列);
8.5.4 用指向函数的指针作为函数参数
指向函数的指针变量的一个重要用途是把函数的入口地址作为参数传递到其他函数。
指向函数的指针可以作为函数参数,把函数的入口地址传递给形参,这样就能够在被调用的函数中使用实参函数。
例:有两个整数a和b,由用户输入1,2或3。如输入1,程序就给出a和b中的大者,输入2,就给出a和b中的小者,输入3,则求a与b之和。
#inlcude<stdio.h>
int main(){
int fun(int x,int y, int (*p)(int,int));
int max(int,int);
int min(int,int);
int add(int,int);
int a=34,b=-21,n;
printf("please choose 1,2 or 3:");
scanf("%d",&n);
switch(n){
case 1:fun(a,b,max);break;
case 2:fun(a,b,min);break;
case 3:fun(a,b,add);break;
default:printf("wrong");
return 0;
}
int fun(int x,int y,int (*p)(int,int)){
int result;
result=(*p)(x,y);
return 0;
}
int max(int x,int y){
printf("max=%d",x>y?x:y);
return 0;}
int min(int x,int y){
printf("min=%d",x>y?y:x);
return 0;}
int add(int x,int y){
printf("add=%d",x+y);
return 0;}
8.6 返回指针值的函数
一般形式:
类型名 *函数名(参数表列);
例:有a个学生,,每个学生有b门课程的成绩。要求在用户输入学生序号以后,能输出该学生的全部成绩。用指针函数来实现。
#include <stdio.h>
int 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,k;
printf("enter the number of student:");
scanf(" %d",&-k); //输入要找的学生的序号
printf("The scores of No.%d are: \n", k);
p=search(score,k); //调用search函数,返回score[k][o]的地址
for(i=0;i<4;i++)
printf("%5.2f\t",*(p+i)); //输出score[k][0]~score[k][3]的值
printf("\n");
return 0;
}
float * search(float ( *pointer)[4], int n){//形参pointer是指向一维数组的指针变量
float *pt:
pt=*(pointer+n); //pt的值是&score[k][0]
return(pt);}
8.7 指针数组和多重指针
8.7.1 什么是指针数组
一个数组,若其元素均为指针类型数据,称为指针数组,也就是说,指针数组中的每一个元素都存放一个地址,相当于一个指针变量。下面定义一个指针数组:
int *p[4];
由于[]比*优先级高,因此p先与[4]结合,形成p[4]形式,这显然是数组形式,表示p数组有4个元素。然后再与p前面的“*”结合,“*”表示此数组是指针类型的,每个数组元素(相当于一个指针变量)都可指向一个整型变量。
注意不要写成
int (*p)[4];//这是指向一维数组的指针变量
定义一维指针数组的一般形式为
类型名 *数组名[数组长度];
类型名中应包括符号“*”,如“int *”表示是指向整型数据的指针类型。
8.7.2 指向指针数据的指针变量
在了解了指针数组的基础上,需要了解指向指针数据的指针变量,简称为指向指针的指针。从图8.38可以看到, name是一个指针数组,它的每一个元素是一个指针型的变量,其值为地址。name既然是一个数组,它的每一元素都应有相应的地址。数组名name代表该指针数组首元素的地址。name+i是name[i]的地址。name+i就是指向指针型数据的指针。还可以设置一个指针变量p,它指向指针数组的元素(见图8.38)。p就是指向指针型数据的指针变量。
怎样定义一个指向指针数据的指针变量呢?下面定义一个指向指针数据的指针变量:
char ** p;
p的前面有两个*号。从附录C可以知道,*运算符的结合性是从右到左,因此**p
相当于*(*p)
,显然*p
是指针变量的定义形式。如果没有最前面的*,那就是定义了一个指向字符数据的指针变量。现在它前面又有一个*号,即 char ** p
。可以把它分为两部分看,即:char *
和(* p
),后面的( *p
)表示p是指针变量,前面的char*
表示p指向的是 char *
型的数据。也就是说,p指向一个字符指针变量(这个字符指针变量指向一个字符型数据)。如果引用*p
,就得到p所指向的字符指针变量的值,如果有:
p=name+2;
printf("%d\n",*p);
printf("%s\n",*p);
第1个printf 函数语句输出name[2]的值(它是一个地址),第2个printf函数语句以字符串形式(%s)输出字符串"Great Wall”。
8.7.3 指针数组作main函数的形参
指针数组的一个重要应用是作为main函数的形参。在以往的程序中, main函数的第1行一般写成以下形式:
int main()
//或
int main(void)
括号中是空的或有“void”,表示main函数没有参数,调用main函数时不必给出实参。这是一般程序常采用的形式。实际上,在某些情况下,main函数可以有参数,即:
int main( int argc,char *argv[])
其中,argc和 argv就是main函数的形参,它们是程序的“命令行参数”。argc( argumentcount的缩写,意思是参数个数),argv(argument vector缩写,意思是参数向量),它是一个*char
指针数组,数组中每一个元素(其值为指针)指向命令行中的一个字符串的首字符。
通常main函数和其他函数组成一个文件模块,有一个文件名。对这个文件进行编译和连接,得到可执行文件(后缀为.exe)。用户执行这个可执行文件,操作系统就调用main函数,然后由main函数调用其他函数,从而完成程序的功能。
什么情况下 main函数需要参数? main函数的形参是从哪里传递给它们的呢?显然形参的值不可能在程序中得到。main函数是操作系统调用的,实参只能由操作系统给出。在操作命令状态下,实参是和执行文件的命令一起给出的。例如在 DOS,UNIX或Linux等系统的操作命令状态下,在命令行中包括了命令名和需要传给main函数的参数。
命令行的一般形式为
命令名 参数1参数2…参数n
命令名和各参数之间用空格分隔。命令名是可执行文件名(此文件包含main函数),假设可执行文件名为filel.exe,今想将两个字符串"China","Beijing"作为传送给main函数的参数。命令行可以写成以下形式:
filel China Beijing
file1为可执行文件名,China和 Beijing 是调用main函数时的实参。实际上,文件名应包括盘符、路径,今为简化起见,用file1来代表。
如果有一个名为file1的文件,它包含以下的main函数:
#include<stdio.h>
int main(int argc, char *argv[])
{
while(--argc>0)
printf("%s%c",*++argv,(argc>1)?'':'\n');
return 0;
}
其实, main函数中的形参不一定命名为argc和 argv,可以是任意的名字,只是人们习惯用argc和argv而已。
利用指针数组作main 函数的形参,可以向程序传送命令行参数(这些参数是字符串),这些字符串的长度事先并不知道,而且各参数字符串的长度一般并不相同,命令行参数的数目也是可以任意的。用指针数组能够较好地满足上述要求。
8.8 动态内存分配与指向它的指针变量
8.8.1 什么是内存的动态分配
第7章介绍过全局变量和局部变量,全局变量是分配在内存中的静态存储区的,非静态的局部变量(包括形参)是分配在内存中的动态存储区的,这个存储区是一个称为栈(stack)的区域。除此以外,C语言还允许建立内存动态分配区域,以存放一些临时用的数据,这些数据不必在程序的声明部分定义,也不必等到函数结束时才释放,而是需要时随时开辟,不需要时随时释放。这些数据是临时存放在一个特别的自由存储区,称为堆(heap)区。可以根据需要,向系统申请所需大小的空间。由于未在声明部分定义它们为变量或数组,因此不能通过变量名或数组名去引用这些数据,只能通过指针来引用。
8.8.2 怎样建立内存的动态分配
1.用malloc函数开辟动态存储区
其函数原型为:
*void malloc(unsigned int size);
其作用是在内存的动态存储区中分配一个长度为size的连续空间。形参size的类型定为无符号整型(不允许为负数)。此函数的值(即“返回值”)是所分配区域的第一个字节的地址,或者说,此函数是一个指针型函数,返回的指针指向该分配域的第一个字节。如:
malloc(100);//开辟100字节的临时分配域,函数值为其第1个字节的地址
注意指针的基类型为void,即不指向任何类型的数据,只提供一个纯地址。如果此函数未能成功地执行(例如内存空间不足),则返回空指针(NULL)。
2.用calloc函数开辟动态存储区
其函数原型为
*void calloc(unsigned n , unsigned size);
其作用是在内存的动态存储区中分配n个长度为size的连续空间,这个空间一般比较大,足以保存一个数组。
用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。这就是动态数组。函数返回指向所分配域的第一个字节的指针;如果分配不成功,返回NULL。如:
p=calloc(50,4);//开辟50×4个字节的临时分配域,把首地址赋给指针变量p
3.用realloc函数重新分配动态存储区其函数原型为
*void * realloc(void p,unsigned int size);
如果已经通过malloc函数或calloc函数获得了动态空间,想改变其大小,可以用recalloc函数重新分配。
用realloc函数将p所指向的动态空间的大小改变为size。p的值不变。如果重分配不成功,返回NULL。如
realloc(p,50);//将p所指向的已分配的动态空间改为50字节
4.用free函数释放动态存储区
其函数原型为
*void free( void p):
其作用是释放指针变量p所指向的动态空间,使这部分空间能重新被其他变量使用。p应是最近一次调用calloc或malloc函数时得到的函数返回值。如:
free(p);//释放指针变量p所指向的已分配的动态空问
free函数无返回值。
以上4个函数的声明在 stdlib.h头文件中,在用到这些函数时应当用“# include<stdlib.h>”指令把stdlib.h头文件包含到程序文件中。
8.8.3 void指针类型
c 99允许使用基类型为void 的指针类型。可以定义一个基类型为void的指针变量(即void *型变量),它不指向任何类型的数据。请注意:不要把“指向void类型”理解为能指向“任何的类型”的数据,而应理解为“指向空类型”或“不指向确定的类型”的数据。在将它的值赋给另一指针变量时由系统对它进行类型转换,使之适合于被赋值的变量的类型。例如:
int a=3;
//定义a为整型变量
int *pl=&a ;
1/pl指向int型变量
char *p2;
// p2指向char型变量
void *p3;
//p3为无类型指针变量(基类型为void型)
p3=(void *)pl;
//将pl的值转换为void *类型,然后赋值给p3
p2=(char *) p3;
//将p3的值转换为char*类型,然后赋值给 p2
printf("%d" ,*pl);
//合法,输出整型变量a的值
p3=&-a; printf("%d", *p3);
//错误,p3是无指向的,不能指向a
8.9 有关指针的小结
(1)首先要准确理解指针的含义。“指针”是C语言中一个形象化的名词,形象地表示"指向”的关系,其在物理上的实现是通过地址来完成的。正如高级语言中的“变量”,在物理上是“命名的存储单元”。Windows中的“文件夹”实际上是“目录”。离开地址就不可能弄清楚什么是指针。明确了“指针就是地址”,就比较容易理解了,许多问题也迎办而解了。例如:
- &a是变量a的地址,也可称为变量a的指针。
- 指针变量是存放地址的变量,也可以说,指针变量是存放指针的变量。
- 指针变量的值是一个地址,也可以说,指针变量的值是一个指针。
- 指针变量也可称为地址变量,它的值是地址。
- &是取地址运算符,&a是a的地址,也可以说,&.是取指针运算符。&a是变量a的指针(即指向变量a的指针)。
- 数组名是一个地址,是数组首元素的地址,也可以说,数组名是一个指针,是数组首元素的指针。
- 函数名是一个指针(指向函数代码区的首字节),也可以说函数名是一个地址(函数代码区首字节的地址)。
- 函数的实参如果是数组名,传递给形参的是一个地址,也可以说,传递给形参的是一个指针。
(2)在C语言中,所有数据都要存储在内存的存储单元中,若写成123,则认为是整数,按整型的存储形式存放,如果写成123.0,则认为是单精度实数,按单精度实型的存储形式存放。此外,不同类型数据有不同的运算规则。可以说,C语言中的数据都是“有类型的数据”,或称“带类型的数据”。
对地址而言,也是同样的,它也有类型,首先,它不是一个数值型数据,不是按整型或浮点型方式存储,它是按指针型数据的存储方式存储的(虽然在VisualC++中也为指针变量分配4个字节,但不同于整型数据的存储形式)。指针型存储单元是专门用来存放地址的,指针型数据的存储形式就是地址的存储形式。
其次,它不是一个简单的纯地址,还有一个指向的问题,也就是说它指向的是哪种类型的数据。如果没有这个信息,是无法通过地址存取存储单元中的数据的。所以,一个地址型的数据实际上包含3个信息:
①表示内存编号的纯地址。
②它本身的类型,即指针类型。
③以它为标识的存储单元中存放的是什么类型的数据,即基类型。
例如:已知变量为a为int型,&a为a的地址,它就包括以上3个信息,它代表的是一个整型数据的地址,int是&a的基类型(即它指向的是int型的存储单元)。可以把②和③两项合成一项,如“指向整型数据的指针类型”或“基类型为整型的指针类型”,其类型可以表示为“int *”型。这样,对地址数据来说,也可以说包含两个要素:内存编号(纯地址)和类型(指针类型和基类型)。这样的地址是“带类型的地址”而不是纯地址。
(3)要区别指针和指针变量。指针就是地址,而指针变量是用来存放地址的变量。有人认为指针是类型名,指针的值是地址。这是不对的。类型是没有值的,只有变量才有值,正确的说法是指针变量的值是一个地址。不要杜撰出“地址的值”这样莫须有的名词。地址本身就是一个值。
(4)什么叫“指向”?地址就意味着指向,因为通过地址能找到具有该地址的对象。对于指针变量来说,把谁的地址存放在指针变量中,就说此指针变量指向谁。但应注意:并不是任何类型数据的地址都可以存放在同一个指针变量中的,只有与指针变量的基类型相同的数据的地址才能存放在相应的指针变量中。例如:
int a,*p; //p是int *型的指针变量,基类型是int型
float b;
p=&a; //a是 int型,合法
p=&b; //b是float型,类型不匹配
既然许多数据对象(如变量,数组、字符串和函数等)都在内存中被分配存储空间,就有了地址,,也就有了指针。可以定义一些指针变量,分别存放这些数据对象的地址,即指向这些对象。
void *指针是一种特殊的指针,不指向任何类型的数据。如果需要用此地址指向某类型的数据,应先对地址进行类型转换。可以在程序中进行显式的类型转换,也可以由编译系统自动进行隐式转换。无论用哪种转换,读者必须了解要进行类型转换。
(5)要深入掌握在对数组的操作中正确地使用指针,搞清楚指针的指向。一维数组名代表数组首元素的地址,如:
int *p,a[10];
p=a;
p是指向int型类型的指针变量,显然,p只能指向数组中的元素(int型变量),而不是指向整个数组。在进行赋值时一定要先确定赋值号两侧的类型是否相同,是否允许赋值。
对“p=a;”,准确地说应该是:p指向a数组的首元素,在不引起误解的情况下,有时也简称为:p指向a数组,但读者对此应有准确的理解。同理,p指向字符串,也应理解为p指向字符串中的首字符。
(6)有关指针变量的归纳比较,见表8.4。
(7)指针运算。
①指针变量加(减)一个整数。
例如: p++,p--,p+i,p-i,p+=i,p-=i
等均是指针变量加(减)一个整数。将该指针变量的原值(是一个地址)和它指向的变量所占用的存储单元的字节数相加(减)。
②指针变量赋值。
将一个变量地址赋给一个指针变量。例如:
p=8a;
//(将变量a的地址赋给p)
p=array;
//(将数组array首元素地址赋给p
p=&array[i];
//(将数组array第i个元素的地址赋给p)
p=max;
//( max为已定义的函数﹐将max的入口地址赋给p)
pl=p2;
//(pl 和 p2是基类型相同指针变量,将p2的值赋给pl)
③两个指针变量可以相减。
如果两个指针变量都指向同一个数组中的元素,则两个指针变量值之差是两个指针之间的元素个数,见图8.42。
④两个指针变量比较。
若两个指针指向同一个数组的元素,则可以进行比较。指向前面的元素的指针变量“小于”指向后面元素的指针变量。如果p1和 p2不指向同一数组则比较无意义。
(8)指针变量可以有空值,即该指针变量不指向任何变量,可以这样表示:p=NULL;
其中,NULL是一个符号常量,代表整数0。在stdio.h头文件中对 NULL进行了定义:
#define NULL 0
它使p指向地址为0的单元。系统保证使该单元不作它用(不存放有效数据)。
应注意,p的值为NULL与未对p赋值是两个不同的概念。前者是有值的(值为0),不指向任何变量,后者虽未对p赋值但并不等于p无值,只是它的值是一个无法预料的值,也就是p可能指向一个事先未指定的单元。这种情况是很危险的。因此,在引用指针变量之前应对它赋值。
任何指针变量或地址都可以与NULL作相等或不相等的比较,例如:if(p==NULL)…
本章介绍了指针的基本概念和初步应用。指针是C语言中很重要的概念,是C的一个重要特色。使用指针的优点:①提高程序效率;②在调用函数时当指针指向的变量的值改变时,这些值能够为主调函数使用,即可以从函数调用得到多个可改变的值;③可以实现动态存储分配。