首页 > 代码库 > C语言指针

C语言指针

1.C语言指针的概念

在计算机中,所有的数据都是存放在内存中的,一般把内存中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不一样,如int占用4个字节,char占用1个字节。为了正确地访问这些内存单元,必须为每个内存单元编上号。每个内存单元的编号是唯一的,根据编号可以准确地找到该内存单元。
 
内存单元的编号叫做地址(Address),也称为指针(Pointer)。
 
内存单元的指针和内存单元的内容是两个不同的概念。 可以用一个通俗的例子来说明它们之间的关系。我们用银行卡到ATM机取款时,系统会根据我们的卡号去查找账户信息,包括存取款记录、余额等,信息正确、余额足够的情况下才允许我们取款。在这里,卡号就是账户信息的指针, 存取款记录、余额等就是账户信息的内容。对于一个内存单元来说,单元的地址(编号)即为指针,其中存放的数据才是该单元的内容。
 
在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。
 
设有字符变量C,其内容为 ‘K‘(ASCII码为十进制数 75),C占用了011A号单元(地址通常用十六进数表示)。设有指针变量P,内容为011A,这种情况我们称为P指向变量C,或说P是指向变量C的指针。
技术分享
 
 
 
严格地说,一个指针是一个地址,是一个常量。而一个指针变量却可以被赋予不同的指针值,是变量。但常把指针变量简称为指针。为了避免混淆,本教程约定:“指针”是指地址,是常量,“指针变量”是指取值为地址的变量。定义指针的目的是为了通过指针去访问内存单元。
 
既然指针变量的值是一个地址,那么这个地址不仅可以是变量的地址,也可以是其它数据结构的地址。在一个指针变量中存放一个数组或一个函数的首地址有何意义呢?
 
因为数组或函数都是连续存放的。通过访问指针变量取得了数组或函数的首地址,也就找到了该数组或函数。这样一来,凡是出现数组,函数的地方都可以用一个指针变量来表示,只要该指针变量中赋予数组或函数的首地址即可。这样做,将会使程序的概念十分清楚,程序本身也精练、高效。
 
在C语言中,一种数据类型或数据结构往往都占有一组连续的内存单元。用“地址”这个概念并不能很好地描述一种数据类型或数据结构,而“指针”虽然实际上也是一个地址,但它却是一个数据结构的首地址,它是“指向”一个数据结构的,因而概念更为清楚,表示更为明确。 这也是引入“指针”概念的一个重要原因。

2.C语言指针变量

变量的指针就是变量的地址。存放变量地址的变量是指针变量。即在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个变量的地址或称为某变量的指针。
 
为了表示指针变量和它所指向的变量之间的关系,在程序中用 * 符号表示“指向”,例如,i_pointer代表指针变量,而*i_pointer是i_pointer所指向的变量。因此,下面两个语句作用相同:
  1. i=3;
  2. *i_pointer=3;
第2个语句的含义是将3赋给指针变量i_pointer所指向的变量。

①.定义指针变量

 
定义指针变量的一般形式为:
类型说明符 *变量名;
其中,*表示这是一个指针变量,变量名是一个合法的标识符,类型说明符表示该指针变量所指向的变量的数据类型。例如:
  1. int *p1;

表示p1是一个指针变量,它的值是某个整型变量的地址。或者说p1指向一个整型变量。至于p1究竟指向哪一个整型变量,应由向p1赋予的地址来决定。再如:
  1. int *p2; //p2是指向整型变量的指针变量
  2. float *p3; //p3是指向浮点变量的指针变量
  3. char *p4; //p4是指向字符变量的指针变量

应该注意的是,一个指针变量只能指向同类型的变量,如 p3 只能指向浮点变量,不能时而指向一个浮点变量,时而又指向一个字符变量。

②.指针变量的引用

 
指针变量同普通变量一样,使用之前不仅要定义说明,而且必须赋予具体的值(初始化)。未初始化的指针变量的值是垃圾值,没有意义,不能使用,例如:
  1. #include <stdio.h>
  2. int main(){
  3. int *p;
  4. printf("%X\n", p); //十六进制输出
  5. return 0;
  6. }

运行结果:
7EFDE000
以上输出是在64位操作系统上的结果,如果你的操作系统是32位,那么输出只有4位,如23EB、983A等,为什么呢?
指针变量的赋值只能赋予地址,决不能赋予任何其它数据,否则将引起错误。在C语言中,变量的地址是由编译器分配的,对用户完全透明,用户不能控制变量的具体地址。
 
两个有关的运算符:
  1. &:取地址运算符;
  2. *:指针运算符(或称“间接访问” 运算符)。
 
C语言中提供了地址运算符&来获取变量的地址。其一般形式为:
&变量名;
&a 表示变量a的地址,&b 表示变量b的地址。变量本身必须预先定义。
 
设有指向整型变量的指针变量p,如要把整型变量 a 的地址赋予p可以有以下两种方式。
 
1) 指针变量初始化的方法:
  1. int a=10;
  2. int *p=&a;

2) 赋值语句的方法:
  1. int a=10;
  2. int *p;
  3. p=&a;

 
不允许把一个数字直接赋予指针变量,故下面的赋值是错误的:
  1. int *p;
  2. p=1000;

被赋值的指针变量前不能再加*说明符,如写为*p=&a也是错误的。假设:
  1. int i=200, x;
  2. int *ip;

我们定义了两个整型变量i、x,还定义了一个指向整型数的指针变量ip。i、x中可存放整数,而ip中只能存放整型变量的地址。我们可以把 i 的地址赋给 ip:
  1. ip=&i;

此时指针变量 ip 指向整型变量 i,假设变量 i 的地址为1800,这个赋值可形象理解为下图所示的联系:
 技术分享
 
以后我们便可以通过指针变量 ip 间接访问变量 i,例如:
  1. x = *ip;

