深入浅出c指针

Time: 八月 13, 2008
Category: Programming practices

指 针(pointer)是对“数据对象或函数”的一种引用,它记录了一个“对象或函数”的“内存地址和类型”。通俗地说,指针是一个变量,为了区分普通的变 量(普通的变量内存空间存储的是数值,而指针存储的却是计算机内存空间的某个地址),网络上很多资料对这个概念的叫法不一,有时叫指针,有时叫指针变量, 实质上都是同一个东西,指针本身就是一个变量,指针变量从语法的角度上看算不算重复累赘?

指针有很多用处,灵活性极强,比如,在C里,如果需要处理大量的数据的话,使用指向数据的指针比直接处理数据本身更经济和高效,又比如,你不需要移动内存中的数据就能对大量的记录进行排序,此外,利用指针,还能实现各种甚至非常复杂动态数据结构等等

声明指针

通常来说,指针的指向一般有几种:非数组对象,数组对象,函数,我们后面会更进一步地讨论“指向数组和函数”的指针,现在首先来看看,如何声明一个“指向非数组对象”的指针,语法描述如下:

C++代码
  1. type   *   [类型的限定符]   名称   [=初始值]

在 声明中,type   *  不同于 type,type 表示需要声明的是一个普通变量,而 type   * 声明的是一个指针,该指针指向 type类型的对象 ;[]表示参数具有可选性,比如 [=初始值] 表示在声明对象的同时初始值是可有可无的;类型的限定符,一般有const,volatile,restrict 等,关于指针类型的限定符,可以自行参考,这里不做详细描述;名称被声明为一个指针对象,其数据类型为 type   * ;

举个例子,声明一个指针p和变量k:   int   *p; 在上面这个声明里,我们可以得出很多有用的信息:

  1. p是指针,而不是 *p
  2. int  * 是指针的类型,而 int 是指针所指向的类型,int  * 的意思是说,我所指向的对象的数据类型必须为 int (当类型不一样时,某些编译器可能会进行隐式转换)
  3. 指针p 是一个变量,在32位机里,有4字节长的内存空间

在讲解指针的之前,我们先来解释一下有关指针的几个重要而且容易混乱的概念:

  1. 指 针:指针是一种非常特殊的变量,特殊的地方在于,一方面指针的内存空间并非用来保存有效数据,而是存储着另外一个对象或者函数的内存地址;另外一方面,不 管指针所指向的对象类型是什么,指针作为变量,自身的内存空间大小是一样的,换句话说,char  * 型和 int  *型的指针所占用的空间大小相同,在32位的计算机上,通常是4个字节长,刚好32位
  2. 指针的类型:既然指针也是变量,自然有自己的数据 类型,不同的是,普通变量的类型描述是 type  ,而指针的类型描述是 type  * ,指针的类型只有一个作用,它给计算机指出了自己所指向的必须是什么类型的对象,事实上,如果你强行将int  *类型的指针指向一个 char 类型的对象,那么编译器会发生隐式类型转换
  3. 指针所指向的类型:如果这个指针是指向对象的,那么指针所指向的类型就是该对象的类型;如果这个指针是指向一个函数的,那么这个指针所指向的类型就是该函数的返回值类型
  4. *  :要区别 * 的两种用法,在表达式里,*  是间接运算符,比如,对于指针 p ,那么*p的运算结果就是指针 p 所指向的对象的值(计算机是这样计算的,遇到 *p 指令时,p是指针,然后读取变量 p 的值,把这个值作为一个内存地址,寻址到这个值所在的内存区域,再读取出这个区域所存储的数据 β,那么 β 就是指针 p 所指向的对象的值);而在指针声明里,*  并不是运算符,而是声明符的一部分,它给计算机指出:我要声明的是指针而不是普通变量
  5. & :取址运算符,意思就是说,可以取得一个对象的内存地址,但是也有值得注意的地方,比如:假定变量 k 的数据类型是 char ,那么 &k 的结果产生一个 char  * 类型的指针,该指针自身的内存区域保存了变量 k 的空间地址

