问题1:<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> 臭名昭著的空指针到底是什么? 答案: 语言定义中说明, 每一种指针类型都有一个特殊值 --- ``空指针" --- 它与同类型的其它所有指针值都不相同, 它 ``与任何对象或函数的指针值都不相等"。也就是说, 取地址操作符 & 永远也不能得到空指针, 同样对 malloc() 的成功调用也不会返回空指针, 如果失败, malloc() 的确返回空指针, 这是空指针的典型用法: 表示 ``未分配" 或者 ``尚未指向任何地方" 的指针。 空指针在概念上不同于未初始化的指针。 空指针可以确保不指向任何对象或函数; 而未初始化指针则可能指向任何地方。 如上文所述, 每种指针类型都有一个空指针, 而不同类型的空指针的内部表示可能不尽相同。尽管程序员不必知道内部值, 但编译器必须时刻明确需要那种空指针, 以便在需要的时候加以区分 问题2: 怎样在程序里获得一个空指针? 答案: 根据语言定义, 在指针上下文中的常数 0 会在编译时转换为空指针。也就是说, 在初始化、赋值或比较的时候, 如果一边是指针类型的值或表达式, 编译器可以确定另一边的常数 0 为空指针并生成正确的空指针值。因此下边的代码段完全合法: char *p = 0; if(p != 0) 然而, 传入函数的参数不一定被当作指针环境, 因而编译器可能不能识别未加修饰的 0 ``表示" 指针。在函数调用的上下文中生成空指针需要明确的类型转换, 强制把 0 看作指针。例如, Unix 系统调用 execl 接受变长的以空指针结束的字符指针参数。它应该如下正确调用: execl("/bin/sh", "sh", "-c", "date", (char *)0); 如果省略最后一个参数的 (char *) 转换, 则编译器无从知道这是一个空指针, 从而当作一个 0 传入。(注意很多 Unix 手册在这个例子上都弄错了。) 如果范围内有函数原型, 则参数传递变为 ``赋值上下文", 从而可以安全省略多数类型转换, 因为原型告知编译器需要指针, 使之把未加修饰的 0 正确转换为适当的指针。函数原型不能为变长参数列表中的可变参数提供类型。 在函数调用时对所有的空指针进行类型转换可能是预防可变参数和无原型函数出问题的最安全的办法。 摘要: |
初始化
函数调用, 作用域内无原型
赋值
函数调用, 作用域内无原型
问题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) 这样的 ``缩写", 尽管完全合法, 但被一些人认为是不好的风格 。
问题4:
NULL 是什么, 它是怎么定义的?
答案:
作为一种风格, 很多人不愿意在程序中到处出现未加修饰的 0。因此定义了预处理宏 NULL (在 <stdio.h> 和其它几个头文件中) 为空指针常数, 通常是 0 或者 ((void *)0)。希望区别整数 0 和空指针 0 的人可以在需要空指针的地方使用 NULL。
使用 NULL 只是一种风格习惯; 预处理器把所有的 NULL 都还原回 0, 而编译还是依照 上文的描述处理指针上下文的 0。特别是, 在函数调用的参数里, NULL 之前 (正如在 0 之前) 的类型转换还是需要。
NULL 只能用作指针常数;
问题5:
在使用非全零作为空指针内部表达的机器上, NULL 是如何定义的?
答案:
跟其它机器一样: 定义为 0 。
当程序员请求一个空指针时, 无论写 ``0" 还是 ``NULL", 都是有编译器来生成适合机器的空指针的二进制表达形式。因此, 在空指针的内部表达不为 0 的机器上定义 NULL 为 0 跟在其它机器上一样合法:编译器在指针上下文看到的未加修饰的 0 都会被生成正确的空指针。
问题6:
如果 NULL 定义成 #define NULL ((char *)0) 难道不就可以向函数传入不加转换的 NULL 了吗?
答案:
一般情况下, 不行。复杂之处在于, 有的机器不同类型数据的指针有不同的内部表达。这样的 NULL 定义对于接受字符指针的的函数没有问题, 但对于其它类型的指针参数仍然有问题 (在缺少原型的情况下), 而合法的构造如 FILE *fp = NULL;则会失败。
不过, ANSI C 允许 NULL 的可选定义 #define NULL ((void *)0)。
除了潜在地帮助错误程序运行 (仅限于使用同样类型指针的机器, 因此帮助有限) 以外, 这样的定义还可以发现错误使用 NULL 的程序。
问题7:
如果 NULL 和 0 作为空指针常数是等价的, 那我到底该用哪一个呢?
答案:
许多程序员认为在所有的指针上下文中都应该使用 NULL, 以表明该值应该被看作指针。另一些人则认为用一个宏来定义 0, 只不过把事情搞得更复杂, 反而令人困惑。因而倾向于使用未加修饰的 0。没有正确的答案。
C 程序员应该明白, 在指针上下文中 NULL 和 0 是完全等价的, 而未加修饰的 0 也完全可以接受。任何使用 NULL (跟 0 相对) 的地方都应该看作一种温和的提示, 是在使用指针; 程序员 (和编译器都) 不能依靠它来区别指针 0 和整数 0。
在需要其它类型的 0 的时候, 即便它可能工作也不能使用 NULL, 因为这样做发出了错误的格式信息。(而且, ANSI 允许把 NULL 定义为 ((void *)0), 这在非指针的上下文中完全无效。特别是, 不能在需要 ASCII 空字符 (NUL) 的地方用 NULL。如果有必要, 提供你自己的定义
问题8:
这有点奇怪。NULL 可以确保是 0, 但空 (null) 指针却不一定?
答案:
随便使用术语 ``null" 或 ``NULL" 时, 可能意味着以下一种或几种含义:
""
)。在 C 中使用空串这个术语可能令人困惑, 因为空串包括空字符 ('\0'
),但 不包括空指针、 问题9:
为什么有那么多关于空指针的疑惑?为什么这些问题如此经常地出现?
答案:
C 程序员传统上喜欢知道很多 (可能比他们需要知道的还要多) 关于机器实现的细节。空指针在源码和大多数机器实现中都用零来表示的事实导致了很多无根据的猜测。而预处理宏 (NULL) 的使用又似乎在暗示这个值可能在某个时刻或者在某种怪异的机器上会改变。``if(p == 0)" 这种结构又很容易被误认为在比较之前把 p 转成了整数类型, 而不是把 0 转成了指针类型。最后, 术语 ``空" 的几种用法 之间的区别又可能被忽视。
冲出这些迷惘的一个好办法是想象 C 使用一个关键字 (或许象 Pascal 那样, 用 ``nil") 作为空指针常数。编译器要么在源代码没有歧义的时候把 ``nil" 转成适当类型的空指针, 或者有歧义的时候发出提示。现在事实上, C 语言的空指针常 数关键字不是 ``nil" 而是 ``0", 这在多数情况下都能正常工作, 除了一个未加修饰的 ``0" 用在非指针上下文的时候, 编译器把它生成整数 0 而不是发出错误信息, 如果那个未加修饰的 0 是应该是空指针常数, 那么生成的程序不行。
问题10:
说真的, 真有机器用非零空指针吗, 或者不同类型用不同的表达?
答案:
至少 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 位的某些位表示一个字节在一个字中的偏移。
文章评论(0条评论)
登录后参与讨论