运算符*访问以 ip 为地址的存储区域,而 ip 中存放的是变量 i 的地址,因此,*ip 访问的是地址为 1800 的存储区域(因为是整数,实际上是从1800开始的两个字节),它就是 i 所占用的存储区域,所以上面的赋值表达式等价于:
  1. x = i;

另外,指针变量和一般变量一样,存放在它们之中的值是可以改变的,也就是说可以改变它们的指向。例如:
  1. int i, j, *p1, *p2;
  2. i = ‘a‘;
  3. j = ‘b‘;
  4. p1 = &i;
  5. p2 = &j;

可建立如下图所示的联系:
 技术分享
 
这时赋值表达式:
p2=p1;
就使p2与p1指向同一对象i,此时*p2就等价于i,而不是j,图所示:
 技术分享
 
如果执行如下表达式:
  1. *p2=*p1;

则表示把p1指向的内容赋给p2所指的区域,此时就变成下图所示:
技术分享 
 
通过指针访问它所指向的变量是以间接访问的形式进行的,所以比直接访问一个变量要费时间,而且不直观,因为通过指针要访问哪一个变量,取决于指针的值(即指向),例如 *p2=*p1 ;实际上就是j=i;,前者不仅速度慢而且目的不明。但由于指针是变量,我们可以通过改变它们的指向,以间接访问不同的变量,这给程序员带来灵活性,也使程序代码编写得更为简洁和有效。
 
指针变量可出现在表达式中,设:
  1. int x, y, *px = &x;

指针变量px指向整数x,则*px可出现在x能出现的任何地方。例如:
  1. y=*px+5; //表示把x的内容加5并赋给y
  2. y=++*px; //px的内容加上1之后赋给y,++*px相当于++(*px)
  3. y=*px++; //相当于y=*px; px++

 
指针使用举例:
  1. #include <stdio.h>
  2. int main(){
  3. int a=100, b=10;
  4. int *pointer_1, *pointer_2;
  5. pointer_1 = &a;
  6. pointer_2 = &b;
  7. printf("%d, %d\n", a, b);
  8. printf("%d, %d\n", *pointer_1, *pointer_2);
  9. return 0;
  10. }

运行结果:
100, 10
100, 10
 
对程序的说明:
  1. 在开头处虽然定义了两个指针变量pointer_1和pointer_2,但它们并未指向任何一个整型变量。只是提供两个指针变量,规定它们可以指向整型变量。程序第5、6行的作用就是使pointer_1指向a,pointer_2指向b。
  2. 第8行的*pointer_1和*pointer_2就是变量a和b。最后两个printf函数作用是相同的。
  3. 程序中有两处出现*pointer_1和*pointer_2,请区分它们的不同含义。
  4. 程序第5、6行的pointer_1=&a和pointer_2=&b不能写成*pointer_1=&a和*pointer_2=&b。
 
请对下面关于“&”和“*”的问题进行考虑:
  1. 如果已经执行了“pointer_1=&a;”语句,则&*pointer_1是什么含义?
  2. *&a含义是什么?
  3. (pointer_1)++和pointer_1++的区别?
 
【示例】输入a和b两个整数,按先大后小的顺序输出。
  1. #include <stdio.h>
  2. int main(){
  3. int *max, *min,*tmp, a, b;
  4. scanf("%d, %d",&a, &b);
  5. max = &a;
  6. min = &b;
  7. if(*max < *min){
  8. // 交换指针变量的值,tmp 为临时变量
  9. tmp = max;
  10. max = min;
  11. min = tmp;
  12. }
  13. printf("a=%d, b=%d\n", a, b);
  14. printf("max=%d, min=%d\n", *max, *min);
  15. return 0;
  16. }

运行结果:
10, 20↙
a=10, b=20
max=20, min=10

3.C语言指针变量作为函数参数

【示例】输入两个整数,按大小顺序输出。
  1. #include <stdio.h>
  2. // 交换两个数
  3. void swap(int *p1, int *p2){
  4. int temp; //临时变量
  5. temp = *p1;
  6. *p1 = *p2;
  7. *p2 = temp;
  8. }
  9. int main(){
  10. int a, b;
  11. int *pointer_1, *pointer_2;
  12. scanf("%d, %d",&a, &b);
  13. pointer_1 = &a;
  14. pointer_2 = &b;
  15. if(a<b){
  16. swap(pointer_1, pointer_2);
  17. }
  18. printf("\n%d, %d\n",a, b);
  19. return 0;
  20. }

运行结果:
10, 20
20, 10
 
对程序的说明:
1) swap是自定义函数,它的作用是交换两个变量(a和b)的值。swap函数的形参p1、p2是指针变量。程序运行时,先执行main函数,输入a和b的值。然后将a和b的地址分别赋给指针变量pointer_1和pointer_2,使pointer_1指向a,pointer_2指向b。
 
2) 接着执行if语句,由于a<b,因此执行swap函数。注意实参pointer_1和pointer_2是指针变量,在函数调用时,将实参变量的值传递给形参变量。采取的依然是“值传递”方式。因此虚实结合后形参p1的值为&a,p2的值为&b。这时p1和pointer_1指向变量a,p2和pointer_2指向变量b。
 
3) 接着执行执行swap函数的函数体使*p1和*p2的值互换,也就是使a和b的值互换。函数调用结束后,p1和p2不复存在(已释放)。
 
4) 最后在main函数中输出的a和b的值是已经过交换的值。请注意交换*p1和*p2的值是如何实现的。请找出下列程序段的错误:
  1. void swap(int *p1,int *p2){
  2. int *temp;
  3. *temp=*p1; //此语句有问题
  4. *p1=*p2;
  5. *p2=temp;
  6. }

 
