一、什么是空指针?
空指针 是一个特殊的指针值。
空指针 是指可以确保没有向任何一个对象的指针。通常使用宏定义 NULL 来表示空指针常量值。
空指针 确保它和任何非空指针进行比较都不会相等,因此经常作为函数发生异常时的返回值使用。另外,对于第 5 章的链表来说,也经常在数据的末尾放上一个空指针来提示:“请注意,后面已经没有元素了哦。”
在如今的操作系统下,应用程序一旦试图通过空指针引用对象,就会马上招致一个异常并且当前应用程序会被操作系统强制终止。因此,如果每次都使用 NULL 来初始化指针变量,在错误地使用了无效(未初始化)的指针时,
我们就可以马上发现潜在的 bug。
通常,我们可以根据指针指向的数据类型来明确地区别指针的类型。如果将“指向 int 的指针”赋给“指向 double 的指针”,如今的编译器会报前面提到的警告。但是,只有 NULL,无论对方指向什么类型的变量,都可以被赋值和比较。
偶尔会见到先将空指针强制转型,然后进行赋值、比较操作的程序,这不但是徒劳的,甚至还会让程序变得难以阅读。
二、区分 NULL、'\0' 和 0
经常有一种错误的程序写法:使用 NULL 来结束字符串。
/* * 通常,C 的字符串使用 '\0' 结尾,可是因为 strncpy() 函数在 src 的长度大于 len * 的情况下没有使用 '\0' 来结束,所以一板一眼写了一个整理成 C 的字符串形成的函数(企图) */ void my_strncpy(char *dest, char *src, int len) { strncpy(dest, src, len); dest[len] = NULL; /* 错误:使用 NULL 来结束字符串!! */ }
上面的代码,尽管在某些运行环境下能跑起来,但无论怎样它就是错误的。因为字符串是使用“空字符”来结束的,而不是用空指针来结束。
在 C 语言标准中,空字符的定义为“所有的位为 0 的字节称为 空字符(null character)”。也就是说,空字符是值为 0 的字符。
空字符在表现上通常使用'\0'。因为'\0'是常量,所以实际上它等同于 0。也许有些吓到你了,'\0'呀'a'呀什么的,它们的数据类型其实并不是 char,而是 int。
另外,在我的环境下,NULL 在 stdio.h 里的定义如下:
#define NULL 0
看到这个,你可能会说:“说来说去,那还不都是 0 嘛。”确实在大部分的情况下是这样的,但背后的事情却异常复杂。
正如前面说的那样,写成 '\0' 和 写成常量的 0 其实是一样的。使用 '\0' 只不过是习惯使然。如果想让代码容易读,遵从习惯是非常重要的。
将 0 当作空指针来使用,除了极其例外的情况,通常是不会发生错误的。
但是,如果在字符串的最后使用 NULL,就必然会发生错误。
标准允许将 NULL 定义成 (void*)0,所以在 NULL 被定义成 (void*)的时候,如果使用 NULL 来结束字符串,编译器必然会提示警告。
看到刚才的关于 NULL 的定义,可能有人会产生下面的推测:
啥呀?所谓的空指针,不就是为 0 的地址嘛。
在 C 中,为 0 的地址上应该是不能保存有效数据的吧?放什么都起不到任何作用,这没什么大不了的。
这种推测好像颇有道理,但也是有问题的。
确实在大多数的环境中,空指针就是为 0 的地址。但是,由于硬件状况等原因,世上也存在值不为 0 的空指针。
偶尔会有人在获得一个结构体之后,先使用 memset() 将它的内存区域清零后再使用。此外,虽然 C 语言提供了动态分配函数 malloc() 和 calloc(),但是抱着“清零后比较好”的观点,偏爱 calloc() 的倒有很多。这样也许可以避免一些难以再现的 bug。
使用 memset() 和 calloc() 将内存区域清零,其实就是单纯地使用 0 来填位。通过这种处理,当结构体的成员中包含指针的时候,这个指针能不能作为空指针来使用,最终是由运行环境来决定的。
顺便说一下,对于 浮点数,即使它的位模式为 0,值也不一定为 0。
说到这里,
哦,原来这样啊,所以要使用宏定义的 NULL 呢。对于空指针的值不为 0 的运行环境,NULL 的值应该被 #define 成别的值吧。
可能会有人产生以上的想法。实际上,这种想法也是有偏差的,这涉及问题的内部根源。
比如,尝试编译下面的代码
int *p = 3;
在我的环境里,会出现以下警告:
warning: initialization makes pointer from integer without a cast
因为 3 无论怎么说都是 int 型,指针和 int 型是不一样的,所以编译器会提示警告。尽管在我的环境里指针和 int 的长度都是 4字节,但还是出现了警告。如今的编译器,几乎都是这样的。
继续,让我们尝试编译下面的代码:
int *p = 0;
这一次没有警告。
如果说将 int 型的值赋予指针就会得到一个警告,那么为什么值为 3 的时候出现警告,值为 0 的时候却没有警告呢?简直匪夷所思!
这是因为在 C 语言中,“当常量 0 处于应该作为指针使用的上下文时,它就作为空指针使用”。上面的例子中,因为接受赋值的对象指针,编译器根据上下文判断出“0 应该作为指针使用”,所以将常数 0 作为空指针来读取。
无论如何,编译器都会针对性地对待“需要将 0 作为指针进行处理的上下文”,所以即便是空指针的值不为 0 的情况下,使用常量 0 来代替空指针也是合法的。
此外,如上所述,有的环境中像下面这样定义 NULL:
#define NULL ((void*)0)
ANSI C 中,根据“应该将 0 作为指针进行处理的上下文”的原则,将常量 0 作为指针来处理。因此,显式将 0 强制转型成 void* 是没有意义的。但是在某些情况下,编译器也可能会理解不了“应该将 0 作为指针进行处理的上下文”。
这些情况是:
(1) 没有原型声明的函数的参数
(2) 可变长参数函数中的可变部分的参数
ANSI C 中,因为引入了原型声明,只有在你确实做了原型声明的情况下,编译器才能知道你“想要传递指针”。
可是,对于以 printf() 为代表的可变长参数函数,其可变部分的参数的类型编译器是不能理解的。另外糟糕的是,在可变长参数的函数中,还经常使用常量 NULL 来表示参数的结束(比如 UNIX 的系统调用 execl()函数)。
以上情况下,简单地传递常量 0,会降低程序的可移植性。
因此,通常使用宏定义 NULL 来将 0 强制转型成 void*,可以显式地告之编译器当前的 0 为指针。