下面的代码显示了一个常见的错误:
int *a;
*a = 12;
a指向哪里呢?我们声明了这个变量,但从未对它进行初始化,所以我们没有办法预测。12这个值将存储于什么地方。从这一点看,指针变量和其他变量并无区别。如果变量是静态的,它会被初始化为0;但如果变量是自动的,它根本不会被初始化。无论是哪种情况,声明一个指向整型的指针都不会“创建”用于存储整型值的内存空间。
所以,如果程序执行这个赋值操作,会发生什么情况呢?如果运气好,a的初始值会是个非法地址,这样赋值语句将会出错,从而终止程序。在UNIX系统上,这个错误被称为“段违例(segmentation violation)”或“内存错误(memory fault)”。它提示程序试图访问一个并未分配给程序的内存位置。在一台运行Windows的PC上,对未初始化或非法指针进行间接的访问操作是一般保护性异常(General Protection Exception)的根源之一。
对于那些要求整数必须存储于特定边界的机器而言,如果这种类型的数据在内存中的存储地址处在错误的边界上,那么对这个地址进行访问时将会产生一个错误。这种错误在UNIX系统中被称为“总线错误(bus error)”。
一个更为严重的情况是:这个指针偶尔可能包含了一个合法的地址。接下来的事很简单:位于那个位置的值被修改,虽然你并无意去修改它。像这种类型的错误非常难以捕捉,因为引发错误的代码可能与原先用于操作那个值的代码完全不相干。所以,在对指针进行间接访问之前,必须非常小心,确保它们已被初始化!
标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针变量为NULL,你可以给它赋一个零值。为了测试一个指针变量是否为NULL,你可以将它与零值进行比较。之所以选择零这个值是因为一种源代码约定。就机器内部而言,NULL指针的实际值可能与此不同。在这种情况下,编译器将负责零值和内部值之间的翻译转换。
NULL指针的概念是非常有用的,因为它给了你一种方法,表示某个特定的指针目前并未指向任何东西。例如,一个用于在某个数组中查找某个特定值的函数可能返回一个指向查找到的数组元素的指针。如果该数组不包含指定条件的值,函数就返回一个NULL指针。这个技巧允许返回值传达两个不同片段的信息。首先,有没有找到元素?其次,如果找到,它是哪个元素?
尽管这个技巧在C程序中极为常用,但它违背了软件工程的原则。用一个单一的值表示两种不同的意思是件危险的事,因为将来很容易无法弄清哪个才是它真正的用意。在大型的程序中,这个问题更为严重,因为你不可能在头脑中对整个设计一览无余。一种更为安全的策略是让函数返回两个独立的值:首先是个状态值,用于提示查找是否成功;其次是个指针,当状态值提示查找成功时,它所指向的就是查找到的元素。
对指针进行解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西。因此,对一个NULL指针进行解引用操作是非法的。在对指针进行解引用操作之前,你首先必须确保它并非NULL指针。