请考虑下面的函数能否实现实现a和b互换。
  1. void swap(int x,int y){
  2. int temp;
  3. temp=x;
  4. x=y;
  5. y=temp;
  6. }

如果在main函数中用“swap(a, b);”调用swap函数,会有什么结果呢?
 
【示例】对上面的代码进行修改。
  1. #include <stdio.h>
  2. // 交换两个数
  3. void swap(int *p1,int *p2){
  4. int *p;
  5. p = p1;
  6. p1 = p2;
  7. p2 = p;
  8. }
  9. int main(){
  10. int a, b;
  11. int *pointer_1, *pointer_2;
  12. scanf("%d, %d",&a, &b);
  13. pointer_1 = &a;
  14. pointer_2 = &b;
  15. if(a<b){
  16. swap(pointer_1, pointer_2);
  17. }
  18. printf("\n%d, %d\n",a, b);
  19. return 0;
  20. }

运行结果:
10, 20
20, 10

4.C语言指针变量的运算

①.指针运算符

指针运算符有两种:
  1. 取地址运算符&:& 是单目运算符,其结合性为自右至左,功能是取变量的地址。
  2. 取内容运算符*:* 是单目运算符,其结合性为自右至左,用来表示指针变量所指的变量。在 * 运算符之后跟的变量必须是指针变量。
 
需要注意的是指针运算符(*)和指针变量说明中的指针说明符(*)不是一回事。在指针变量说明中,“*”是类型说明符,表示其后的变量是指针类型,而表达式中出现的“*”则是一个运算符用以表示指针变量所指的变量。例如:
  1. #include <stdio.h>
  2. int main(){
  3. int a=5;
  4. int *p=&a;
  5. printf("%d\n", *p);
  6. return 0;
  7. }

运行结果:
5
 
int *p=&a;表示指针变量p取得了整型变量a的地址,printf("%d\n",*p);语句表示输出变量a的值。

②.赋值运算

 
指针变量的赋值运算有以下几种形式。
 
1) 指针变量初始化赋值,前面已作介绍。
 
2) 把一个变量的地址赋予指向相同数据类型的指针变量。例如:
  1. int a, *pa;
  2. pa=&a; //把整型变量a的地址赋予整型指针变量pa

 
3) 把一个指针变量的值赋予指向相同类型变量的另一个指针变量。如:

由于pa、pb均为指向整型变量的指针变量,因此可以相互赋值。
 
4) 把数组的首地址赋予指向数组的指针变量。例如:
  1. int a[5], *pa;
  2. pa=a; //数组名表示数组的首地址,可以赋予指向数组的指针变量pa

也可写为:
  1. pa=&a[0]; //数组第一个元素的地址也是整个数组的首地址,也可赋予pa

当然也可采取初始化赋值的方法:
  1. int a[5], *pa=a;

 
5) 把字符串的首地址赋予指向字符类型的指针变量。例如:
  1. char *pc;
  2. pc="C Language";

或用初始化赋值的方法写为:
  1. char *pc="C Language";

这里应说明的是并不是把整个字符串装入指针变量,而是把存放该字符串的字符数组的首地址装入指针变量,后面还会详细介绍。
 
6) 把函数的入口地址赋予指向函数的指针变量。例如:
  1. int (*pf)();
  2. pf = func; //func 为函数名

③.加减算术运算(针对数组)

 
对于指向数组的指针变量,可以加上或减去一个整数n。设pa是指向数组a的指针变量,则pa+n、pa-n、pa++、++pa、pa-、--pa运算都是合法的。指针变量加或减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。应该注意,数组指针变量向前或向后移动一个位置和地址加1或减1在概念上是不同的。因为数组可以有不同的类型,各种类型的数组元素所占的字节长度是不同的。如指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1。例如:
  1. int a[5],*pa;
  2. pa=a; //pa指向数组a,也是指向a[0]
  3. pa=pa+2; //pa指向a[2],即pa的值为&pa[2]

指针变量的加减运算只能对数组指针变量进行,对指向其它类型变量的指针变量作加减运算是毫无意义的。

④.两个指针变量之间的运算

 
只有指向同一数组的两个指针变量之间才能进行运算,否则运算毫无意义。
1) 两指针变量相减
 
两指针变量相减所得之差是两个指针所指数组元素之间相差的元素个数。实际上是两个指针值(地址)相减之差再除以该数组元素的长度(字节数)。例如pf1和pf2是指向同一浮点数组的两个指针变量,设pf1的值为2010H,pf2的值为2000H,而浮点数组每个元素占4个字节,所以pf1-pf2的结果为(2000H-2010H)/4=4,表示pf1和 pf2之间相差4个元素。
注意:两个指针变量不能进行加法运算。例如,pf1+pf2是什么意思呢?毫无实际意义。
2) 两指针变量进行关系运算
 
指向同一数组的两指针变量进行关系运算可表示它们所指数组元素之间的关系。例如:
  1. pf1==pf2 表示pf1和pf2指向同一数组元素;
  2. pf1>pf2 表示pf1处于高地址位置;
  3. pf1<pf2 表示pf2处于低地址位置。
 
指针变量还可以与0比较。设p为指针变量,则p==0表明p是空指针,它不指向任何变量;p!=0表示p不是空指针。
 
空指针是由对指针变量赋予0值而得到的。例如:
  1. #define NULL 0
  2. int *p = NULL;

对指针变量赋0值和不赋值是不同的。指针变量未赋值时,值是随机的,是垃圾值,不能使用的,否则将造成意外错误。而指针变量赋0值后,则可以使用,只是它不指向具体的变量而已。
 
