看到 nullptr,很多人第一反应会把话题归到 C++。但这次讨论的重点其实是 C23:它正式把 nullptr 关键字和对应的 nullptr_t 类型引入了 C 语言。问题也随之而来:这项改动到底是在补真实短板,还是只是把 C++ 的做法搬了过来?
要回答这个问题,不能只看新语法本身,还得回到 NULL、空指针、空指针常量这些老概念,再结合 C++ 当年为什么要发明 nullptr。把这些背景理顺之后,才能判断在自己的项目里该不该用、值不值得用。
NULL 到底是什么
NULL 是 C 语言里预定义的“空指针常量”,通常定义在 以及其他多个头文件中。

这里先区分两个容易混淆的概念:
- 空指针:某种指针类型的一个特殊值,表示它不指向任何对象或函数。
- 空指针常量:一种常量表达式,它可以转换成任意指针类型,并得到该类型的空指针。
换句话说,空指针本身一定是指针类型;空指针常量则不一定是指针类型,但它必须能生成空指针。
在 C23 之前,C 对“空指针常量”的定义有两种:
- 值为零的整数常量表达式
- 值为零的整数常量表达式,并显式转换为
void *类型
从 C23 开始,第三种形式也被纳入标准:
nullptr
因为第一种定义只要求“值为零的整数常量表达式”,并没有限定具体类型,所以 0、0L、0U、(char)0 都算;进一步说,1 - 1、-0 这样的表达式也符合定义。
POSIX 只提到了第二种定义。至于具体实现,主流标准库常见的写法是把 NULL 定义为 ((void*)0),例如 gcc 和 clang 常见环境就是如此;但也有实现会为了兼容 C++ 等原因把 NULL 定义为 0。
C++ 当年为什么一定要引入 nullptr
nullptr 最早来自 C++11。要理解它为什么出现,关键不是语法,而是 C++ 对空指针常量和类型转换的规定与 C 不同。

在 C++11 之前,C++ 中“空指针常量”只有一种标准定义:值为零的整数字面量。这比 C 更严格,像 1 - 1 和 -0 这样的表达式在 C++ 里就不算空指针常量。
更关键的是,C++ 没有沿用 C 中那条“void * 也能作为空指针常量”的思路。因为在 C++ 里,任意对象指针都可以隐式转换为 void *,但反过来不行。像下面这样的代码,在 C++ 中不能通过编译:
int *p = (void *)0;
这意味着 void * 类型的零值,在 C++ 里失去了“可无条件变成任意指针”的资格,也就不适合继续承担空指针常量的角色。
于是很多 C++ 实现把 NULL 定义成 0。而在 gcc 和 clang 中,还存在一个非标准扩展 __null:它被视作字面量,值为零,类型是整数,大小与 void * 相同;在 LP64 数据模型中,它等同于 0L。不过当 __null 用于算术运算时,编译器会给出警告。
真正把问题暴露出来的是 C++ 的函数重载。看下面这段代码:
#include
#ifdef NULL
#undef NULL
#endif
#define NULL 0
void fn(int arg) {
std::cout << "int: " << arg << std::endl;
}
void fn(const char *arg) {
std::cout << "string: " << (arg arg : "nil") << std::endl;
}
int main() {
const char *s = NULL;
fn(s);
fn(NULL);
return 0;
}
fn(s) 会调用指针版本,这没有争议。但 fn(NULL) 按直觉像是在传空指针,标准却会把它当成整数常量,结果更匹配 fn(int)。也就是说,“直接传 NULL” 和 “传一个空指针变量” 在重载解析里可能不是一回事。如果用 gcc/clang 的 __null,这里通常又会变成“调用有歧义”。
这正是 nullptr 诞生的直接原因:它专门表示空指针常量,可以隐式转换成任意指针类型,但不能再和整数混用,从根上避开重载与类型推断里的歧义。
nullptr 是关键字,不需要包含任何头文件;它还有一个专属类型 nullptr_t,定义在 和 中:
using nullptr_t = decltype(nullptr);
C23 把 nullptr 带进 C,主要想解决什么
回到 C 语言,其实要先看 NULL 在 C 里究竟有没有现实问题。

