一、指针简介
从根本上看,指针(pointer)是一个值为内存地址的变量(或数据对象)。正如char类型变量的值是字符,int类型变量的值是整数,指针变量的值是地址。在C语言中,指针有许多用法。接下来要介绍如何把指针作为函数参数使用,以及为何这样用。
假设一个指针变量名是ptr,可以编写如下语句:
ptr=&pooh; //将pooh的地址赋给ptr
要创建指针变量,先要声明指针变量的类型。假设想把ptr声明为储存int类型变量地址的指针,就要使用下面介绍的新运算符。
1.1 间接运算符 *
假设已知ptr指向bah,如下所示:
ptr=&bah;
我们就可以使用间接运算符*找出储存在bah中的值,该运算符有时也称为解引用运算符:
val=*ptr; //找出ptr指向的值
1.2 声明指针
声明指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。另外,程序必须知道储存在指定地址上的数据类型。long和float可能占用相同的存储空间,但是它们储存数据却大相径庭。下面是一些指针的声明示例:
int * pi; //pi是指向int类型变量的指针
char * pc; //pc是指向char类型变量的指针
float * pf,* pg; //pf、pg都是指向float类型变量的指针
类型说明符表明了指针所指向对象的类型,*表明声明的变量是一个指针,int *pi
的含义是pi为一个指针,*pi
是int类型。
*和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。
1.3 使用指针在函数间通信
观察程序9.15,该程序在interchange()函数中使用了指针参数。接下来对这个程序做详细分析:
//程序 9.15 swap3.c程序
#include<stdio.h>
void interchange(int * u,int * v);
int main(void){
int x=5,y=10;
printf("Originally x = %d and y = %d.\n",x,y);
interchange(&x,&y);
printf("Now x = %d and y = %d.\n",x,y);
return 0;
}
void interchange(int *u,int *v){
int temp;
temp=*u;
*u=*v;
*v=temp;
}
程序输出为:
Originally x = 5 and y = 10.
Now x = 10 and y = 5.
二、指针和数组
数组表示法其实是在变相地使用指针,举一个例子:数组名是数组首元素的地址,也就是说,如果flizny是一个数组,下面的语句成立:
flizny == &flizny[0];
flizny和&flizny[0]都表示数组首元素的内存地址。两者都是常量,在程序的运行过程中,不会改变。但是,可以把它们赋值给指针变量,然后可以修改指针变量的值,如程序10.8所示(注意转换说明%p通常以十六进制显示指针的值):
//程序10.8 pnt_add.c程序 --指针地址
#include<stdio.h>
#define SIZE 4
int main(void){
short dates[SIZE];
short * pti;
short index;
double bills[SIZE];
double * ptf;
pti=dates;
ptf=bills;
printf(%23s %15s\n","short","double");
for(index=0;index<SIZE,index++)
printf("pointers + %d: %10p %10p\n",index,pti+index,ptf+index);
return 0;
}
下面是这个例子的输出示例:
在系统中,地址按照字节编址,short类型占用2字节,double类型占用8字节。在C中,指针+1指的是增加一个存储单元。对数组而言,这意味着+1后的地址是下一个元素的地址,而不是下一个字节的地址,如图10.3所示:
现在可以更清楚地定义指向int的指针、指向float的指针,以及指向其他数据对象的指针:
- 指针的值是它所指向对象的地址。地址的表示方式依赖于计算机内部的硬件。许多计算机都是按字节编址,意思是内存中的每个字节都按顺序编号。这里,一个较大对象的地址(如double类型的变量)通常是该对象第一个字节的地址。
- 在指针前面使用*运算符可以得到该指针所指向对象的值
- 指针+1,指针的值递增它所指向类型的大小(以字节为单位)
下面的等式体现了C语言的灵活性:
dates+2==&date[2]
*(data+2)==date[2]
需要注意不要混淆*(date+2)和*date+2。间接运算符(*)的优先级高于+,所以*date+2相当于(*date)+2
*(date+2) //date第三个元素的值
*date+2 //date第一个元素的值+2
三、函数、数组和指针
假设要编写一个处理数组的函数,该函数返回数组中所有元素之和,待处理的名为marbles的int类型数组。我们可以通过如下的方式调用函数:
total=sum(marbles);
接下来我们需要定义函数原型,注意,数组名是该数组首元素的地址,所以实际参数marbles是一个储存int类型值的地址,应把它赋给一个指针形式参数,即该形参是一个指向int的指针:
int sum(int * ar); //对应的函数原型
sum函数从该参数获取到了数组首元素的地址,知道要在该位置上找出一个整数。注意,该参数并未包含数组元素个数的信息。有两种方式让函数获得这一信息。第一种方法是,在函数代码中写上固定的数组大小:
int sum(int * ar){
int i;
int total = 0;
for(i=0;i<10;i++)
total+=ar[i];
return total;
}
但是上面函数的定义有限制,只能计算10个int类型的元素。另外一个比较灵活的方法是把数组大小作为第二个参数:
int sum(int * ar,int n){
int i;
int total=0;
for(i=0;i<n;i++)
total+=ar[i];
return total;
}
这里,第一个形参告诉函数该数组的地址和数据类型,第二个形参告诉函数该数组中元素的个数。
关于函数的形参,还有一点需要注意。只有在函数原型后函数定义头中,才可以用int ar[]代替int * ar:
int sum(int ar[], int n);
int *ar形式和int ar[]形式都表示ar是一个指向int的指针。但是,int ar[]只能用于声明形式参数。第二种形式(int ar[])提醒读者指针ar指向的不仅仅是一个int类型值,还是一个int类型数组的元素。
3.1 使用指针形参
函数要处理数组必须知道何时开始、何时结束。sum()函数使用一个指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。但是这并不是给函数传递必备信息的唯一方法。还有一种方法是传递两个指针,第一个指针指明数组的开始处(与前面用法相同),第二个指针指明数组的结束处。程序10.11演示了这种方法,同时该程序也表明了指针形参是变量,这意味着可以用索引表访问数组中的哪一个元素:
#include<stdio.h>
#define SIZE 10
int sump(int * start,int * end);
int main(void){
int marbles[SIZE]={20,10,5,39,4,16,19,26,31,20};
long answer;
answer=sump(marbles,marbles+SIZE);
printf("The total number of marbles is %ld.\n",answer);
return 0;
}
int sump(int *start,int *end){
int total=0;
while(start<end){
total+=*start;
start++;
}
return total;
}
这里sump()函数使用第二个指针来结束循环:
while(start<end)
因为while循环的测试条件是一个不相等的关系,所以循环最后处理的一个元素是end所指向位置的前一个元素。这意味着end指向的位置实际上在数组最后一个元素的后面。C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是一个有效的指针。这使得while循环的测试条件是有效的,因为start在循环中最后的值是end。注意,使用这种“越界”指针的函数调用更为简洁:
answer=sump(marbles,marbles+SIZE);
因为下标从0开始,所以marbles+SIZE指向数组末尾的下一个位置。如果end指向数组的最后一个元素而不是数组末尾的下一个位置,则必须使用下面的代码:
answer=sump(marbles,marbles+SIZE-1);
这种写法不简洁也不好记,很容易导致编程错误。顺带一提,虽然C保证了marbles+SIZE有效,但是对marles[SIZE](即储存在该位置上的值)未作任何保证,所以程序不能访问该位置。
还可以把循环体压缩成一行代码:
total+=start++;
一元运算符和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是start。也就是说,指针start先递增后指向。使用后缀形式(即start++而不是++start)意味着先把指针指向位置上的值加到total上,然后再递增指针。如果使用*++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使用(*start)++,则先使用start指向的值,再递增该值,而不是递增指针。这样,指针将一直指向同一个位置,但是该位置上的值发生了变化。虽然*start++的写法比较常用,但是*(start++)这样写更清楚。
程序10.12演示了这些优先级的情况:
3.2 指针表示法和数组表示法
ar[i]和*(ar+1)两个表达式是等价的。
四、指针操作
C提供了一些基本的指针操作,程序10.13演示了8种不同的操作。为了显示每种操作的结果,该程序打印了指针的值(该指针指向的地址)、储存在指针指向地址上的值,以及指针自己的地址:
//程序10.13 prt_ops.c --指针操作
#include<stdio.h>
int main(void){
int urn[5]={100,200,300,400,500};
int *ptr1,*ptr2,*ptr3;
ptr1=urn;
ptr2=&urn[2];
printf("pointer value, dereferenced pointer, pointer address:\n");
printf("ptr1=%p, *ptr1=%d,&ptr1=%p\n",ptr1,*ptr1,&ptr1);
//指针加法
ptr3=ptr1+4;
printf("\nadding an int to a pointer:\n");
printf("ptr1+4=%p,*(ptr1+4)=%d\n",ptr1+4,*(ptr1+4));
ptr1++;
printf("\nvalues after ptr1++:\n");
printf("ptr1=%p,*ptr1=%d,&ptr1=%p\n",ptr1,*ptr1,&ptr1);
ptr2--;
printf("\nvalues after ptr2--:\n");
printf("ptr2=%p,*ptr2=%d,&ptr2=%p\n",ptr2,*ptr2,&ptr2);
--ptr1; //恢复为初始值
++ptr2; //恢复为初始值
printf("\nPointers reset to original values:\n");
printf("ptr1=%p,ptr2=%p\n",ptr1,ptr2);
//一个指针减去另一个指针
printf("\nsubtracting one pointer from another:\n");
printf("ptr2=%P,ptr1=%p,ptr2-ptr1=%td\n",ptr2,ptr1,ptr2-ptr1);
//一个指针减去一个整数
printf("\nsubstracting an int from a pointer:\n");
printf("ptr3=%p,ptr3-2=%p\n",ptr3,ptr3-2);
return 0;
}
系统执行后输出如下:
接下来描述下指针变量的基本操作:
- 赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。在该例中,把urn的数组的首地址赋给了ptr1,该地址的编号为0x7fff5bff8d0。变量ptr2获得数组urn的第三个元素(urn[2])的地址。注意,地址应该和指针类型兼容。也就是说,不能把double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。C99/C11已经强制不允许这样做。
- 解引用:*运算符给出指针指向地址上存储的值。因此,*ptr1的初值是100,该值储存在编号为0x7fff5fbff8d0的地址上。
- 取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。本例中,ptr1储存在内存编号为0x7fff5fbff8c8的地址上,该存储单元储存的内容是0x7fff5fbff8d0,即urn的地址。因此&ptr1是指向ptr1的指针,而ptr1是指向urn[0]的指针。
- 指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。因此ptr1+4正好与&urn[4]等价。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
- 递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。因此ptr1++相当于把ptr1的值+4(系统中int为4字节),ptr1指向urn[1]。现在ptr1的值是0x7fff5fbff8d4(数组的下一个元素的地址),*ptr的值为200(即urn[1] )的值。注意,ptr1本身的地址仍是0x7fff5fbff8c8。
- 指针减去一个整数:可以使用
-
运算符从一个指针中减去一个整数。指针必须是第一个运算对象,整数是第二个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3-2与&urn[2]等价,因为ptr3指向的是&urn[4]。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效 - 递减指针:除了递增指针还可以递减指针,作用和递增相反
- 指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如,程序10.13的输出中,ptr2-ptr1=2,意思是这两个指针所指向两个元素相隔两个int,而不是2字节。只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第一个地址),C都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误
- 比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象
在递增或递减指针时还要注意一些问题。编译器不会检查指针是否仍指向数组元素。C只能保证指向数组任意元素的指针和指向数组后面第一个位置的指针有效。但是,如果递增或递减一个指针后超出了这个范围,则是未定义的。另外,可以解引用指向数组任意元素的指针。但是,即使指针指向数组后面一个位置是有效的,也能解引用这样的越界指针。
使用解引用时,需要注意不能解引用未初始化的指针。例如,考虑如下的例子:
int * pt; //未初始化的指针
*pt=5; //严重的错误
第二行的意思是将5储存在pt指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5将储存在何处。这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃。切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。例如,可以用一个现有变量的地址初始化该指针(使用带指针形参的函数时,就属于这种情况)。或者还可以使用malloc()函数先分配内存。