【示例】指针运算。
  1. #include <stdio.h>
  2. int main(){
  3. //pa、pb为整型指针变量
  4. int a=10, b=20, sum, product, *pa, *pb;
  5. pa = &a; //给指针变量pa赋值,pa指向变量a
  6. pb = &b; //给指针变量pb赋值,pb指向变量b
  7. sum = *pa + *pb; //求a+b之和,*pa就是a,*pb就是b
  8. product = *pa * *pb; //本行是求a*b之积
  9. printf("a=%d\nb=%d\na+b=%d\na*b=%d\n", a, b, a+b, a*b);
  10. printf("s=%d\nt=%d\n", sum, product);
  11. return 0;
  12. }

运行结果:
a=10
b=20
a+b=30
a*b=200
s=30
t=200

4.C语言数组指针

一个变量有一个地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。所谓数组的指针是指数组的起始地址,数组元素的指针是数组元素的地址。
字节是内存中的最小可操作单位,我们通常将一个字节称为一个内存单元。
一个数组是由连续的一块内存单元组成的。数组名就是这块连续内存单元的首地址。一个数组也是由各个数组元素(下标变量)组成的。每个数组元素按其类型不同占有几个连续的内存单元。一个数组元素的首地址也是指它所占有的几个内存单元的首地址。
 
定义一个指向数组元素的指针变量的方法,与以前介绍的指针变量相同。例如:
  1. int a[10]; //定义a为包含10个整型数据的数组
  2. int *p; //定义p为指向整型变量的指针

应当注意,因为数组为int型,所以指针变量也应为指向int型的指针变量。下面是对指针变量赋值:
  1. p=&a[0];

把a[0]元素的地址赋给指针变量p。也就是说,p指向a数组的第0个元素。
 技术分享
 
C语言规定,数组名代表数组的首地址,也就是第0号元素的地址。因此,下面两个语句等价:
  1. p=&a[0];
  2. p=a;

在定义指针变量时可以赋给初值:
  1. int *p=&a[0];

它等效于:
  1. int *p;
  2. p=&a[0];

当然定义时也可以写成:
  1. int *p=a;

 
从图中我们可以看出有以下关系:p、a、&a[0]均指向同一单元,它们是数组a的首地址,也是第 0 个元素a[0]的首地址。应该说明的是p是变量,而a、&a[0]都是常量,在编程时应予以注意。

①.通过指针引用数组

 
C语言规定:如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素。
 
引入指针变量后,就可以用两种方法来访问数组元素了。
 
如果p的初值为&a[0],则:
  1. p+i 和 a+i 就是 a[i] 的地址,或者说它们指向a数组的第 i 个元素。
  2. *(p+i) 或 *(a+i) 就是 p+i 或 a+i 所指向的数组元素(的内容),即a[i]。例如,*(p+5) 或*(a+5) 就是 a[5]。
  3. 指向数组的指针变量也可以带下标,如 p[i] 与 *(p+i) 等价。
 
根据以上叙述,引用一个数组元素可以用:
  1. 下标法:即用 a[i] 形式访问数组元素,在前面介绍数组时都是采用这种方法。
  2. 指针法:即采用 *(a+i) 或 *(p+i) 形式,用间接访问的方法来访问数组元素,其中a是数组名,p是指向数组的指针变量,它们的值相同。
【示例①】输出数组中的全部元素(下标法)。
  1. #include <stdio.h>
  2. int main(){
  3. int a[10], i;
  4. for(i=0; i<10; i++)
  5. a[i]=i;
  6. for(i=0;i<10;i++)
  7. printf("a[%d]=%d\n", i, a[i]);
  8. return 0;
  9. }

 
【示例②】输出数组中的全部元素(通过数组名计算元素的地址,找出元素的值)。
  1. #include <stdio.h>
  2. int main(){
  3. int a[10], i;
  4. for(i=0; i<10; i++)
  5. *(a+i) = i;
  6. for(i=0; i<10; i++)
  7. printf("a[%d]=%d\n", i, *(a+i));
  8. return 0;
  9. }

 
【示例③】输出数组中的全部元素(用指针变量指向元素)。
  1. #include <stdio.h>
  2. int main(){
  3. int a[10], i, *p;
  4. p = a;
  5. for(i=0; i<10; i++)
  6. *(p+i) = i;
  7. for(i=0; i<10; i++)
  8. printf("a[%d]=%d\n", i, *(p+i));
  9. return 0;
  10. }


 

②.几个注意的问题:

1) 指针变量可以实现本身的值的改变。如p++是合法的;而a++是错误的。因为a是数组名,它是数组的首地址,是常量。
 
2) 要注意指针变量的当前值。请找出下面程序的错误:
  1. #include <stdio.h>
  2. int main(){
  3. int *p, i, a[10];
  4. p=a;
  5. for(i=0; i<10; i++)
  6. *p++=i;
  7. for(i=0; i<10; i++)
  8. printf("a[%d]=%d\n", i, *p++);
  9. return 0;
  10. }

改正:
  1. #include <stdio.h>
  2. int main(){
  3. int *p, i, a[10];
  4. p=a;
  5. for(i=0; i<10; i++)
  6. *p++=i;
  7. p=a;
  8. for(i=0; i<10; i++)
  9. printf("a[%d]=%d\n", i, *p++);
  10. return 0;
  11. }

 
3) 从上例可以看出,虽然定义数组时指定它包含10个元素,但指针变量可以指到数组以后的内存单元,系统并不认为非法。
 
4) *p++,由于++和*同优先级,结合方向自右而左,等价于*(p++)。
 
5) *(p++)与*(++p)作用不同。若p的初值为a,则*(p++)等价a[0],*(++p)等价a[1]。
 
6) (*p)++表示p所指向的元素值加1。
 
7) 如果p当前指向a数组中的第i个元素,则:
*(p--)相当于a[i--];
*(++p)相当于a[++i];
*(--p)相当于a[--i]。

5.C语言用数组作函数参数

用普通变量做函数参数,形参和实参位于不同的内存区域,发生函数调用时,会把实参的值传递给形参,改变形参的值不会影响到实参,它们是相互独立的。这称为按值传递。
 