- 如果
NULL定义为((void*)0),那么在 C 中通常不会出现 C++ 那类问题,因为 C 允许void *与其他对象指针之间进行隐式转换。 - 如果
NULL定义为0,那在绝大多数普通代码里也没事;但只要场景对“类型”和“实参宽度”足够敏感,就可能出问题。
这也是 C23 引入 nullptr 的背景:它不是为了修复所有 C 代码,而是为那些“NULL 恰好被定义为整数零,且代码又依赖精确类型语义”的场景提供更明确的写法。
场景一:_Generic 会把 NULL 当成整数
先看 _Generic:
#include
#ifdef NULL
#undef NULL
#endif
#define NULL 0
void fn_int(int arg) {
printf("int: %dn", arg);
}
void fn_str(const char *arg) {
printf("string: %sn", arg arg : "nil");
}
#define FN(X) _Generic((X),
int: fn_int,
void *: fn_str,
char *: fn_str,
const char *: fn_str
)(X);
int main() {
const char *s = NULL;
FN(s);
FN(NULL);
return 0;
}
这里的 _Generic 模拟了“按类型分派”的效果:int 走 fn_int,字符串指针走 fn_str。问题在于,如果 NULL 被定义成 0,那么 FN(NULL) 匹配到的就是 int,而不是指针分支。
这和 C++ 重载里的问题本质类似:语义上你想表达空指针,类型系统却先把它认成了整数。
场景二:可变参数函数里的宽度风险
另一个更值得警惕的场景是可变参数函数:
#include
#include
#ifdef NULL
#undef NULL
#endif
#define NULL 0
void fn(int count, ...) {
va_list ptr;
va_start(ptr, count);
for (int iter = 0; iter < count; iter++) {
const char *str = va_arg(ptr, char *);
puts(str str : "nil");
}
va_end(ptr);
}
int main(void) {
const char *s = NULL;
fn(3, s, NULL, "hello, world");
return 0;
}
在 LP64 数据模型中,int 是 32 位,指针是 64 位。可变参数调用会发生默认参数提升,但整数提升最多只是把比 int 更小的整数提升到 int;如果你传入的本来就是 int,那它仍然是 32 位整数。
于是当 NULL 实际上传进去的是 0,而读取时你却用 va_arg(ptr, char *) 把它当作 64 位指针取出,就产生了类型和宽度不匹配的问题。即便在某些平台上由于调用约定或对齐规则,程序看起来还能“正常工作”,这依然是带有内存安全风险的未定义或不可靠行为。
从这个角度看,C23 的 nullptr 确实解决了一个真实问题:它让“这是空指针而不是整数零”这件事变得更明确,也能避免在某些边角场景里把整数当指针传递。
C 里的 nullptr 和 nullptr_t 有哪些语义
在 C23 中,nullptr 的类型是 nullptr_t,定义在 中。它可以隐式转换成任意指针类型,从而得到对应类型的空指针;同时它不能和整数自由互转。
标准还给了它几个很关键的性质:
nullptr_t的大小和对齐方式与void *相同。nullptr_t只有一个合法值,也就是nullptr。- 值为
nullptr的对象,与值为(void *)0的对象具有相同的数据格式。
这意味着,至少从对象表示的角度看,它与传统空指针表示保持了非常强的兼容关系。也正因为这样,很多人会觉得它本质上就是“加了类型限制的 ((void*)0)”。
它有用,但真的有必要吗
如果把“是否有用”和“是否有必要”拆开看,结论会更清楚。
先说有用。 从类型约束的角度,nullptr 的确更严格,也更明确。它专门表示空指针,不容易被误用成整数零;在 _Generic、可变参数、类型推导等敏感场景里,确实能减少歧义。
但再问有没有必要,答案就未必一样。
- 从功能上看,很多需要
nullptr的地方,本来就可以通过((void*)0)避开问题。 - 从兼容性看,标准委员会选择新增关键字和新类型,而不是直接统一
NULL的定义,这保住了编译器对旧代码的兼容,却牺牲了新代码对旧编译器的兼容。使用nullptr的 C23 代码,无法在老编译器上直接通过。 - 从收益看,问题只会出现在
NULL恰好是0且代码又依赖特殊类型语义的少数场景,很多项目可能长期都碰不到。 - 从语言设计看,C 一向强调简单和灵活。一个特性能不能进入语言,不只是看它“好不好”,还要看它是否值得增加新的复杂度。
- 从实际开发看,很多 C 程序员本来就更关注编译器和平台行为,而不是最新标准特性;有些代码甚至不会直接使用
NULL,而是按项目约束选择更底层的写法。
所以,一个比较稳妥的判断是:nullptr 在 C23 里有现实意义,但它解决的问题范围并不大,远不到“没有它就不行”的程度。
项目里到底该怎么选
如果只看语言语义,nullptr 的确更干净,也更不容易引发误判。但工程上做选择,不能只看语义,还得看编译器版本、目标平台和团队约束。
比较实际的建议是:
- 如果你用的是较新的工具链,项目本身也明确以 C23 为基线,那么使用
nullptr没问题,尤其是在_Generic、可变参数等对类型很敏感的代码里,它能表达得更直接。 - 如果你做的是公司项目、公共开源软件,或者目标平台上的编译器普遍偏旧,那么应优先考虑兼容性,此时不建议贸然引入
nullptr。 - 如果项目只是常规 C 代码,且
NULL在当前实现里本来就定义为((void*)0),那么为了这点收益专门切到新特性,价值通常不高。
归根结底,C23 引入 nullptr 不等于它马上会成为 C 项目的默认写法。对大多数人来说,更重要的不是“立刻改用它”,而是知道 NULL 在哪些边角情况下会暴露出类型问题,然后在需要时做出有依据的选择。