另外,即使指针保存的是另外一个对象在内存中的地址,但是也不可否认指针本身也是内存中的一个对象,因为它也是一个变量。站在对象的角度来看指针,那么其他的指针就可以指向该对象,这里就涉及到指针的复杂类型问题。解决这些疑问之前,先来看看下面的东西:

C++代码
  1. int**  a; //就是声明一个二级整型指针
  2. int*  *a; //就是声明一个整型指针,而这个指针本身也是一个指针,也就是二级指针
  3. int **a; //那就是声明一个整型变量,这个变量是一个二级指针变量

实际上这种解释并不正确,声明的描述也不规范。首先,我不清楚多级指针这个说法;其次,上面三种声明的描述其效果都是一样,但只有 int  **a  的写法才符合标准的;还有,说这话的人没真正的理解指针,因为指针本身就是一个 int 型变量。在C的规范里,我们说这类型指针 P 是复杂的,要声明一个复杂的指针,语法描述应该是:

C++代码
  1. type   *…*   [类型的限定符]   对象名   [=初始值]

*  描述符的多少取决于指针指向的复杂程度,假设你这样声明一个指针  int  ***p ;      这个声明这样解释:

  1. 声明了一个对象,这个对象是 p 而不是 *p 或者 **p ,p是指针!
  2. p 的类型是 int  ** ,int  ** 的意思是说,该指针最终所指向的对象一定是 int 类型的,当然,** 的意思,暗示了多重指引

还要说一下指向数组的指针的问题,在许多C程序里,指针通常指向一个数组对象,或者作为元素存储到数组里面。“指向数组的指针”通常被简称为数组指针,而“有指针类型元素的数组”则被成为指针数组,要声明指向数组类型的指针,必须使用括号,语法描述是:

C++代码
  1. type   (*…*   [类型的限定符]   对象名)[n]   [=初始值]

下面举个简单的例子来说明一下:

C++代码
  1. int  (* arrPtr)[10] = NULL ;

上面的声明描述可以这样解释:

  1. 声明一个指针 arrPtr 指向一个数组对象
  2. arrPtr 的指针类型是 int   (* )[10],它表示 arrPtr 必须是一个指向“拥有10个 int 元素的数组”

指向函数的指针比较复杂,后面我会专门讨论到这个问题,这里讲一下有个需要注意的地方,那就是 [类型的限定符] 的位置问题,无论是声明指向非数组对象的指针还是指向数组对象的指针,类型限定符处于不同位置,声明的效果是不一样的:

type   *   [类型的限定符]   名称   [=初始值]      (or)       type   [类型的限定符]   *   名称   [=初始值]

对于限定符 const,const 是一个常量关键字:

C++代码
  1. int  * const  Ptr;   //关键字 const 出现在 * 右边,表示要声明的是一个指针常量
  2. int  const  * Ptr;   //关键字 const 出现在 * 左边,表示要声明的是一个常量指针

要注意指针常量和常量指针的区别:指针常量,指针自身就是一个常量,表示指针自身不可变,但其指向的地址的内容是可以被修改的;常量指针,就是指向常量的指针,表示指针所指向的对象的值是不可修改的,但指针自身可变。

同样,对于指向数组的指针,下面的描述也是不一样,理解同上:

type   (*…*   [类型的限定符]   对象名)[n]   [=初始值]    和    type    [类型的限定符]     (*…*    对象名)[n]   [=初始值]

前者声明了数组指针常量,指针自身是不可被修改的,正因为如此,还多说一句,在声明一个指针常量的时候一定要进行初始化。后者声明了常量数组指针, 该指针指 向了一个数组名,不过,在C语言里,数组名本身就是一个指针,该指针指向了这个数组内存区域的首地址,按照语法描述的意思就是说,这个数组名就是一个常 量,但是计算机在给一个数组分配完内存的时候就是不变了的啊,这么说,这个语法描述就没什么意义了。