在用数组名作函数参数时,不是进行值的传送,不会把实参数组的每一个元素的值都赋予形参数组的各个元素。因为实际上形参数组并不存在,编译系统不为形参数组分配内存。那么,数据的传送是如何实现的呢?数组名就是数组的首地址,用数组名作函数参数时所进行的传送只是地址的传送,也就是说把实参数组的首地址赋予形参数组名。形参数组名取得该首地址之后,也就等于有了实实在在的数组。实际上是形参数组和实参数组为同一数组,拥有同一段内存空间。这种传值方式成为按引用传递。
技术分享
 
 
图中设a为实参数组,类型为int。a占有以2000为首地址的一块内存区;b为形参数组名。当发生函数调用时,进行地址传送,把实参数组a的首地址传送给形参数组名b,于是b也取得该地址2000。于是a,b两数组共同占有以2000为首地址的一段连续内存单元。从图中还可以看出a和b下标相同的元素实际上也占有相同的一块内存(整型数组每个元素占4个字节),例如a[0]和b[0]都占用2000、2001、2002、2003四个字节。
 
【示例】数组a中存放了一个学生5门课程的成绩,求平均成绩。
  1. #include <stdio.h>
  2. float aver(float a[5]){
  3. int i;
  4. float average, sum=a[0];
  5. for(i=1; i<5; i++)
  6. sum += a[i];
  7. average = sum/5;
  8. return average;
  9. }
  10. int main(){
  11. float scores[5], average;
  12. int i;
  13. printf("Input 5 scores:\n");
  14. for(i=0; i<5; i++)
  15. scanf("%f", &scores[i]);
  16. average = aver(scores);
  17. printf("Average score is %5.2f", average);
  18. return 0;
  19. }

注意:形参中给出数组长度是没有意义的,编译器并不为它分配内存,将上面 aver 函数的形参改为 float a[1]、float a[10] 依然是正确的。所以一般用指针变量来代替,可以改为float *a。
用普通变量作函数参数时,所进行的值传递是单向的,只能从实参传向形参,不能从形参传回实参。形参的初值和实参相同,而形参的值发生改变后,实参并不变化,两者的终值是不同的。而当用数组作函数参数时,情况有所不同。由于实际上形参和实参为同一数组,因此当形参数组发生变化时,实参数组也随之变化。当然这种情况不能理解为发生了“双向”的值传递。但从实际情况来看,调用函数之后实参数组的值将由于形参数组值的变化而变化。
 
【示例】判别一个整数数组中各元素的值,若大于0 则输出该值,若小于等于0则输出0值。
  1. #include <stdio.h>
  2. void nzp(int *a){
  3. int i;
  4. for(i=0; i<5; i++){
  5. if(a[i]<0) a[i]=0; //小于0的元素,赋值为0
  6. }
  7. }
  8. int main(){
  9. int b[5], i;
  10. printf("Input 5 numbers:\n");
  11. for(i=0; i<5; i++)
  12. scanf("%d", &b[i]);
  13. printf("Initial values of array b are: ");
  14. for(i=0; i<5; i++)
  15. printf("%d ", b[i]);
  16. nzp(b);
  17. printf("\nFinal values of array b are: ");
  18. for(i=0; i<5; i++)
  19. printf("%d ",b[i]);
  20. return 0;
  21. }

运行结果:
Input 5 numbers:
1↙
3↙
-90↙
-23↙
100↙
Initial values of array b are: 1 3 -90 -23 100
Final values of array b are: 1 3 0 0 100
 
总结:
1) 形参数组和实参数组的类型必须一致,否则将引起错误。
 
2) 形参数组和实参数组的长度可以不相同,因为在调用时,只传送首地址而不检查形参数组的长度。
 
3) 在函数形参表中,允许不给出形参数组的长度,例如可以写为:
  1. void nzp(int a[])

也可以用指针来代替:
  1. void nzp(int *a)

6.C语言字符串指针

在C语言中,可以通过字符数组存放一个字符串,也可以用字符指针指向一个字符串。
 
【示例】用字符数组存放一个字符串,然后输出该字符串。
  1. #include <stdio.h>
  2. int main(){
  3. char string[] = "I love China!";
  4. printf("%s\n", string);
  5. return 0;
  6. }

和前面介绍的数组属性一样,string是数组名,它代表字符数组的首地址。
 
【示例】用字符串指针指向一个字符串。
  1. #include <stdio.h>
  2. int main(){
  3. char *string = "I love China!";
  4. printf("%s\n", string);
  5. return 0;
  6. }

对指向字符变量的指针变量应赋予该字符变量的地址。如:
  1. char c, *p=&c;

表示p是一个指向字符变量c的指针变量。而:
  1. char *s="C Language";

则表示s是一个指向字符串的指针变量。把字符串的首地址赋予s。
 
上例中,首先定义string是一个字符指针变量,然后把字符串的首地址赋予string(应写出整个字符串,以便编译器把它装入一块连续的内存单元)。程序中的:
  1. char *string = "I love China!";

等效于:
  1. char *string;
  2. string = "I love China!";

 
【示例】输出字符串中n个字符后的所有字符。
  1. #include <stdio.h>
  2. int main(){
  3. char *ps = "This is a good tutorial!";
  4. int n=10;
  5. ps=ps+n;
  6. printf("%s\n", ps);
  7. return 0;
  8. }

运行结果为:
good tutorial!
 
在程序中对ps初始化时,把字符串首地址赋予ps,当ps= ps+10之后,ps指向字符‘b‘,因此输出为 "good tutorial!"。
 
