第8章 善于利用指针
学习 C 语言的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。
正如您所知道的,每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址。请看下面的实例,它将输出定义的变量地址:
#include <stdio.h>
int main () { int var1; char var2[10];
printf("var1 变量的地址: %p\n", &var1 ); printf("var2 变量的地址: %p\n", &var2 );
return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
var1 变量的地址: 0x7fff5cc109d4 var2 变量的地址: 0x7fff5cc109de |
通过上面的实例,我们了解了什么是内存地址以及如何访问它。接下来让我们看看什么是指针。
8.1什么是指针?
指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。
指针变量声明的一般形式为:type *var-name;
在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
int *ip; /* 一个整型的指针 */ double *dp; /* 一个 double 型的指针 */ float *fp; /* 一个浮点型的指针 */ char *ch; /* 一个字符型的指针 */ |
所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
8.2指针变量
8.2.1 使用指针变量的例子
#include <stdio.h> int main() { int a=100,b=10; //定义整型变量a,b,并初始化 int *pointer_1,*pointer_2; //定义指向整型数据的指针变量pointer_1, pointer_2 pointer_1=&a; //把变量a的地址赋给指针变量pointer_1 pointer_2=&b; //把变量b的地址赋给指针变量pointer_2 printf("a=%d,b=%d\n",a,b); //输出变量a和b的值 printf("*pointer_1=%d,*pointer_2=%d\n",*pointer_1,*pointer_2); //输出变量a和b的值 return 0; } |
运行结果:
上面变量对应的关系,如图:
注:定义指针变量时,左侧应有类型名,否则就不是定义指针变量。也称为基类型
8.2.2 怎样定义指针变量
定义指针的一般形式为:
类型名 *指针变量名;
如:
int *pointer_1;
注意:左端的int是在定义指针变量时必须指定的"基类型".换个说法就是类型名一定要有,否则是错误的定义。
如:
*pointer_1; //企图定义pointer_1为指针变量。出错
int *pointer_1; //正确,必须指定指针变量的基类型
说明:在定义指针变量时要注意一下几点:
(1) 指针变量前面的"*"表示该变量的类型为指针型变量。
(2) 在定义指针变量时必须制定基类型(即类型名)
一个变量的指针的含义包括两个方面,一是以存储单元编号表示的地址(如编号为2000的字节),一是它指向的存储单元的数据类型(如int,char,float等)。
(3)如何表示指针类型
指向整型数据的指针类型表示为"int*",读作"指向int的指针"或简称"int指针"
(4) 指针变量中只能存放地址(地址),不要讲一个整数赋给一个指针变量。如:
*pointer_1 = 100; //pointer_1是指针变量,100是整数,不合法的赋值
8.2.3 怎样引用指针变量
(1) 给指针变量赋值。如:
p = &a; //把a的地址赋给指针变量a
(2) 引用指针变量指向的变量
如果已执行"p = &a;",即指针变量p指向了整型变量a,则
printf("%d",*p);
作用:以整数形式输出指针变量p所指向的变量的值,即变量a的值
注:一定要带'*',否则输出地址
也可以用指针的形式对a重新赋值
*p = 1;
表示将1赋值给p当前p指向的变量,因为p指向变量a,则相当于把1赋值给a,即和"a = 1"等价。
(3) 引用指针变量的值。如:
printf("%o",p);
作用:以八进制数形式输出指针变量p的值,如果p指向了a,就是输出了a的地址,即&a.
printf("%o",&a); <==> printf("%o",p); //两者等价
例:
#include <stdio.h> int main() { int *p,a; a = 100; p = &a; printf("%o\n",p); printf("%o\n",&a);
} |
要熟练掌握两个有关的运算符:
(1) &取地址运算符。&a是变量a的地址。
(2) * 指针运算符(或称“间接访问”运算符),*p代表指针变量p指向的对象。
#include <stdio.h> int main() { int *p1,*p2,*p,a,b; //p1,p2的类型是int *类型 printf("please enter two integer numbers:"); scanf("%d,%d",&a,&b); //输入两个整数 p1=&a; //使p1指向变量a p2=&b; //使p2指向变量b if(a<b) //如果a<b { p=p1;p1=p2;p2=p;} //使p1与p2的值互换 printf("a=%d,b=%d\n",a,b); //输出a,b printf("max=%d,min=%d\n",*p1,*p2); //输出p1和p2所指向的变量的值 return 0; } |
运行结果:
指针在过程中的变化:
8.2.4 指针变量作为函数参数
例:对输入的两个整数按大小顺序输出。现用函数处理,而且用指针类型的数据作函数参数。
#include <stdio.h> void swap(int *p1,int *p2); //对swap函数的声明 int main() { int a,b; int *pointer_1,*pointer_2; //定义两个int *型的指针变量 printf("please enter a and b:"); scanf("%d,%d",&a,&b); //输入两个整数 pointer_1=&a; //使pointer_1指向a pointer_2=&b; //使pointer_2指向b if(a<b) swap(pointer_1,pointer_2); //如果a<b,调用swap函数 printf("max=%d,min=%d\n",a,b); //输出结果 return 0; }
void swap(int *p1,int *p2) //定义swap函数 { int temp; temp=*p1; //使*p1和*p2互换 *p1=*p2; *p2=temp; } |
运行结果:
指针变量pointer_1 pointer_2的变量过程
注:要注意区分以下三种形式,可自己调试,增加理解
void swap(int *p1,int *p2)//定义swap函数 { int temp; temp=*p1;//使*p1和*p2互换 *p1=*p2; *p2=temp; } |
void swap(int *p1,int *p2) { int *temp; *temp=*p1; *p1=*p2; *p2=*temp; } |
void swap(int x,int y) { int temp; temp=x; x=y; y=temp; } |
下面有一个问题需要注意,也可以直接跳过,如果想提升可以理解,以理解为主,不需要死记硬背。
指针变量作为函数参数函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。如果不用指针变量是难以做到这一点的。要善于利用指针法。 如果想通过函数调用得到n个要改变的值,可以这样做:
1.在主调函数中设n个变量,用n个指针变量指向它们;
2.设计一个函数,有n个指针形参。在这个函数中改变这n个形参的值;
3.在主调函数中调用这个函数,在调用时将这n个指针变量作实参,将它们的值,也就是相关变量的地址传给该函数的形参;
3.在执行该函数的过程中,通过形参指针变量,改变它们所指向的n个变量的值;
5.主调函数中就可以使用这些改变了值的变量。 |
例:输入3个整数a,b,c,要求按由大到小的顺序将它们输出。用函数实现。
#include <stdio.h> int main() { void exchange(int *q1, int *q2, int *q3); //函数声明 int a,b,c,*p1,*p2,*p3; printf("please enter three numbers:"); scanf("%d,%d,%d",&a,&b,&c); p1=&a;p2=&b;p3=&c; exchange(p1,p2,p3); printf("The order is:%d,%d,%d\n",a,b,c); return 0; }
void exchange(int *q1, int *q2, int *q3) //将3个变量的值交换的函数 { void swap(int *pt1, int *pt2); //函数声明 if(*q1<*q2) swap(q1,q2); //如果a<b,交换a和b的值 if(*q1<*q3) swap(q1,q3); //如果a<c,交换a和c的值 if(*q2<*q3) swap(q2,q3); //如果b<c,交换b和c的值 }
void swap(int *pt1, int *pt2) //交换2个变量的值的函数 { int temp; temp=*pt1; //交换*pt1和*pt2变量的值 *pt1=*pt2; *pt2=temp; } |
运行结果:
8.3 通过指针引用数组
8.3.1 数组元素的指针
数组元素的指针就是素组元素的地址。
int a[10]={1,3,5,7,9,11,13,15,17,19}; //定义a为包含10个整型数据的数组 int *p; //定义p为指向整型变量的指针变量 p=&a[0]; //把a[0]元素的地址赋给指针变量p |
引用数组元素的方式
(1) 下标法
(2) 指针法
p=&a[0]; //p的值是a[0]的地址
等价于
p=a; //p的值是数组a首元素(即a[0])的地址
注:程序中的数组名不代表整个数组,只代表数组首元素的地址。
以下是指针变量的初始化方式:
(1) 在定义后再单独进行初始化
int *p;
p=&a[0]; //不应写成*p=&a[0];
(2) 在定义时直接进行初始化
int *p=&a[0];
等价于
int *p=a;
以上三种的作用:将a数组首元素(即a[0])的地址赋值给指针变量p(而不是赋给*p)
8.3.2 在引用数组元素时指针的运算
在指针已指向一个数组元素时,可以对指针进行以下运算:
1.加一个整数(用+或+=),如p+1,表示指向同一数组中的下一个元素;
2.减一个整数(用-或-=),如p-1,表示指向同一数组中的上一个元素;
3.自加运算,如p++,++p;
4.自减运算,如p--,--p。
两个指针相减,如p1-p2(p1和p2都指向同一数组中的元素时才有意义),结果为两个地址之差除以数组元素的长度。
注:两个地址不能相加,如p1+p2是无实际意义的。
说明:
*(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i]。
p的初值为&a[0],则p+i和a+i就是数组元素a[i]的地址
[]实际上是变址运算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。
例:有一个整型数组a,有10个元素,要求输出数组中的全部元素。
//下标法 #include <stdio.h> int main() { int a[10]; int i; printf("please enter 10 integer numbers:"); for(i=0;i<10;i++) scanf("%d",&a[i]); for(i=0;i<10;i++) printf("%d ",a[i]); //数组元素用数组名和下标表示 printf("%\n"); return 0; } |
//通过数组名计算数组元素地址,找出元素的值 #include <stdio.h> int main() { int a[10]; int i; printf("please enter 10 integer numbers:"); for(i=0;i<10;i++) scanf("%d",&a[i]); for(i=0;i<10;i++) printf("%d ",*(a+i)); //通过数组名和元素序号计算元素地址找到该元素 printf("\n"); return 0; } |
//用指针变量指向数组元素 #include <stdio.h> int main() { int a[10]; int *p,i; printf("please enter 10 integer numbers:"); for(i=0;i<10;i++) scanf("%d",&a[i]); for(p=a;p<(a+10);p++) printf("%d ",*p); //用指针指向当前的数组元素 printf("\n"); return 0; } |
说明:
第(1)和第(2)种方法执行效率是相同的。C编译系统是将a[i]转换为*(a+i)处理的,即先计算元素地址。因此用第(1)和第(2)种方法找数组元素费时较多。
第(3)种方法比第(1)、第(2)种方法快,用指针变量直接指向元素,不必每次都重新计算地址,像p++这样的自加操作是比较快的。这种有规律地改变地址值(p++)能大大提高执行效率。 |
例:通过指针变量输出整型数组a的10个元素。
#include <stdio.h> int main() { int *p,i,a[10]; p=a; //p指向a[0] ① printf("please enter 10 integer numbers:"); for(i=0;i<10;i++) scanf("%d",p++); //输入10个整数给a[0]~a[9] for(i=0;i<10;i++,p++) printf("%d ",*p); //想输出a[0]~a[9] ② printf("\n"); return 0; } |
#include <stdio.h> int main() { int i,a[10],*p=a; //p的初值是a,p指向a[0] printf("please enter 10 integer numbers:"); for(i=0;i<10;i++) scanf("%d",p++); p=a; //重新使p指向a[0] for(i=0;i<10;i++,p++) printf("%d ",*p); printf("\n"); return 0; } |
运行上面两个代码看看有何区别,造成这种区别的原因是什么?
答:第一种是因为指针已经到了数组的末尾,继续循环则超出了数组的范围,则会直接输出P对应的地址值。如图所示:
技巧:
设p开始时指向数组a的首元素(即p=a)
(1)
p++; //使p指向下一元素a[1]
*p; //得到下一个元素a[1]的值
(2)
*p++; /*由于++和*同优先级,结合方向自右而左,因此它等价于*(p++)。先引用p的值,实现*p的运算,然后再使p自增1*/
(3)
*(p++); //先取*p值,然后使p加1
*(++p); //先使p加1,再取*p
(4)
++(*p); /*表示p所指向的元素值加1,如果p=a, 则相当于++a[0],若a[0]的值为3,则a[0]的值为4。注意: 是元素a[0]的值加1,而不是指针p的值加1*/
(5)
如果p当前指向a数组中第i个元素a[i],则:
*(p--) //相当于a[i--],先对p进行“*”运算,再使p自减
*(++p) //相当于a[++i],先使p自加,再进行“*”运算
*(--p) //相当于a[--i],先使p自减,再进行“*”运算
8.3.4 用数组名作函数参数
以变量名和数组名作为函数参数的比较
C语言调用函数时虚实结合的方法都是采用“值传递”方式,当用变量名作为函数参数时传递的是变量的值,当用数组名作为函数参数时,由于数组名代表的是数组首元素地址,因此传递的值是地址,所以要求形参为指针变量。
注意:实参数组名代表一个固定的地址,或者说是指针常量,但形参数组名并不是一个固定的地址,而是按指针变量处理。
例:将数组a中n个整数按相反顺序存放
#include <stdio.h> int main() { void inv(int x[],int n); //inv函数声明 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); //调用inv函数,进行交换 printf("The array has been inverted:\n"); for(i=0;i<10;i++) printf("%d ",a[i]); //输出交换后数组各元素的值 printf("\n"); return 0; } 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; //把x[i]和x[j]交换 } return; } |
#include <stdio.h> int main() { void inv(int *x,int n); 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"); return 0; }
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;} //*i与*j交换 return; } |
以上两种方式所得结果是一致的,只是实现的形式有区别。所得结果都为下图所示:
说明:以下的形式要认识,以下四种都是等价的
例:将数组a中n个整数按相反顺序存放,用指针变量作实参。
#include <stdio.h> int main() { void inv(int *x,int n); //inv函数声明 int i,arr[10],*p=arr; //指针变量p指向arr[0] printf("The original array:\n"); for(i=0;i<10;i++,p++) scanf("%d",p); //输入arr数组的元素 printf("\n"); p=arr; //指针变量p重新指向arr[0] inv(p,10); //调用inv函数,实参p是指针变量 printf("The array has been inverted:\n"); for(p=arr;p<arr+10;p++) printf("%d ",*p); printf("\n"); return 0; }
void inv(int *x,int n) //定义inv函数,形参x是指针变量 { 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; } |
注:如果用指针变量作实参,必须先使指针变量有确定值,指向一个已定义的对象。
例:用指针方法对10个整数按由大到小顺序排序。(选择排序法)
//形参是数组 #include <stdio.h> int main() { void sort(int x[],int n); //sort函数声明 int i,*p,a[10]; p=a; //指针变量p指向a[0] printf("please enter 10 integer numbers:"); for(i=0;i<10;i++) scanf("%d",p++); //输入10个整数 p=a; //指针变量p重新指向a[0] sort(p,10); //调用sort函数 for(p=a,i=0;i<10;i++) { printf("%d ",*p); //输出排序后的10个数组元素 p++; } printf("\n"); return 0; }
void sort(int x[],int n)//x是形参数组名 { 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;} } } |
//形参是指针 #include <stdio.h> int main() { void sort(int x[],int n); //sort函数声明 int i,*p,a[10]; p=a; //指针变量p指向a[0] printf("please enter 10 integer numbers:"); for(i=0;i<10;i++) scanf("%d",p++); //输入10个整数 p=a; //指针变量p重新指向a[0] sort(p,10); //调用sort函数 for(p=a,i=0;i<10;i++) { printf("%d ",*p); //输出排序后的10个数组元素 p++; } printf("\n"); return 0; }
void sort(int *x,int n) //形参x是指针变量 { 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; //*(x+j)就是x[j],其他亦然 if(k!=i) { t=*(x+i); *(x+i)=*(x+k); *(x+k)=t;} } } |
8.3.5 通过指针引用多维数组
注:这章节了解即可,如果想提升自己,不仅需要理解还需要会使用。
理解以下这张图便可了解多维数组:
说明:我们在这里说明一下,在计算机底层是不存在二维数组这种存储方式的,都是以一维数组为基础逻辑化出二维数组,而一维数组不仅是逻辑化更是物理上也是可以实现的。总的来说,二维数组是一维数组的逻辑化,本质上二维数组还是一维数组。只是为了方便人们理解,才把二维数组分为行和列的方式显示。
如果用一个指针变量pt来指向此一维数组:
int (*pt)[4];
//表示pt指向由4个整型元素组成的一维数组,此时指针变量pt的基类型是由4个整型元素组成的一维数组
例:输出二维数组的有关数据(地址和元素的值)。
#include <stdio.h> int main() { int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23}; printf("%d,%d\n",a,*a); //0行起始地址和0行0列元素地址 printf("%d,%d\n",a[0],*(a+0)); //0行0列元素地址 printf("%d,%d\n",&a[0],&a[0][0]); //0行起始地址和0行0列元素地址 printf("%d,%d\n",a[1],a+1); //1行0列元素地址和1行起始地址 printf("%d,%d\n",&a[1][0],*(a+1)+0); //1行0列元素地址 printf("%d,%d\n",a[2],*(a+2)); //2行0列元素地址 printf("%d,%d\n",&a[2],a+2); //2行起始地址 printf("%d,%d\n",a[1][0],*(*(a+1)+0)); //1行0列元素的值 printf("%d,%d\n",*a[2],*(*(a+2)+0)); //2行0列元素的值 return 0; } |
结果如下:
说明:要注意看懂以上的格式,区分各种形式的联系和区别。如a和*a在输出上是等价的
例:有一个3×4的二维数组,要求用指向元素的指针变量输出二维数组各元素的值。
#include <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; } |
运行结果:
例:输出二维数组任一行任一列元素的值。
#include <stdio.h> int main() { int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23}; //定义二维数组a并初始化 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; } |
#include <stdio.h> int main() { int a[4]={1,3,5,7}; //定义一维数组a,包含4个元素 int (*p)[4]; //定义指向包含4个元素的一维数组的指针变量中 p=&a; //使p指向一维数组 printf("%d\n",(*p)[3]); //输出a[3],输出整数7 return 0; } |
比较: ① int a[4];(a有4个元素,每个元素为整型) ② int (*p)[4]; 第②种形式表示(*p)有4个元素,每个元素为整型。也就是p所指的对象是有4个整型元素的数组,即p是指向一维数组的指针,见图8.24。应该记住,此时p只能指向一个包含4个元素的一维数组,不能指向一维数组中的某一元素。p的值是该一维数组的起始地址。虽然这个地址(指纯地址)与该一维数组首元素的地址相同,但它们的基类型是不同的。 |
指向由m个元素组成的一维数组的指针变量 要注意指针变量的类型,从“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的值就不相同了。 |
一维数组名可以作为函数参数,多维数组名也可作函数参数。
用指针变量作形参,以接受实参数组名传递来的地址。可以有两种方法:
① 用指向变量的指针变量;
② 用指向一维数组的指针变量。
例8:有一个班,3个学生,各学4门课,计算总平均分数以及第n个学生的成绩。
#include <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"); } |
运行结果:
注:实参与形参如果是指针类型,应当注意它们的基类型必须一致。不应把int *型的指针(即数组元素的地址)传给int (*)[4] 型(指向一维数组)的指针变量,反之亦然。
例:在上例的基础上,查找有一门以上课程不及格的学生,输出他们的全部课程的成绩。
#include <stdio.h> int main() { void search(float (*p)[4],int n); //函数声明 float score[3][4]={{65,57,70,60},{58,87,90,81},{90,99,100,98}}; //定义二维数组函数score search(score,3); //调用search函数 return 0; }
void search(float (*p)[4],int n) //形参p是指向包含4个float型元素的一维数组的指针变量 { 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; //*(*(p+j)+i)就是score[j][i] 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)); //输出*(*(p+j)+i)就是输出score[j][i]的值 printf("\n"); } } } |
运行结果:
8.4 通过指针引用字符串
8.4.1 字符串的引用方式
(1)用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明“%s”输出该字符串。
(2)用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
例:定义一个字符数组,在其中存放字符串″I love China!″,输出该字符串和第8个字符。
#include <stdio.h> int main() { char string[]="I love China!"; //定义字符数组sting printf("%s\n",string); //用%s格式声明输出string,可以输出整个字符串 printf("%c\n",string[7]); //用%c格式输出一个字符数组元素 return 0; } |
运行结果:
例:通过字符指针变量输出一个字符串。
#include <stdio.h> int main() { char *string="I love China!"; //定义字符指针变量string并初始化 printf("%s\n",string); //输出字符串 return 0; } |
运行结果:
在C语言中只有字符变量,没有字符串变量。
char *string="I love China!";
等价于
char *string; //定义一个char *型变量
string=″I love China!″;
//把字符串第1个元素的地址赋给字符指针变量string
注:string被定义为一个指针变量,基类型为字符型。它只能指向一个字符类型数据,而不能同时指向多个字符数据,更不是把″I love China!″这些字符存放到string中(指针变量只能存放地址),也不是把字符串赋给*string。只是把″I love China!″的第1个字符的地址赋给指针变量string。
可以对指针变量进行再赋值,string=″I am a student.″; //对指针变量string重新赋值
可以通过字符指针变量输出它所指向的字符串,printf(″%s\n″,string); //%s可对字符串进行整体的输入输出
说明:%s是输出字符串时所用的格式符,在输出项中给出字符指针变量名string,则系统会输出string所指向的字符串第1个字符,然后自动使string加1,使之指向下一个字符,再输出该字符……如此直到遇到字符串结束标志′\0′为止。注意,在内存中,字符串的最后被自动加了一个′\0′。
例:将字符串a复制为字符串b,然后输出字符串b。
//用数组的方式输出 #include <stdio.h> int main() { char a[]="I am a student.",b[20]; //定义字符数组 int i; for(i=0;*(a+i)!='\0';i++) *(b+i)=*(a+i); //将a[i]的值赋给b[i] *(b+i)='\0'; //在b数组的有效字符之后加'\0' printf("string a is:%s\n",a);//输出a数组中全部有效字符 printf("string b is:"); for(i=0;b[i]!='\0';i++) printf("%c",b[i]); //逐个输出b数组中全部有效字符 printf("\n"); return 0; } |
//用指针变量 #include <stdio.h> int main() { char a[]="I am a boy.",b[20],*p1,*p2; p1=a;p2=b; //p1,p2分别指向a数组和b数组中的第一个元素 for(;*p1!='\0';p1++,p2++) //p1,p2每次自加1 *p2=*p1; //将p1所指向的元素的值赋给p2所指向的元素 *p2='\0'; //在复制完全部有效字符后加'\0' printf("string a is:%s\n",a); //输出a数组中的字符 printf("string b is:%s\n",b); //输出b数组中的字符 return 0; } |
8.4.2 字符指针作函数参数
例:用函数调用实现字符串的复制
(1) 用字符数组名作为函数参数
#include <stdio.h> int main() { void copy_string(char from[], char to[]); char a[]="I am a teacher."; char b[]="You are a student."; printf("string a=%s\nstring b=%s\n",a,b); printf("copy string a to string b:\n"); copy_string(a,b); //用字符数组名作为函数实参 printf("\nstring a=%s\nstring b=%s\n",a,b); return 0; }
void copy_string(char from[], char to[]) //形参为字符数组 { int i=0; while(from[i]!='\0') { to[i]=from[i]; i++;} to[i]='\0'; } |
运行结果:
(2) 用字符型指针变量作实参
#include <stdio.h> int main() { void copy_string(char from[], char to[]); //函数声明 char a[]="I am a teacher."; //定义字符数组a并初始化 char b[]="You are a student."; //定义字符数组b并初始化 char *from=a,*to=b; //from指向a数组首元素,to指向b数组首元素 printf("string a=%s\nstring b=%s\n",a,b); printf("copy string a to string b:\n"); copy_string(from,to); //实参为字符指针变量 printf("\nstring a=%s\nstring b=%s\n",a,b); return 0; } void copy_string(char from[], char to[]) //形参为字符数组 { int i=0; while(from[i]!='\0') { to[i]=from[i]; i++;} to[i]='\0'; } |
运行结果:
说明:指针变量from的值是a数组首元素的地址,指针变量to的值是b数组首元素的地址。它们作为实参,把a数组首元素的地址和b数组首元素的地址传递给形参数组名from和to(它们实质上也是指针变量)。其他与程序(1)相同。
(3) 用字符指针变量作形参和实参
#include <stdio.h> int main() { void copy_string(char *from, char *to); char *a="I am a teacher."; //a是char*型指针变量 char b[]="You are a student."; //b是字符数组 char *p=b; //使指针变量p指向b数组首元素 printf("string a=%s\nstring b=%s\n",a,b); //输出a串和b串 printf("copy string a to string b:\n"); copy_string(a,p); //调用copy_string函数,实参为指针变量 printf("\nstring a=%s\nstring b=%s\n",a,b); //输出改变后的a串和b串 return 0; }
void copy_string(char *from, char *to) //定义函数,形参为字符指针变量 { for(;*from!='\0';from++,to++) { *to=*from;} *to='\0'; } |
运行结果:
函数void copy_string(char *from, char *to)还有以下等价几种,只需记住一种即可,其它做了解。最好的情况当然是全都会用。
void copy_string(char *from, char *to) { for(;(*to++=* from++)!='\0';); //或for(;*to++=* from++;); } |
void copy_string(char *from, char *to) { while((*to=*from)!='\0') //或while(*to=*from) { to++; from++;} } |
void copy_string(char *from, char *to) { while(*from!='\0') //或while(*from) ,因为'\0'的ASCII码为0 *to++=*from++; *to='\0'; } |
void copy_string(char *from, char *to) { while((*to++=*from++)!='\0'); //或while(*to++=*from++) } |
void copy_string(char from[],char to[]) { char *p1, *p2; p1=from;p2=to; while((*p2++=*p1++)!='\0'); } |
|
字符指针作为函数参数时,实参与形参的类型有以下几种对应关系:
8.4.3 使用字符指针变量和字符数组的比较
例:改变指针变量的值。
#include <stdio.h> int main() { char *a="I love China!"; a=a+7; //改变指针变量的值,即改变指针变量的指向 printf("%s\n",a); //输出从a指向的字符开始的字符串 return 0; } |
运行结果:
#include <stdio.h> int main() { char str[]={"I love China!"}; str=str+7; printf("%s\n",str); return 0; } |
说明:
(1)指针变量的值是可以改变的,而字符数组名代表一个固定的值(数组首元素的地址),不能改变。
(2)指针变量a的值是可以变化的。printf函数输出字符串时,从指针变量a当时所指向的元素开始,逐个输出各个字符,直到遇'\0'为止。而数组名虽然代表地址,但它是常量,它的值是不能改变的。
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",&a,&b); c=max(a,b); //通过函数名调用max函数 printf("a=%d\nb=%d\nmax=%d\n",a,b,c); return 0; }
int max(int x,int y) //定义max函数 { int z; if(x>y) z=x; else z=y; return(z); } |
(2) 通过指针变量调用它所指向的函数 #include <stdio.h> int main() { int max(int,int); //函数声明 int (*p)(int,int); //定义指向函数的指针变量p int a,b,c; p=max; //使p指向max函数 printf("please enter a and b:"); scanf("%d,%d",&a,&b); c=(*p)(a,b); //通过指针变量调用max函数 printf("a=%d\nb=%d\nmax=%d\n",a,b,c); return 0; } int max(int x,int y) //定义max函数 { int z; if(x>y)z=x; else z=y; return(z); } |
运行结果:
8.5.3 怎样定义和使用指向函数的指针变量
定义指向函数的指针变量的一般形式为:
类型名 (*指针变量名)(函数参数表列)
如:
int (*p)(int,int);
说明:
(1) 定义指向函数的指针变量,并不意味着这个指针变量可以指向任何函数,它只能指向在定义时指定的类型的函数。
(2) 如果要用指针调用函数,必须先使指针变量指向该函数。
(3) 在给函数指针变量赋值时,只须给出函数名而不必给出参数。
(4) 用函数指针变量调用函数时,只须将(*p)代替函数名即可(p为指针变量名),在(*p)之后的括号中根据需要写上实参。
(5) 对指向函数的指针变量不能进行算术运算,如p+n,p++,p--等运算是无意义的。
(6) 用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。
例:输入两个整数,然后让用户选择1或2,选1时调用max函数,输出二者中的大数,选2时调用min函数,输出二者中的小数。
#include <stdio.h> int main() { int max(int,int); //函数声明 int min(int x,int y); //函数声明 int (*p)(int,int); //定义指向函数的指针变量 int a,b,c,n; printf("please enter a and b:"); scanf("%d,%d",&a,&b); printf("please choose 1 or 2:"); scanf("%d",&n); //输入1戓2 if(n==1) p=max; //如输入1,使p指向max函数 else if (n==2) p=min; //如输入2,使p指向min函数 c=(*p)(a,b); //调用p指向的函数 printf("a=%d,b=%d\n",a,b); if(n==1) printf("max=%d\n",c); else printf("min=%d\n",c); return 0; }
int max(int x,int y) { int z; if(x>y) z=x; else z=y; return(z); }
int min(int x,int y) { int z; if(x<y) z=x; else z=y; return(z); } |
运行结果:
OR
8.5.4 用指向函数的指针作函数参数
例:有两个整数a和b,由用户输入1,2或3。如输入1,程序就给出a和b中的大者,输入2,就给出a和b中的小者,输入3,则求a与b之和。
#include <stdio.h> int main() { int fun(int x,int y, int (*p)(int,int)); //fun函数声明 int max(int,int); //max函数声明 int min(int,int); //min函数声明 int add(int,int); //add函数声明 int a=34,b=-21,n; printf("please choose 1,2 or 3:"); scanf("%d",&n); //输入1,2或3之一 if(n==1) fun(a,b,max); //输入1时调用max函数 else if(n==2) fun(a,b,min); //输入2时调用min函数 else if(n==3) fun(a,b,add); //输入3时调用add函数 return 0; }
int fun(int x,int y,int (*p)(int,int)) //定义fun函数 { int result; result=(*p)(x,y); printf("%d\n",result); //输出结果 }
int max(int x,int y) //定义max函数 { int z; if(x>y) z=x; else z=y; printf("max=" ); return(z); //返回值是两数中的大者 }
int min(int x,int y) //定义min函数 { int z; if(x<y) z=x; else z=y; printf("min="); return(z); //返回值是两数中的小者 }
int add(int x,int y) //定义add函数 { int z; z=x+y; printf("sum="); return(z); //返回值是两数之和 } |
运行结果:不同选择有不同的答案
8.6 返回指针值的函数
定义返回指针值的函数的一般形式为:
类型名 *函数名(参数表列);
解释:
一个函数可以返回一个整型值、字符值、实型值等,也可以返回指针型的数据,即地址。其概念与以前类似,只是返回的值的类型是指针类型而已。如:
int *a(int x,int y);
a是函数名,调用它以后能得到一个int*型(指向整型数据)的指针,即整型数据的地址。x和y是函数a的形参,为整型。
注:在“*a”两侧没有括号,在a的两侧分别为*运算符和()运算符。而()优先级高于*,因此a先与()结合,显然这是函数形式。这个函数前面有一个*,表示此函数是指针型函数(函数值是指针)。最前面的int表示返回的指针指向整型变量。
例:有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][0]的地址 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); } |
运行结果:
例:对上例题,找出其中有不及格的课程的学生及其学生号。
#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]); //函数声明 float *p; int i,j; for(i=0;i<3;i++) //循环3次 { p=search(score+i); //调用search函数,如有不及格返回score[i][0]的地址,否则返回NULL if(p==*(score+i)) //如果返回的是score[i][0]的地址,表示p的值不是NULL { printf("No.%d score:",i); for(j=0;j<4;j++) printf("%5.2f ",*(p+j)); //输出score[i][0]~score[i][3]的值 printf("\n"); } } return 0; }
float *search(float (*pointer)[4]) //定义函数,形参pointer是指向一维数组的指针变量 { int i=0; float *pt; pt=NULL; //先使pt的值为NULL for(;i<4;i++) if(*(*pointer+i)<60) pt=*pointer; //如果有不及格课程,使pt指向score[i][0] return(pt); } |
运行结果:
8.7 指针数组和多重指针
8.7.1 什么是指针数组
定义一维指针数组的一般形式为
类型名 *数组名[数组长度];
如:
int *p[4];
一个数组,若其元素均为指针类型数据,称为指针数组,也就是说,指针数组中的每一个元素都存放一个地址,相当于一个指针变量。
指针数组比较适合用来指向若干个字符串,使字符串处理更加方便灵活。
例:将若干字符串按字母顺序(由小到大)输出。
#include <stdio.h> #include <string.h> int main() { void sort(char *name[],int n); //函数声明 void print(char *name[],int n); //函数声明 char *name[]={"Follow me","BASIC", "Great Wall","FORTRAN","Computer design"}; //定义指针数组,它的元素分别指向5个字符串 int n=5; sort(name,n); //调用sort函数,对字符串排序 print(name,n); //调用print函数,输出字符串 return 0; }
void sort(char *name[],int n) //定义sort函数 { 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) //定义print函数 { int i; for(i=0;i<n;i++) printf("%s\n",name[i]); //按指针数组元素的顺序输出它们所指向的字符串 } |
运行结果:
在了解了指针数组的基础上,需要了解指向指针数据的指针变量,简称为指向指针的指针。
例:使用指向指针数据的指针变量。
#include <stdio.h> int 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); } return 0; } |
运行结果:
说明:
(1) p是指向char*型数据的指针变量,即指向指针的指针。在第1次执行for循环体时,赋值语句“p=name+i;”使p指向name数组的0号元素name[0],*p是name[0]的值,即第1个字符串首字符的地址,用printf函数输出第1个字符串(格式符为%s)。执行5次循环体,依次输出5个字符串。
(2) 指针数组的元素也可以不指向字符串,而指向整型数据或实型数据等。
例:有一个指针数组,其元素分别指向一个整型数组的元素,用指向指针数据的指针变量,输出整型数组各元素的值。
#include <stdio.h> int main() { 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是指向指针型数据的指针变量 p=num; //使p指向num[0] for(i=0;i<5;i++) { printf("%d ",**p); p++; } printf("\n"); return 0; } |
运行结果:
8.7.3 指针数组作main函数的形参
int main(int argc,char *argv[]) { while(argc>1) { ++argv; printf("%s\n", *argv); --argc; } return 0; } |
int main(int argc,char *argv[]) { while(argc-->1) printf("%s\n", *++argv); return 0; }. |
8.8 动态内存分配与指向它的指针变量
8.8.1 什么是内存的动态分配
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的连续空间,这个空间一般比较大,足以保存一个数组。
p=calloc(50,4); //开辟50×4个字节的临时分配域,把首地址赋给指针变量p
用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。这就是动态数组。函数返回指向所分配域的第一个字节的指针;如果分配不成功,返回NULL。
3.用realloc函数重新分配动态存储区
函数原型为
void *realloc(void *p,unsigned int size);
作用:如果已经通过malloc函数或calloc函数获得了动态空间,想改变其大小,可以用realloc函数重新分配。
realloc(p,50); //将p所指向的已分配的动态空间改为50字节
用realloc函数将p所指向的动态空间的大小改变为size。p的值不变。如果重分配不成功,返回NULL。
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*型变量),它不指向任何类型的数据。在将它的值赋给另一指针变量时由系统对它进行类型转换,使之适合于被赋值的变量的类型。
int *pt;
pt=(int *)mcaloc(100); //mcaloc(100)是void *型,把它转换为int *型
注:不要把“指向void类型”理解为能指向“任何的类型”的数据,而应理解为“指向空类型”或“不指向确定的类型”的数据。
例:建立动态数组,输入5个学生的成绩,另外用一个函放数检查其中有无低于60分的,输出不合格的成绩。
#include <stdio.h> #include <stdlib.h> //程序中用了malloc函数,应包含stdlib.h int main() { void check(int *); //函数声明 int *p1,i; //p1是int型指针 p1=(int *)malloc(5*sizeof(int)); //开辟动态内存区,将地址转换成int *型,然后放在p1中 for(i=0;i<5;i++) scanf("%d",p1+i); //输入5个学生的成绩 check(p1); //调用check函数 return 0; }
void check(int *p) //定义check函数,形参是int*指针 { int i; printf("They are fail:"); for(i=0;i<5;i++) if(p[i]<60) printf("%d ",p[i]); //输出不合格的成绩 printf("\n"); } |
运行结果:
有关指针的小结