指针的初始化

具有自动生存周期的指针(被声明为 static ),是没有定义值的,除非声明的同时提供显式的初始化器(initializer),另外,如果既没有被声明为 static 又没有对其进行赋初值的,系统默认把空指针当作其初始化值,一个指针的初始化有以下几种方式:

一、使用空指针常量,比如,标准函数 fopen() 如果无法在指定的模式下打开某文件,将返回一个空指针常量 NULL

 

C++代码
  1. /* … */
  2. FILE *fp = fopen(“test.txt”,”r”)
  3. if(fp==NULL){
  4.     //Wrong ! Can’t open the file
  5. }

二、指向相同类型的指针,或者指向较少类型限定符的相同类型指针,要注意的是,如果指针不具有自动生存周期,必须使用常量表达式来初始化,比如“取址运算”的结果或者数组、函数的名称

 

 

C++代码
  1. #include <stdio.h>
  2. /* … */
  3. int s[10],*p;
  4. p=s;

三、一个 void 指针(被初始化的指针不能是函数指针)

 

另外注意,在对指针进行赋值操作时,如果表达式两边指针的类型不一致时,要注意编译器是否会进行一些隐式的操作,或者将一个指针显式地转换成另一个指针的类型,具体的细节和范例大家可以参考《C in a Nutsbell》 里的第四章:类型转换,有很详尽的描述

还有就是,给数组指针赋值或者初始化时,一定要注意数组指针的类型,这个很容易混乱,作为一个出色的程序员,你不应该忽略这些东西,下面是一个不规范写法的例程:

C++代码
  1. int main()
  2. {
  3.     int a[5];
  4.     int  const (* p)[10]=&a;
  5.     return 0;
  6. }

 


指针的运算

这 一节我们着重讨论指针相关的一些操作。常用的操作有:存储、读取和修改“指针所指向的”对象或函数,还有就是指针本身的运算,比如指针的自加自减运算等 等,也可以比较指针或者使用指针来遍历内存区域。上面我们提到过关乎指针运算很重要的两个符号,间接运算符 * 与取址运算符 & ,在这里有个需要注意的地方,乘法运算符 * 虽然和间接运算符 * 符号一样,但是性质是不一样的,前者是二元运算符,后者是一元运算符,也就是说,间接运算符只需要一个操作数。

假定 ptr 是一个指针,那么 *ptr 就是 ptr 所指向的对象或者函数,在指针的定义里我们得知,指针的类型决定所指向对象的类型,比方说,当用 int 指针存取一个特定位置时,读取或写入的也必须是 int 类型的对象,在下面的例子中,ptr 指向变量 x ,因此表达式 *ptr 等同于 x 本身

 

C++代码
  1. double x,y,*ptr;   //声明两个double变量,和一个指向double型对象的指针ptr
  2. ptr=&x;            //对x进行取址运算,&x产生一个指针,该指针指向一个double型的对象,也就是说,该指针的类型是 double * ,与 ptr 类型相同
  3. *ptr=7.8;          //因为 ptr 是指向 x 的指针,所以 *ptr 等同于 x ,对 *ptr 进行赋值操作也就是给 x 赋值
  4. y=*ptr+0.5;        //将 x 的值加上0.5再赋值给 y

对于复杂类型的指针,如指针 p,要注意给 p、*p、**p、***p 等赋值的意义是不一样的,示例:

 

 

C++代码
  1. Ptr = &k      //指针Ptr自身作为变量保存k的内存地址
  2. *Ptr = &k     //将k保存到指针Ptr所指向的对象的内存区域里
  3. **Ptr = &k    //计算机先找到Ptr指针指向的对象,将这个对象保存的数据作为内存地址继续找到下一个指向,然后写入k的首地址到这块内存区域里
  4. /* …… */