【示例】在输入的字符串中查找有无‘k’字符。
  1. #include <stdio.h>
  2. int main(){
  3. char str[20], *ps;
  4. int i;
  5. printf("Input a string: ");
  6. ps = str;
  7. scanf("%s", ps);
  8. for(i=0; ps[i]!=‘\0‘; i++){
  9. if(ps[i]==‘k‘){
  10. printf("There is a ‘k‘ in the string!\n");
  11. break;
  12. }
  13. }
  14. if(ps[i]==‘\0‘) printf("There is no ‘k‘ in the string\n");
  15. return 0;
  16. }

运行结果:
Input a string: thank you
There is a ‘k‘ in the string!
 
【示例】不使用 strcpy 函数实现字符串的复制。
  1. #include <stdio.h>
  2. void cpystr(char *pss, char *pds){
  3. while((*pds=*pss)!=‘\0‘){
  4. pds++;
  5. pss++;
  6. }
  7. }
  8. int main(){
  9. char *pa = "Apple, Samsung, Xiaomi, Smartisan", b[100], *pb;
  10. pb=b;
  11. cpystr(pa, pb);
  12. printf("string a=%s\nstring b=%s\n",pa, pb);
  13. return 0;
  14. }

运行结果:
string a=Apple, Samsung, Xiaomi, Smartisan
string b=Apple, Samsung, Xiaomi, Smartisan
 
函数 cprstr 的形参为两个字符指针变量,pss 指向源字符串,pds 指向目标字符串。注意表达式(*pds=*pss)!=`\0‘的用法。
 
本例中程序完成了两项工作:一是把pss指向的源字符串复制到pds所指向的目标字符串中,二是判断所复制的字符是否为`\0‘,若是则表明源字符串结束,不再循环。否则,pds和pss都加1,指向下一字符。
 
在主函数中,以指针变量pa、pb为实参,分别取得确定值后调用cprstr 函数。由于采用的指针变量pa和pss、pb和pds均指向同一字符串,因此在主函数和cprstr函数中均可使用这些字符串。也可以把cprstr函数简化为以下形式:
  1. void cprstr(char *pss,char*pds){
  2. while( (*pds++=*pss++) != ‘\0‘ );
  3. }

