指针变量的存储实质,C语言指针变量的存储详解

相信大家都知道这样一个学习指针的观点:要想彻底理解 C 语言中的指针,首先一定要理解 C 语言中变量的存储实质。谈到变量的存储,我们就不得不先说说计算机的内存概念。计算机的内存是一个用于存储数据的空间,由一系列连续的存储单元组成,它就好像电影院中的座位一样,如图 1 所示。


图 1 内存中某一区域的编号

在电影院中,为了保证大家能够快速找到自己的座位,每个座位都用一个唯一的编号来标识。而对计算机内存来说,它同样需要像座位一样编号,这样我们才能够知道内存中的数据存放在什么位置,这就是我们所说的内存编址。如图 1 所示,每一个内存单元都有一个唯一的地址,系统根据这个地址来识别内存单元,在地址所标识的存储单元中存取数据。在这里,我们需要分清两个概念:

内存单元的地址。如图 1 中的编号(如 10000、10001 等),通过引用这些不同的地址编号,我们就可以使用不同内存单元中存储的数据。值得注意的是,内存单元的地址是固定的,在程序中不能修改。


图 2 内存单元中的数据

内存单元中的数据。如图 2 中的表格内的数据,编号为 10000 的内存单元保存的数据为 119,编号为 10001 的内存单元保存的数据为 120,编号为 10002 的内存单元保存的数据为 121。与内存单元的地址不同,内存单元中的数据是可以被程序修改的,例如,可以在程序中将编号为 10001 的内存单元的值由 120 修改为 122。

在了解计算机内存之后,下面来看看 C 语言中的变量是如何存储的,如下面的代码所示:
int i;
char c;
在上面的代码中,我们声明了两个变量,它们将要求系统在内存中分配一个类型为 int 型的存储空间和一个类型为 char 型的存储空间。因此,执行上面两个语句后,内存中的映像可能如图 3 所示。


图 3 变量的存储

如图 3 所示,在 32 位计算机中,int 类型的变量占用 4 字节(即图 3 中编号为 10000~10003,共 4 个存储单元),char 类型的变量占用 1 字节(即图中编号为 10004 的存储单元)。其实这里很容易看出,变量名实质上就是内存单元地址的一个符号,如变量i代表内存地址 10000(变量所占内存单元的首地址),而变量 c 代表内存地址 10004。当用户使用变量时,本质上是访问该变量所对应的内存单元。

在申请变量之后,接下来需要为变量赋值,如下面的代码所示:
i=100;
c='w';
对于上面的赋值语句,相信大家都能够很好地理解,它表示将整型常量 100 保存到变量i中(实质上是将 100 保存到内存地址 10000 为起始地址的 4 个存储单元中),而将字符常量 w 保存到变量 c 中(实质上是将 w 保存到内存地址为 10004 的存储单元中)。因此,在执行上面的语句后,我们可以利用这样的形象来理解,如图 4 所示。


图 4 变量赋值后的存储

看到图 4,你或许会问,为什么内存地址为 10004 的存储单元存储的是 119,而不是 w 呢?这很简单,字符常量保存的是其 ASCII 码值,所以在编号为 10004 的内存单元中保存的是字符常量 w 的 ASCII 码 119。

到现在为止,相信你对变量的申请与赋值的内存分配变化都有了一定的了解。那么我们现在反过来继续问,变量究竟存储在哪里,我们要如何得到变量的存储地址呢?

要想知道变量的存储地址,就需要用到运算符“&”了,使用该运算符可获得变量的内存单元地址(如果变量占用多个内存单元,将得到首地址)。例如,要在屏幕上显示上面的变量 i 与 c 的地址值,可以使用如下代码:
printf("%x\n", &i);
printf("%x\n", &c);
这里以上面图中的内存映像为例,屏幕上显示的不是 i 值 100,而是显示i的内存地址编号 10000(即如果变量占用多个内存单元,将得到首地址)。同理,显示的不是 c 值 w,而是显示 c 的内存地址编号 10004。当然,在实际操作中,变量 i 与 w 的地址值不会是这个数了。

了解变量的存储与地址之后,现在我们可以来看看指针的概念了。

在 C 语言中,将内存单元的编号或地址称为指针。可通过一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址,或称为某内存单元的指针。它与其他一般变量所不同的是,一般的变量包含的是实际的、真实的数据;而指针只是一个指示器,它告诉程序在内存的哪块区域可以找到数据。

下面来看一条声明一个指向整型变量的指针的语句:
int *p;
很显然,在上面的代码中声明了一个指针变量 p。在执行该的代码后,我们可以通过图 5 所示的存储结构来理解(在 32 位系统中,指针的宽度是 4 字节)。


图 5 指针变量 p 的存储情况

由图 5 中可以看出,指针变量 p 与前面的一般变量 i 与 c 并没有什么本质的区别。那么,它为什么又会被称为“指针”呢?其实,关键是要看这个变量所存储的内容是什么。继续来看下面的语句:
p = &i;
即上面的代码表示把i地址的编号赋值给指针变量 p,也就是说在 p 里面写上i内存地址编号 10000(如果变量占用多个内存单元,将得到首地址),结果图 6 所示。


图 6 执行 p=&i 后的存储情况

如图 6 所示,在指针变量 p 中保存变量i的首地址编号 10000,因此,通过指针变量 p 就可间接访问内存单元 10000 开始的 4 个内存单元。也就是说,程序先通过指针变量 p 的值找到变量i的首地址编号 10000,再通过该地址即可访问对应的内存单元,这种访问数据的方式也称为“间接访问”

在了解上面这些原理之后,下面的语句就不难理解了:
/*指针的地址*/
printf("%x\n", &p);
/*指针保存的地址*/
printf("%x\n", p);
/*指针所保存的地址的值*/
printf("%d\n", *p);
以上面图中的内存结构为例,对于语句“printf("%x\n”,&p)”,很显然输出的结果就是变量 p 的首地址编号 10006(即指针的地址);而对于语句“printf("%x\n”,p)”,输出的结果就是变量i的首地址编号 10000(即指针保存的地址);而语句“printf("%d\n",*p)”输出的结果就是变量 i 的值 100(即指针所保存的地址的值),它等价于语句“printf("%d\n",i)”。

最后来继续温习一下指针的 4 个基本概念。

1)指针的类型

从语法的角度看,只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型,如下面的代码所示:
int *p;            // 指针的类型是int*
int **p;            // 指针的类型是int**
int (*p)[3];      // 指针的类型是int(*)[3]

2) 指针所指向的类型
当通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当作什么来看待。从语法上看,只需要将指针声明语句中的指针名字和名字左边的指针声明符“*”去掉,剩下的就是指针所指向的类型。如下面的代码所示:
int *p;             // 指针所指向的类型是int
char *p;            // 指针所指向的类型是char
int **p;             // 指针所指向的类型是int*
int (*p)[3];          // 指针所指向的类型是int()[3]
int *(*p)[4];       // 指针所指向的类型是int*()[4]

3) 指针的值
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在 32 位程序中,所有类型的指针的值都是一个 32 位整数,因为 32 位程序里内存地址全都是 32 位长。

指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为 sizeof(指针所指向的类型)的一片内存区。所以当我们说一个指针的值是 XX 的时候,就相当于说该指针指向了以 XX 为首地址的一片内存区域;而我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。

4) 指针本身所占据的内存区
指针本身占了多大的内存?只要用函数 sizeof(指针的类型)测一下就知道了。在 32 位平台中,指针本身占据了 4 字节的长度。