以此类推,在运算表达式里,* 的作用是在寻找指向的对象

 

除 了使用赋值操作让一个指针指向某对象或数组,也可以使用数学运算来修改指针,比如,对一个指针执行整数加法和减法操作;两个指针相减;比较两个指针 等等,通常这些相关运算的对象是数组指针。当两个指针想减时,这两个指针就必须具有相同的基本类型,但是类型限定符则不需要一样,因为类型限定符的作用是 指针本身,它就像一个监视器,必须保持指针具有什么样的性质,而不关乎你进行什么样的动作。

为了演示这些运算是怎么使用的,我们来看看下面的例子,其中两个指针 P1 和 P2 都指向数组 a 内的元素:

  1. 如果 P1 指向数组元素 a[i] ,并且 n 是一个整数,那么表达式 P2=P1+n 的结果就是:P2 指向了数组元素 a[i+n],也就是 *P2 等同于 a[i+n]
  2. 如果 temp = P2 – P1 ,那么 temp 的值就是两个指针之间数组元素的个数,temp 的类型是 ptrdiff_t,ptrdiff_t 是有符号整型 (int),在 stddef.h 头文件中有定义,用来记录两个指针的差距
  3. 在使用数组的时候,例:a[n],计算机通常会把 n 作为数组 a 内的一个索引,因此如果 P2 所指向的元素比 P1 所指向的元素具有更大的索引值,那么我们就说 P1<P2 的结果为 true,否则为 false

对 于第一种很好解释,数组名不同于变量名,在C里,数组的名称本身就是一个指针,它指向了数组的第一个元素,如果你非常熟悉汇编或计算机内的存储结构,那么 这点就不难理解了,我们可以把数组的下标计数法对换成指针的算术,定义数组 a[100] 和一个指向数组 a 内元素的指针 P1 ;

  1. a 是数组名,a + i 表达式的结果产生了一个指向 a[i] 的指针,而 *(a+i) 则是 a[i] 本身
  2. P1 – a  表达式的结果是 P1 所指向的元素的索引值

对 于指针与 int 型变量的相加减,编译器是这样处理的:假定 Ptr 是一个指向 char 型数组的指针,Ptr + n 的意思是,将指针 Ptr 向高地址移动 n*sizeof(char) ,在32位PC里,char 变量是1字节,也就是 8bits 的长度,如果 n 是3 ,那 n*sizeof(char)  就是 3*8=24(bits),相当于 n 个 char 型对象的长度,实际上也就是说,如果指针指向 Ptr 了一个数组对象,那么 Ptr 每次加 1 就相当于指针 Ptr 指向了数组的下一个元素。

现在你明白了吧,也许你会问,如果将一个 int *型的指针指向了 char 型的数组,类型不一样,按照上面的说法,指针加1岂不是不能指向到数组的下一个元素了吗?不是这样的,因为这种情况是不可能存在的,将一个 type  * 型的指针指向一个非 type 类型的数组,编译器会自动隐式地进行类型转换,这并是一种好的编程习惯,所以,应该尽量避免这种情况,否则,就显式地强制类型转换

为了助于大家的理解,我写了一个用指针来进行选择排序的算法

 

C++代码
  1. void selection_sort(int a[],int n){
  2.     int *last,*p,*minPtr;
  3.     last=a+(n-1)
  4.     if(n<=1)
  5.         return(-1);
  6.     for(;a<last;++a){
  7.         minPtr=a;
  8.         for(p=a+1;p<=last;++p)
  9.             if(*p<*minPtr)
  10.                 minPtr=p;
  11.         swap(a,minPtr);
  12.     }
  13. }

通常我们使用索引 i 来获取对应元素的数组,如 a[i] 或者 *(a+i) ,因为计算机会将数组的首地址 a 加上 i*sizeof(element type) 的值,这就增加了计算的重复工作量,而在上面的 selection_soft 算法里使指针自身进行递增的,无需索引就可以直接指向所需元素

 