即把指针的移动和赋值合并在一个语句中。 进一步分析还可发现 `\0‘ 的ASCII码为0,对于while语句只看表达式的值为非0就循环,为0则结束循环,因此也可省去!=‘\0‘这一判断部分,而写为以下形式:
  1. void cprstr(char *pss,char*pds){
  2. while(*pds++ = *pss++);
  3. }

表达式的意义可解释为,源字符向目标字符赋值,移动指针,若所赋值为非 0 则循环,否则结束循环。这样使程序更加简洁。

7.C语言指针型函数和函数指针变量

①.指针型函数

 
前面我们介绍过,所谓函数类型是指函数返回值的类型。在C语言中允许一个函数的返回值是一个指针(即地址),这种返回指针值的函数称为指针型函数。定义指针型函数的一般形式为:

函数名之前加了*号表明这是一个指针型函数,它的返回值是一个指针。如:
  1. #include <stdio.h>
  2. // 返回两个字符串中较长的一个
  3. char *func(char *str1, char *str2){
  4. if(strlen(str1) >= strlen(str2)){
  5. return str1;
  6. }else{
  7. return str2;
  8. }
  9. }
  10. int main(){
  11. char *s1 = "C Language";
  12. char *s2 = "C is very great!";
  13. char *longstr = func(s1, s2);
  14. printf("Long string: %s\n", longstr);
  15. return 0;
  16. }

运行结果:
Long string: C is very great!
 
func 是一个指针型函数,它返回的指针指向一个字符串。

②.函数指针变量(选读)

 
在C语言中,一个函数总是占用一段连续的内存区域,函数名就是该函数所占内存区域的首地址。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使该指针变量指向该函数,然后通过指针变量就可以找到并调用这个函数。我们把这种指向函数的指针变量称为函数指针变量。
 
函数指针变量定义的一般形式为:
  1. 数据类型 (*指针变量名)();

数据类型表示被指函数的返回值的类型,(*指针变量名)表示 * 后面的变量是指针变量,最后的空括号表示指针变量所指的是一个函数。例如:
int (*pf)();
表示 pf 是一个指向函数入口的指针变量,该函数的返回值是整型。
 
【示例】用指针形式实现对函数的调用。
  1. #include <stdio.h>
  2. // 返回两个值中较大的值
  3. int max(int a, int b){
  4. if(a>b) return a;
  5. else return b;
  6. }
  7. int main(){
  8. int(*pmax)();
  9. int x, y, maxval;
  10. pmax = max;
  11. printf("Input two numbers:");
  12. scanf("%d %d", &x, &y);
  13. maxval = (*pmax)(x, y);
  14. printf("Max value: %d\n", maxval);
  15. return 0;
  16. }

运行结果:
Input two numbers:100 200
Max value: 200
 
从上述程序可以看出用,函数指针变量形式调用函数的步骤如下:
  1. 先定义函数指针变量,第10行int(*pmax)();定义 pmax 为函数指针变量。
  2. 把被调函数的入口地址(函数名)赋予该函数指针变量,如程序中第12行pmax=max;
  3. 用函数指针变量形式调用函数,如程序第15行z=(*pmax)(x,y);
  4. 调用函数的一般形式为:(*指针变量名) (实参列表)
 
使用函数指针变量还应注意以下两点:
  1. 函数指针变量不能进行算术运算,这是与数组指针变量不同的。数组指针变量加减一个整数可使指针移动指向后面或前面的数组元素,而函数指针的移动是毫无意义的。
  2. 函数调用中(*指针变量名)的两边的括号不可少,其中的*不应该理解为求值运算,在此处它只是一种表示符号。

8.C语言指针与二维数组

设有整型二维数组a[3][4]如下:
  1. 0 1 2 3
  2. 4 5 6 7
  3. 8 9 10 11

它的定义为:
  1. int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}};

二维数组在概念上是二维的,但在内存中地址是连续的,也就是说内存单元是按一维线性排列的。在C语言中,二维数组是按行排列的。也就是先存放a[0]行,再存放a[1]行,最后存放a[2]行;每行中的四个元素也是依次存放。数组a为int类型,每个元素占用4个字节,整个数组共占用4×(3×4)=48个字节。
 
C语言允许把一个二维数组分解为多个一维数组来处理。因此数组a可分解为三个一维数组,即a[0]、a[1]、a[2]。每一个一维数组又含有四个元素,例如a[0]数组,含有a[0][0],a[0][1],a[0][2],a[0][3]四个元素。
 
设数组a的首地址为1000,那么每个一维数组的首地址如下图所示:
 技术分享
 
 
a是二维数组名,a代表整个二维数组的首地址,也是二维数组0行的首地址,等于1000。a+1代表第一行的首地址,等于1016。如图:
 技术分享
 
a[0]是第一个一维数组的数组名和首地址,因此也为1000。*(a+0)或*a是与 a[0] 等效的, 它表示一维数组a[0]中第 0 个元素的首地址,也为1000。&a[0][0]是二维数组a的0行0列元素首地址,同样是1000。因此,a、a[0]、*(a+0)、*a、&a[0][0]是相等的。
 
同理,a+1是二维数组1行的首地址,等于1016。a[1]是第二个一维数组的数组名和首地址,因此也为1016。&a[1][0]是二维数组a的1行0列元素地址,也是1016。因此a+1、a[1]、*(a+1)、&a[1][0]是等同的。
 
由此可得出:a+i,a[i],*(a+i),&a[i][0]是等同的。
 
此外,&a[i]和a[i]也是等同的。因为在二维数组中不能把&a[i]理解为元素a[i]的地址,不存在元素a[i]。C语言规定,它是一种地址计算方法,表示数组a第i行首地址。由此,我们得出:a[i],&a[i],*(a+i)和a+i也都是等同的。
 
另外,a[0]也可以看成是a[0]+0,是一维数组a[0]的0号元素的首地址,而a[0]+1则是a[0]的第1个元素首地址,由此可得出a[i]+j则是一维数组a[i]的j号元素首地址,它等于&a[i][j]。
 
由a[i]=*(a+i)得a[i]+j=*(a+i)+j。由于*(a+i)+j是二维数组a的i行j列元素的首地址,所以,该元素的值等于*(*(a+i)+j)。
 
【示例】二维数组举例。
  1. #include <stdio.h>
  2. int main(){
  3. int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
  4. printf(" a=%d,",a);
  5. printf(" *a=%d,",*a);
  6. printf(" a[0]=%d,",a[0]);
  7. printf(" &a[0]=%d,",&a[0]);
  8. printf(" &a[0][0]=%d\n",&a[0][0]);
  9. printf("a+1=%d,",a+1);
  10. printf(" *(a+1)=%d,",*(a+1));
  11. printf(" a[1]=%d,",a[1]);
  12. printf(" &a[1]=%d,",&a[1]);
  13. printf(" &a[1][0]=%d\n",&a[1][0]);
  14. printf("a+2=%d,",a+2);
  15. printf(" *(a+2)=%d,",*(a+2));
  16. printf(" a[2]=%d,",a[2]);
  17. printf(" &a[2]=%d,",&a[2]);
  18. printf(" &a[2][0]=%d\n\n",&a[2][0]);
  19. printf(" a[1]+1=%-8d,",a[1]+1);
  20. printf(" *(a+1)+1=%-8d\n",*(a+1)+1);
  21. printf("*(a[1]+1)=%-8d,",*(a[1]+1));
  22. printf(" *(*(a+1)+1)=%-8d\n",*(*(a+1)+1));
  23. return 0;
  24. }

运行结果:
  a=2686736,     *a=2686736, a[0]=2686736, &a[0]=2686736, &a[0][0]=2686736
a+1=2686752, *(a+1)=2686752, a[1]=2686752, &a[1]=2686752, &a[1][0]=2686752
a+2=2686768, *(a+2)=2686768, a[2]=2686768, &a[2]=2686768, &a[2][0]=2686768
 
   a[1]+1=2686756 ,    *(a+1)+1=2686756
*(a[1]+1)=5       , *(*(a+1)+1)=5

①.指针数组

 
指针数组中每个元素的值都为指针。一般的定义形式为:
  1. 数据类型 *数组名[数组长度];

用 pa 表示上面的二维数组:
  1. int *pa[3] = { a[0], a[1], a[2] };

pa是一个指针数组,它有3个元素,每个元素值都是一个指针,指向整型变量。
 
【示例】指针数组的简单应用。
  1. #include <stdio.h>
  2. int main(){
  3. int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
  4. int *pa[3]={a[0], a[1], a[2]}; //也可以不指定长度,写作 int*pa[]
  5. int *p=a[0]; //整型指针
  6. printf("%d, %d, %d\n", a[1][2], *a[1], *(*(a+1)+2));
  7. printf("%d, %d, %d\n", *pa[1], p[2], *(p+2));
  8. return 0;
  9. }

运行结果:
6, 4, 6
4, 2, 2
 
pa是一个指针数组,三个元素分别指向二维数组a的各行。其中*a[1]表示1行0列元素值;*(*(a+1)+2)表示1行2列的元素值;*pa[1]表示1行0列元素值;由于p与a[0]相同,故p[2]表示0行2列的值;*(p+2)表示0行2列的值。
拓展阅读
一道题目,玩转C语言指针数组和指向指针的指针
指向二维数组的指针(选读)
 
把上面的二维数组a分解为一维数组a[0]、a[1]、a[2]之后,设p为指向二维数组的指针变量,可定义为:
  1. int (*p)[4];

它表示p是一个指针变量,它指向包含4个元素的一维数组。若指向第一个一维数组a[0],其值等于a,a[0],或&a[0][0]等。而p+i则指向一维数组a[i]。从前面的分析可得出*(p+i)+j是二维数组i行j 列的元素的地址,而*(*(p+i)+j)则是i行j列元素的值。
 
二维数组指针变量定义的一般形式为:
  1. 数据类型 (*指针变量名)[长度];

*表示其后的变量是指针类型,长度表示二维数组分解为多个一维数组时,一维数组的长度,也就是二维数组的列数。应注意(*指针变量名)两边的括号不可少,如缺少括号则表示是指针数组,意义就完全不同了。
 
【示例】输出二维数组。
  1. #include <stdio.h>
  2. int main(){
  3. int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
  4. int(*p)[4];
  5. int i,j;
  6. p=a;
  7. for(i=0; i<3; i++){
  8. for(j=0; j<4; j++) printf("%2d ",*(*(p+i)+j));
  9. printf("\n");
  10. }
  11. return 0;
  12. }

运行结果:
 0   1   2   3
 4   5   6   7
 8   9  10  11
指针数组和二维数组指针变量的区别
 
两者虽然都可用来表示二维数组,但是其表示方法和意义是不同的。
 
二维数组指针变量是单个的变量,其一般形式中(*指针变量名)两边的括号不可少。而指针数组类型表示的是多个指针(一组有序指针)在一般形式中*指针数组名两边不能有括号。例如:
int (*p)[3];
表示一个指向二维数组的指针变量。该二维数组的列数为3或分解为一维数组的长度为3。
int *p[3];
表示p是一个指针数组,有三个下标变量p[0],p[1],p[2]均为指针变量。
 
指针数组也常用来表示一组字符串,这时指针数组的每个元素被赋予一个字符串的首地址。

如果一个指针变量存放的又是另一个指针变量的地址,则称这个变量为指向指针的指针变量或指向指针的指针。如下图所示:
 
 
a 为 int 型变量,值为100;p1 指向 a,它的值是 a 的地址;p2 为指向指针的指针,它的值是 p1 的地址。
 

9.C语言指向指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个变量为指向指针的指针变量或指向指针的指针。如下图所示:
 技术分享
 
a 为 int 型变量,值为100;p1 指向 a,它的值是 a 的地址;p2 为指向指针的指针,它的值是 p1 的地址。
 
指向指针的指针定义方式一般为:
  1. 数据类型 **变量名;

例如:
int a =100;
int *p1 = &a;
int **p2 = &p1;
int **p2;可以理解为int *(*p2);。显然 *p2 是指针变量,如果前面没有 *,就是定义了一个指针整型数据的指针变量。现在它前面又有了一个 * 号,表示 p2 指向的是一个整型指针变量。*p2 就是 p2 所指向的另一个指针变量。
 
请看下面的输出结果:
  1. #include <stdio.h>
  2. int main(){
  3. int a =100;
  4. int *p1 = &a;
  5. int **p2 = &p1;
  6. printf("a=%d, p1=%X, &p1=%X\n", a, p1, &p1);
  7. printf("**p2=%d, *p2=%X, p2=%X\n", **p2, *p2, p2);
  8. return 0;
  9. }

运行结果:
a=100,    p1=28FF44,  &p1=28FF40
**p2=100, *p2=28FF44, p2=28FF40
 
指向指针的指针常用在二维数组中。请看下面的例子:
  1. #include <stdio.h>
  2. int main(){
  3. char *name[]={
  4. "Follow me",
  5. "BASIC",
  6. "Great Wall",
  7. "FORTRAN",
  8. "Computer desighn"
  9. };
  10. char **p=name;
  11. printf("*p=%s\n", *p);
  12. printf("*(p+1)=%s\n", *(p+1));
  13. printf("**(p+1)=%c\n", **(p+1));
  14. printf("**(p+1)=%c\n", *(*(p+2)+3));
  15. return 0;
  16. }

运行结果:
*p=Follow me
*(p+1)=BASIC
**(p+1)=B
**(p+1)=a

10.关于指针的总结

指针(Pointer)就是内存单元的地址。在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。
 
定义含义
int i;
定义整型变量 i。
int *p;
p为指向整型数据的指针变量。
int a[n];
定义整型数组a,它有n个元素。
int *p[n];
定义指针数组p,它由n个指向整型数据的指针元素组成
int (*p)[n];    
p为指向含n个元素的一维数组的指针变量。
int f();
f 为一个返回整型的函数。
int *p();
p为一个返回指针的函数,该指针指向整型数据。
int (*p)();
p为指向函数的指针,该函数返回一个整型值。
int **p;
p是一个指针变量,它又指向另外一个指针变量,该指针变量指向整型数据。
1) 指针变量可以加(减)一个整数,例如p++、p+i、p-=i。
 
一个指针变量加(减)一个整数并不是简单地将原值加(减)一个整数,而是将该指针变量的原值(是一个地址)和它指向的变量所占用的内存单元字节数加(减)。
 
2) 指针变量赋值:将一个变量的地址赋给一个指针变量。
  1. p=&a; //将变量a的地址赋给p
  2. p=array; //将数组array的首地址赋给p
  3. p=&array[i]; //将数组array第i个元素的地址赋给p
  4. p=max; //max为已定义的函数,将max的入口地址赋给p
  5. p1=p2; //p1和p2都是指针变量,将p2的值赋给p1

注意,不能将一个数值直接赋给指针变量,例如p=1000;是没有意义的,一般会引起程序崩溃。
 
3) 指针变量可以有空值,即该指针变量不指向任何变量,如p=NULL;。
 
4) 两个指针变量可以相减:如果两个指针变量指向同一个数组的元素,则两个指针变量值之差是两个指针之间的元素个数。


来自为知笔记(Wiz)


C语言指针