指针是C语言强大的秘密也是最容易犯错误的地方。究其原因,还是在于指针的使用灵活、复杂、多变。但是如果从本质上理解了指针的原理和机制他们用起来就有一种如臂使指的感觉。

内存和地址

现代计算机的内存由数以亿万计的位(bit)组成,每个位可以容纳值0或者1。由于一个位能表示的值得范围太有限,所以单独的位用处不大,通常许多位合成一组作为一个单位,这样就可以存储范围较大的值。这一组bit被称为字节(byte)。如下图所示,每个方块都是一个字节,上方是他们各自的地址,每个字节都包含了存储一个字符所需要的位数。在大多数现代计算机上,每个字节包含8个位,可以存储无符号值0到255,或者有符号值-128到127。下面这个图,并没有显示每个字节的内容,只显示了他们的地址。

为了存储更大的值,我们把两个或者更多的字节合在一起作为一个更大的内存单位。例如,许多计算机以字为单位存储整数,每个字一般由2各或者4个字节组成,下图所示的内存位置与上图相同,但是这次它的基本单位用4个字节表示。

由于他们包含了更多的位,每个字可以容纳的无符号整数的范围是从0到$2^{23}-1$,可以容纳的有符号整数的范围是从$-2^{23}$到$2^{31}-1$。

这里要说一下,尽管一个字包含了4个字节,它仍然只有一个地址(如上图的100,104)。至于它的地址是它最左边那个字节的位置还是最右边那个字节的位置,不同的计算机有不同的规定。

另一个需要注意的是

边界对齐(boundary alignment),在要求边界对齐的机器上,整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数。

对于程序来说,尤其是高级程序,重要的只有两个事:

  1. 内存中的每个位置由一个独一无二的地址标示。
  2. 内存中的每个位置都包含一个值。

如下图,显示内存中4个字的内容。

这里显示了4个整数,每个都位于自己的字中。如果你记住了一个值的存储地址,你以后可以根据这个地址取得这个值。

但是,这个例子中的地址比较短,然而在现代计算机中地址比较长,采用地址访问内容,实在是太笨了。所以高级语言所提供的特性之一就是通过名字而不是地址来访问内存的位置。如Fig. 4,就是使用名字(a, b, c, d, e)来代替地址。当然这些名字就是我们所称的变量。这里需要注意的是,名字与内存位置之间的关联并不是硬件本身提供的,它是由编译器为我们实现的。变量给我们一种更方便的方法记住地址,硬件仍然通过地址访问内存位置。

指针初始化

了解了内存和地址之间的关系后,我们可以开始使用指针,第一步是先创建并初始化指针:

int a = 112, b = -1;
float c = 3.14;
int *d = &a;
float *e = &c;

从上面的代码我们可以看到我们声明并且初始化了两个指针变量d和e。使用*号就可以声明一个指针变量,而初始化是通过 & 操作符来完成的,它用于产生操作数的内存地址。所以我们现在可以记住,

  • *是获取与这个变量的值相等的地址中所存着的内容
  • &是获取当前变量的地址

指针代码风格

在指针这块,熟悉的代码风格可能会带来更快的理解速度,不好的代码风格甚至会导致意想不到的错误。

我们可以写

int *x; 或者 int* y;

对于计算机编译器来说,这两种写法没有任何区别,其实单就这两个语句,后者的意义更明确一些,一个整型的指针变量d,但是我们依然选择前者作为主流写法是因为后者在某些情况下具有迷惑性。比如下面这句,我们很容易的理解成声明了整型指针变量x和y。

int* x, y;

但其实编译器解读的时候并不是这样:我们声明了整型指针变量x和整型变量y。

int *x, y;

所以我个人的代码风格是,全部分开声明。这样虽然代码行数变多,但是更加清晰。

int* x;
int y;

指针的值

那么对于上面这个例子,d和e是多少?根据Fig.3和Fig.4,可以负责任的说,d的值是100,e的值是108。&a的意思是对a这个变量取地址(a地址是100,内容是112),赋给指针变量d(d的值是100)。而这个指针代表的意思是地址是100的那个位置中内容是多少。

a: 112
&a: 100
d: 100
*d: 112

所以,不能简单的认为d和e是指针,他们就可以自动的获得存储于位置100和108的值。变量就是变量,它的值就是分配给给变量内存位置所存储的数值,而之所以指针可以实现,使我们通过操作符的形式赋予他们之间关系。通过*和&,我们可以进行指针的声明和初始化。