关于指针的运算还有更为复杂的一种形式,那就是在二维数组里,以 int  a[3][5] ;  为例,首先来讲解一下二维数组的存储结构以及它和指针之间的关系,给二维数组分配内存的时候,首先将一块区域分成 3 大块,再将每一大块分成 5 小块,如果是三维数组,那就再将每一小块分成更小的块,以此类推

在一维数组里我们知道数组名本身就是一个指针,指向数组的第一个元素,但是,在二维数组里,并非如此,一旦声明一个二维数组 int  a[n][m] ;   我们就可以得到许多有用的信息:

  1. 集合 A = { a[x][y] | x∈[0,n-1] , y∈[0,m-1] } 中的每一个元素都是一个 int 型变量
  2. 集合 B = { a[x] | x∈[0,n-1]  } 中的每一个元素都是一个指针,指针的类型是 int  *,所指向对象的类型是 int
  3. 数组名 a 也是一个指针,指针的类型是 int  (* )[m],所指向对象的类型是 int  [m]

第 一条不解释,第二条很好理解,因为二维数组还有另一层含义,那就是它本身也是一个存储了n个指针(每个指针指向一个一维数组)的一维数组,比如你看上 图,a[0] 可以看作是由 a[0][0] , a[0][1] ,a[0][2] ,a[0][3] ,a[0][4] 所组成的一维数组的数组名, a[1] 也可以看作是由 a[1][0] , a[1][1] ,a[1][2] ,a[1][3] ,a[1][4] 所组成的一维数组的数组名,这么说来 a[0] 和 a[1] 又是指针,指向了各自数组的第一个元素,所指向的元素的数据类型是 int,所以 a[0] , a[1] 指针的类型是 int  * 。同样的道理,a 可以是指针数组的数组名,a 是指针,不同的是,数组元素并不是指针 a[0] , a[1] , a[2],而是 a[0]‘ , a[1]‘ , a[2]‘,a[0] , a[1] , a[2] 指向的是一个数组,但 a[0]‘ , a[1]‘ , a[2]‘ 指向的却是与数组a[0] , 数组a[1] , 数组a[2] 占有相同大小空间的内存区域,所以指针 a 的指针类型是 int (* )[5],这一点一定要区分。

如果你明白我上面所说的,下面的一些演示例子会帮助你加深理解:

 

C++代码
  1. int matrix[3][10];   //定义一个二维数组matrix
  2. int (* arrPtr)[3];      //定义一个指向int (* )[10]型指针的指针
  3. arrPtr = matrix;      //让 arrPtr 指向matrix的第一大块内存区域
  4. (* arrPtr)[0]=5;      //给matrix[0][0]赋值
  5. arrPtr[2][9]=6;       //给最后一个元素赋值
  6. ++arrPtr;                  //将 arrPtr 指向matrix的下一大块内存区域
  7. (* arrPtr)[0]=7       //给matrix[1][0]赋值

 


指向函数的指针

与“指向数组”指针的声明一样,函数指针的声明也需要括号。下面的范例展示如何声明“指向函数”的指针:

 

C++代码
  1. int  (* funcPtr) (int , int)

此声明定义了一个函数指针,被指向的函数需要两个 int 参数和一个 int 型返回值,注意一定要将 * 与标识符括起来,如果没有括号,int  *(funcPtr) (int , int); 的声明就是函数的原型,而不是指针的定义了。还有就是,函数名称会被编译器隐式地转换成函数指针,因此,下面的语句会将标准函数 pow 的地址赋值给 funcPtr ,然后利用指针调用此函数:

 

 

C++代码
  1. double result;
  2. funcPtr = pow;
  3. ……
  4. result = (*funcPtr)(2.2,4.3);  //调用funcPtr 所指向的函数
  5. result = funcPtr(2.2,4.3);     //效果与上面是一样的

 

Leave a Comment