空(null)指针与0值
5.1 臭名昭著的空指针到底是什么?
语言定义中说明, 每一种指针类型都有一个特殊值—— “空指针” —— 它与同类型的其它所有指针值都不相同, 它“与任何对象或函数的指针值都不相等”。
也就是说, 取地址操作符& 永远也不能得到空指针, 同样对malloc() 的成功调用也不会返回空指针, 如果失败, malloc() 的确返回空指针, 这是空指针的典型用法:
表示“未分配” 或者“尚未指向任何地方” 的指针。
空指针在概念上不同于未初始化的指针。空指针可以确保不指向任何对象或函数; 而未初始化指针则可能指向任何地方。参见问题1.10、7.1 和7.26。
如上文所述, 每种指针类型都有一个空指针, 而不同类型的空指针的内部表示可能不尽相同。尽管程序员不必知道内部值, 但编译器必须时刻明确需要那种空指针, 以便在需要的时候加以区分(参见问题5.2、5.5 和5.6)。
5.2 怎样在程序里获得一个空指针?
根据语言定义, 在指针上下文中的常数0 会在编译时转换为空指针。也就是说, 在初始化、赋值或比较的时候, 如果一边是指针类型的值或表达式, 编译器可以确定另一边的常数0 为空指针并生成正确的空指针值。
因此下边的代码段完全合法:
char *p = 0;
if(p != 0)
参见问题5.3。
然而, 传入函数的参数不一定被当作指针环境, 因而编译器可能不能识别未加修饰的0 “表示” 指针。在函数调用的上下文中生成空指针需要明确的类型转换,强制把0 看作指针。例如, Unix 系统调用execl 接受变长的以空指针结束的字符
指针参数。它应该如下正确调用:
execl("/bin/sh", "sh", "-c", "date", (char *)0);
如果省略最后一个参数的(char *) 转换, 则编译器无从知道这是一个空指针,从而当作一个0 传入。(注意很多Unix 手册在这个例子上都弄错了。)
如果范围内有函数原型, 则参数传递变为“赋值上下文”, 从而可以安全省略多数类型转换, 因为原型告知编译器需要指针, 使之把未加修饰的0 正确转换为适当的指针。函数原型不能为变长参数列表中的可变参数提供类型。(参见问题15.3)
在函数调用时对所有的空指针进行类型转换可能是预防可变参数和无原型函数出问题的最安全的办法。
5.3 用缩写的指针比较“if(p)” 检查空指针是否可靠?如果空指针的内部表达不是0 会怎么样?
当C 在表达式中要求布尔值时, 如果表达式等于0 则认为该值为假, 否则为真。换言之, 只要写出
if(expr)
无论“expr” 是任何表达式, 编译器本质上都会把它当
if((expr) != 0)
处理。
如果用指针p 代替“expr” 则
if(p) 等价于if(p != 0)。
而这是一个比较上下文, 因此编译器可以看出0 实际上是一个空指针常数, 并使用正确的空指针值。这里没有任何欺骗; 编译器就是这样工作的, 并为、二者生成完全一样的代码。空指针的内部表达无关紧要。
布尔否操作符! 可如下描述:
!expr 本质上等价于(expr)?0:1
或等价于((expr) == 0)
从而得出结论
if(!p) 等价于if(p == 0)
类似if(p) 这样的“缩写”, 尽管完全合法, 但被一些人认为是不好的风格(另外
一些人认为恰恰是好的风格; 参见问题17.8)。
参见问题9.2。
5.4 NULL 是什么, 它是怎么定义的?
作为一种风格, 很多人不愿意在程序中到处出现未加修饰的0。因此定义了预处理宏NULL (在<stdio.h> 和其它几个头文件中) 为空指针常数, 通常是0 或者((void *)0) (参见问题5.6)。希望区别整数0 和空指针0 的人可以在需要空指针的地方使用NULL。
使用NULL 只是一种风格习惯; 预处理器把所有的NULL 都还原回0, 而编译还是依照上文的描述处理指针上下文的0。特别是, 在函数调用的参数里, NULL之前(正如在0 之前) 的类型转换还是需要。问题5.2 下的表格对0 和NULL 都有效(带修饰的NULL 和带修饰的0 完全等价)。
NULL 只能用作指针常数; 参见问题5.7。
5.5 在使用非全零作为空指针内部表达的机器上, NULL 是如何定义的?
跟其它机器一样: 定义为0 (或某种形式的0; 参见问题5.4)。
当程序员请求一个空指针时, 无论写“0” 还是“NULL”, 都是有编译器来生成适合机器的空指针的二进制表达形式。因此, 在空指针的内部表达不为0 的机器上定义NULL 为0 跟在其它机器上一样合法:编译器在指针上下文看到的未加修饰的0 都会被生成正确的空指针。参见问题5.2、5.8 和5.14。
5.6 如果NULL 定义成#define NULL ((char *)0) 难道不就可以向函数传入不加转换的NULL 了吗?
一般情况下, 不行。复杂之处在于, 有的机器不同类型数据的指针有不同的内部表达。这样的NULL 定义对于接受字符指针的的函数没有问题, 但对于其它类型的指针参数仍然有问题(在缺少原型的情况下), 而合法的构造如
FILE *fp = NULL;
则会失败。
不过, ANSI C 允许NULL 的可选定义
#define NULL ((void *)0)
除了潜在地帮助错误程序运行(仅限于使用同样类型指针的机器, 因此帮助有限) 以外, 这样的定义还可以发现错误使用NULL 的程序(例如, 在实际需要使用ASCII NUL 字符的地方; 参见问题5.7)。
无论如何, ANSI 函数原型确保大多数(尽管不是全部; 参见问题5.2)指针参数在传入函数时正确转换。因此, 这个问题有些多余。
5.7 如果NULL 和0 作为空指针常数是等价的, 那我到底该用哪一个呢?
许多程序员认为在所有的指针上下文中都应该使用NULL, 以表明该值应该被看作指针。另一些人则认为用一个宏来定义0, 只不过把事情搞得更复杂, 反
而令人困惑。因而倾向于使用未加修饰的0。没有正确的答案。(参见问题9.2 和17.8) C 程序员应该明白, 在指针上下文中NULL 和0 是完全等价的, 而未加修饰的0 也完全可以接受。任何使用NULL (跟0 相对) 的地方都应该看作一种温和的提示, 是在使用指针; 程序员(和编译器都) 不能依靠它来区别指针0 和整数0。
在需要其它类型的0 的时候, 即便它可能工作也不能使用NULL, 因为这样做发出了错误的格式信息。(而且, ANSI 允许把NULL 定义为((void *)0), 这在非
指针的上下文中完全无效。特别是, 不能在需要ASCII 空字符(NUL) 的地方用NULL。如果有必要, 提供你自己的定义
#define NUL ’\0’
5.8 但是如果NULL 的值改变了, 比如在使用非零内部空指针的机器上,难道用NULL (而不是0) 不是更好吗?
不。(用NULL 可能更好, 但不是这个原因。) 尽管符号常量经常代替数字使用以备数字的改变, 但这不是用NULL 代替0 的原因。语言本身确保了源码中的0 (用于指针上下文) 会生成空指针。NULL 只是用作一种格式习惯。参见问题5.5和9.2。
5.9 用预定义宏#define Nullptr(type) (type *)0 帮助创建正确类型的空指针。
这种技巧, 尽管很流行而且表面上看起来很有吸引力, 但却没有多少意义。在赋值和比较时它并不需要; 参见问题5.2。它甚至都不能节省键盘输入。参见问题9.1 和10.1。
5.10 这有点奇怪。NULL 可以确保是0, 但空(null) 指针却不一定?
随便使用术语“null” 或“NULL” 时, 可能意味着以下一种或几种含义:
1. 概念上的空指针, 问题5.1 定义的抽象语言概念。它使用以下的东西实现的
⋯⋯
2. 空指针的内部(或运行期) 表达形式, 这可能并不是全零, 而且对不用的指针类型可能不一样。真正的值只有编译器开发者才关心。C 程序的作者永远看不到它们, 因为他们使用⋯⋯
3. 空指针常数, 这是一个常整数0 (参见问题5.2)。它通常隐藏在⋯⋯
4. NULL 宏, 它被定义为0 (参见问题5.4)。最后转移我们注意力到⋯⋯
5. ASCII 空字符(NUL), 它的确是全零, 但它和空指针除了在名称上以外, 没有任何必然关系; 而⋯⋯
6. “空串” (null string), 它是内容为空的字符串("")。在C 中使用空串这个术语可能令人困惑, 因为空串包括空字符(’\0’),但不包括空指针, 这让我们绕
了一个完整的圈子⋯⋯
本文用短语“空指针” (“null pointer”, 小写) 表示第一种含义, 标识“0” 或短语“空指针常数” 表示含义3, 用大写NULL 表示含义4。
5.11 为什么有那么多关于空指针的疑惑?为什么这些问题如此经常地出现?
C 程序员传统上喜欢知道很多(可能比他们需要知道的还要多) 关于机器实现的细节。空指针在源码和大多数机器实现中都用零来表示的事实导致了很多无根据的猜测。而预处理宏(NULL) 的使用又似乎在暗示这个值可能在某个时刻或者在某种怪异的机器上会改变。“if(p == 0)” 这种结构又很容易被误认为在比较之前把p 转成了整数类型, 而不是把0 转成了指针类型。最后, 术语“空” 的几种用法(如上文问题5.10 所列出的) 之间的区别又可能被忽视。
冲出这些迷惘的一个好办法是想象C 使用一个关键字(或许象Pascal 那样,用“nil”) 作为空指针常数。编译器要么在源代码没有歧义的时候把“nil” 转成适当类型的空指针, 或者有歧义的时候发出提示。
现在事实上, C 语言的空指针常数关键字不是“nil” 而是“0”, 这在多数情况下都能正常工作, 除了一个未加修饰的“0” 用在非指针上下文的时候, 编译器把它生成整数0 而不是发出错误信息, 如果那个未加修饰的0 是应该是空指针常数, 那么生成的程序不行。??????
5.12 我很困惑。我就是不能理解这些空指针一类的东西。
有两条简单规则你必须遵循:
1. 当你在源码中需要空指针常数时, 用“0” 或“NULL”。
2. 如果在函数调用中“0” 或“NULL” 用作参数, 把它转换成被调函数需要的指针类型讨论的其它内容是关于别人的误解, 关于空指针的内部表达(这你无需了解),
和关于函数原型的复杂性的。(考虑到这些复杂性, 我们发现规则2 有些保守; 但它没什么害处。)理解问题5.1、5.2 和5.4, 考虑问题5.3、5.7、5.10 和5.11 你就就会变得清晰。
5.13 考虑到有关空指针的所有这些困惑, 难道把要求它们内部表达都必须为0 不是更简单吗?
如果没有其它的原因, 这样做会是没脑筋的。因为它会不必要地限制某些实现, 阻止它们用特殊的非全零值表达空指针, 尤其是当那些值可以为非法访问引发自动的硬件陷阱的时候。
况且, 这样的要求真正完成了什么呢?对空指针的正确理解不需要内部表达的知识, 无论是零还是非零。假设空指针内部表达为零不会使任何代码的编写更容易(除了一些不动脑筋的calloc() 调用; 参见问题7.26)。用零作空指针的内部表达也不能消除在函数调用时的类型转换, 因为指针的大小可能和int 型的大小依然不同。(如果象上文问题5.11 所述, 用“nil” 来请求空指针, 则用0 作空指针的内部表达的想法都不会出现。)
5.14 说真的, 真有机器用非零空指针吗, 或者不同类型用不同的表达?
至少PL/I, Prime 50 系列用段07777, 偏移0 作为空指针。后来的型号使用段0, 偏移0 作为C 的空指针, 迫使类似TCNP (测试C 空指针) 的指令明显地成了现成的作出错误猜想的蹩脚C 代码。旧些的按字寻址的Prime 机器同样因为要求字节指针(char *) 比字指针(int *) 长而臭名昭著。
Data General 的Eclipse MV 系列支持三种结构的指针格式(字、字节和比特指针), C 编译器使用了其中之二:char * 和void * 使用字节指针, 而其它的使用字指针。
某些Honeywell-Bull 大型机使用比特模式06000 作为(内部的) 空指针。
CDC Cyber 180 系列使用包含环(ring), 段和位移的48 位指针。多数用户(在环11 上) 使用的空指针为0xB00000000000。在旧的1 次补码的CDC 机器上用全1 表示各种数据, 包括非法指针, 是十分常见的事情。
旧的HP 3000 系列对字节地址和字地址使用不同的寻址模式; 正如上面的机器一样, 它因此也使用不同的形式表达char * 和void * 型指针及其它指针。
Symbolics Lisp 机器是一种标签结构, 它甚至没有传统的数字指针; 它使用<NIL, 0> 对(通常是不存在的<对象, 偏移> 句柄) 作为C 空指针。
根据使用的“内存模式”, 8086 系列处理器(PC 兼容机) 可能使用16 位的数据指针和32 位的函数指针, 或者相反。
一些64 位的Cray 机器在一个字的低48 位表示int *; char * 使用高16 位的某些位表示一个字节在一个字中的偏移。
参考资料: [K&R1, Sec. A14.4 p. 211]。
5.15 运行时的“空指针赋值” 错误是什么意思?
这个信息, 通常由MS-DOS 编译器发出, 表明你通过空指针向非法地址(可能是缺省数据段的偏移0 位置) 写入了数据。参见问题16.7。