Menu

C 语言空指针:NULL 的含义和判空方法

NULL 是指针不指向任何东西时保存的地址。本页介绍 NULL 到底是什么,为什么解引用它会崩溃,它和 0 以及未初始化指针有什么区别,以及让空指针 bug 远离你代码的防御性写法。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

指针总是保存着某个值。当它暂时没有可以指向的对象时(还没赋值时、它指向的东西被释放后,或者函数没有结果可以返回时),就需要一个明确表示“这里什么也没有”的值。这个值就是空指针,而 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。

Coddy 编程语言插图

和 Coddy 一起学编程

开始