这听起来像是一个愚蠢的问题,但由于在 C 中,NULL
的字面意思是
#define NULL 0
为什么它不能是有效的内存地址?为什么我不能取消引用它,为什么任何数据都不可能位于内存地址 0?
我确定这个问题的答案是类似于"内存的前 n 字节始终由内核保留"或类似的东西,但我在互联网上找不到这样的东西。
我推理的另一部分是,这不是独立于平台吗?我不能发明一种内存地址 0 可供进程访问的新架构吗?
取消引用NULL
是未定义的行为。任何事情都可能发生,大多数时候坏事都会发生。所以要害怕。
一些旧的架构(VAX ...)允许您拒绝NULL
。
C11标准规范(读作n1570)不要求NULL
指针全为零位(参见C FAQ Q5.17);它可以是别的东西,但它应该是一个永远无效的地址,所以不能被成功的malloc
或C11意义上的地址运算符(一元&
)获得。但是这样做更方便,并且在实践中大多数(但不是全部)C实现都这样做。
IIRC,在 Linux 上,你可以 mmap(2) 包含带有MAP_FIXED
(void*)0
的页面,但这样做是不明智的(例如,因为允许一个符合优化的编译器来优化NULL
的取消引用)。
所以(void*)0
实际上不是一个有效的地址(在具有一些MMU和虚拟内存运行足够好的操作系统的普通处理器上!),因为确定它是NULL
很方便,并且确保取消引用它会产生分段错误是很方便的。但这不是C标准所要求的(在今天的廉价微控制器上是错误的)。
C 实现必须提供某种方法来表示NULL
指针(并保证它永远不会是某个有效位置的地址)。这甚至可以通过约定来完成:例如,提供一个完整的 232字节地址空间,但承诺永远不使用地址 0(或您为NULL
分配的任何地址,也许是 42!
当NULL
恰好是可去腐蚀的时,子错误不会被分段错误捕获(因此 C 程序更难调试)。
我不能发明一种内存地址 0 可供进程访问的新架构吗?
你可以,但你不想这样做(如果你关心提供任何符合标准的C实现)。您更愿意将地址 0 设为NULL
。否则会使编写 C 编译器(和标准 C 库)变得更加困难。并使该地址无效,以至于在取消引用时出现分段错误,使调试(以及用 C 编码的用户的生命周期)更容易。
如果你梦想着奇怪的架构,请阅读Lisp机器(以及Rekursiv和iapx 432),并查看Liam Proven在FOSDEM2018的电路较少旅行的谈话。这确实很有启发性,而且是一次很好的演讲。
使地址零未映射,以便在程序尝试访问时发生陷阱,这是许多操作系统提供的便利。C 标准不要求它。
根据C标准:
NULL
不是任何对象或函数的地址。(具体来说,它要求NULL
不等于任何对象或函数的指针进行比较。- 如果确实将
*
应用于NULL
,则结果行为不是由标准定义的。
这对您来说意味着您可以使用NULL
作为指针不指向任何对象或函数的指示器。这是 C 标准为NULL
提供的唯一目的——使用的就是if (p != NULL)…
等测试。C 标准不保证如果在NULL
时使用*p
p
就会发生陷阱。
换句话说,C 标准不需要NULL
提供任何陷印功能。它只是一个与任何实际指针不同的值,前提是您有一个指针值,表示"不指向任何内容"。
通用操作系统通常专门安排地址 0 处的内存取消映射(它们的 C 实现将NULL
定义为(void *) 0
或类似的东西),以便在取消引用空指针时发生陷阱。当他们这样做时,他们被扩展了超出规范要求的C语言。它们故意从进程的内存映射中排除地址零,以使这些陷阱正常工作。
但是,C 标准并不要求这样做。C 实现可以自由地将内存保留在地址零映射的位置,并且当您将*
应用于空指针时,那里可能有数据,并且如果操作系统允许,您的程序可以读取和/或写入该数据。完成此操作后,它通常出现在旨在在操作系统内核(例如设备驱动程序、内核扩展或内核本身)或嵌入式系统或其他具有简单操作系统的特殊用途系统内运行的代码中。
空指针常量(NULL
) 的值为 0。 空指针值可能是 0 以外的值。 在转换过程中,编译器会将出现的空指针常量替换为实际的空指针值。
NULL
不表示"地址 0";相反,它表示一个明确定义的无效指针值,该值保证不指向任何对象或函数,并且尝试取消引用无效指针会导致未定义的行为。