指针总是保存着某个值。当它暂时没有可以指向的对象时(还没赋值时、它指向的东西被释放后,或者函数没有结果可以返回时),就需要一个明确表示“这里什么也没有”的值。这个值就是空指针,而 NULL 是你在代码中写的名字。
围绕它的规范很简单,却总能带来回报:指针没有目标时设为 NULL,解引用前检查是否为 NULL,没有结果时返回 NULL。
NULL 到底是什么
NULL 是一个宏,定义在 <stddef.h> 中,<stdio.h>、<stdlib.h>、<string.h> 等头文件也会引入它。它展开为一个空指针常量,通常是 ((void*)0) 或单纯的 0。
C 标准保证了最重要的性质:空指针与指向任何实际对象或函数的指针比较都不相等。同一类型的两个空指针比较时总是相等。
大多数系统会把空指针打印为 (nil) 或 0x0,也就是地址零。这只是常见的实现方式,并不是规则;标准从来没有保证它的二进制位全是零。标准保证的是比较结果,所以要写 p == NULL,不要写任何依赖其内部表示的代码。
解引用 NULL 会崩溃
这就是所有判空检查存在的原因:
int *p = NULL;
printf("%d\n", *p); // undefined behavior - almost certainly a crash
在 Windows、macOS 和 Linux 上,地址空间的第一页被有意设置为未映射,所以 CPU 会拦截这次访问,操作系统会终止程序。你会看到:
Segmentation fault (core dumped) # Linux
zsh: segmentation fault ./program # macOS
这种立即崩溃其实是一个特性。它把逻辑 bug 变成了一个响亮的失败,而且就发生在导致问题的那一行,而不是变成三周后才发现的悄无声息的数据损坏。如何解读崩溃信息并找到出错的行,请看“段错误”一节。
这也是为什么在 C 中,“空指针”并不是一个可以捕获的异常。没有任何恢复机制,只能事先检查。
检查可能失败的操作
可能失败的函数会通过返回 NULL 来表示失败。检查并不是可有可无的礼貌,不检查的代价就是崩溃。
标准库中有很多这样的函数:malloc、calloc、realloc、fopen、strchr、strstr、getenv、bsearch。它们都用 NULL 表示“没有结果”。完整的内存分配内容请看“动态内存”一节。
检查有两种常见写法:
if (p == NULL) { /* handle */ } // explicit - preferred for clarity
if (!p) { /* handle */ } // terse - relies on NULL being falsy
在条件中,空指针被当作假,有效指针被当作真,所以两种写法都能用。当陌生人在你的代码中查找 bug 时,显式的写法更容易读懂。
NULL、0 与未初始化
初学者常把这三种不同的情况混为一谈:
int *a = NULL; "points at nothing" - testable, safe to check
int *b; uninitialized - holds garbage, NOT testable
int *c = &x; points at a real object
危险的是 b。它的字节是那个栈位置上残留的内容,可能是零(看起来没问题),也可能是之前某次调用留下的、看似合理的地址。没有任何检查能把它和有效指针区分开,而且它的行为在调试版本和发布版本之间还会变化。
初始化每一个指针。 如果暂时还没有地址,那就用 NULL。用 -Wall -Wextra 编译,编译器会帮你标记出许多使用未初始化变量的地方。
关于 0:在指针上下文中,整数常量 0 就是空指针常量,所以 p = 0; 是合法的 C 代码。但还是优先用 NULL。它能表明意图,而且在编译器无法替你做转换的可变参数调用中很重要:
execl("/bin/ls", "ls", 0); // risky - may pass an int where a pointer is expected
execl("/bin/ls", "ls", (char *)NULL); // correct
C23 新增了 nullptr,这是一个拥有独立类型的关键字,能彻底避免这类歧义;NULL 在任何地方仍然是正确且可移植的。
防御性写法
在接收指针的函数开头做检查。
释放后设为 NULL。 free 不会改变你的指针,它只会释放内存。留下的过期指针叫作悬空指针,使用它是未定义行为,而且往往不会立即崩溃。
这个例子中有两个事实值得牢记。free(NULL) 被明确规定为什么也不做,所以清理代码永远不需要为它做检查。而在 free 之后设置 p = NULL,会把释放后使用(可能会悄悄破坏数据)变成一次立即发生、便于调试的崩溃。
返回 NULL 表示“没有结果”,并写明这一点。
// returns a pointer to the matching element, or NULL if there is none
int *find(int *arr, int n, int target);
在函数上方的注释中写明这一点。知道可能返回 NULL 的调用者会写检查,不知道的就不会。
快速检查清单
- 初始化每一个指针,要么指向真实地址,要么设为
NULL。 - 使用
malloc、calloc、realloc和fopen的返回值前,先检查它。 - 任何可能收到错误输入的函数,都要在开头检查指针参数。
- 调用
free后立即把指针设为NULL。 - 凡是表示指针的地方,都写
NULL而不是0。 - 用
-Wall -Wextra构建,条件允许时在 sanitizer 下运行:gcc -fsanitize=address,undefined能捕获空指针解引用,并给出精确的报告。
常见问题
C 语言中的空指针是什么?
保存着空指针常量的指针,空指针常量保证与任何真实对象的地址比较都不相等。它是表示“这个指针暂时不指向任何东西”的标准方式,NULL 是代表它的宏,定义在 <stddef.h> 以及其他几个头文件中。
在 C 语言中解引用空指针会发生什么?
这是未定义行为。实际上,在桌面和服务器系统上,程序会立即因段错误而崩溃,因为地址零被有意设置为未映射。在没有内存保护的嵌入式系统上,它可能会悄悄读取或破坏某些数据,这要糟糕得多。
C 语言中 NULL 和 0 一样吗?
在指针上下文中是一样的:整数常量 0 就是空指针常量,所以 p = 0; 和 p == NULL 都能用。但指针要用 NULL,数字要用 0:这能告诉读代码的人你指的是哪一个,在可变参数函数调用中也更安全,因为单独的 0 可能会被当作 int 而不是指针传递。
空指针和未初始化的指针有什么区别?
空指针肯定不指向任何东西,你可以检测它。未初始化的指针保存的是那块内存里碰巧留下的字节,可能看起来像一个有效地址,所以没有任何检测能发现它。如果暂时没有更合适的值,就一定要把指针初始化为 NULL。