39、空指针与 NULL:NULL、0、nullptr 的区别
说实话,空指针这个问题,看着简单,但坑是真不少。我见过不少工作三五年的工程师,在面试时被问到「NULL、0、nullptr 到底有什么区别」,照样答得支支吾吾。
我自己刚入行那会儿也犯过糊涂。有一次在 ARM 裸机项目里,把一个整型 0 直接赋给了指针,结果编译器没报错,程序跑起来却莫名其妙地崩溃。查了两天才发现,是宏定义 NULL 在不同平台下被展开成了不同的东西。
嗯,咱们今天就把这个事彻底说清楚。
先看一个经典场景
你写 C 语言的时候,肯定写过这样的代码:
int *p = NULL;
if (p == NULL) {
// 处理空指针
}
看起来没问题,对吧?但问题在于——NULL 到底是什么?
在 C 语言标准里,NULL 被定义为:
#define NULL ((void *)0)
或者在某些实现里:
#define NULL 0
你看,问题就出在这里。NULL 可能是一个 (void *)0,也可能就是一个整型 0。这在 C 语言里勉强能混过去,但到了 C++ 里,麻烦就大了。
NULL、0、nullptr 的本质区别
我直接给你列个表,一目了然:
| 写法 | 类型 | 适用语言 | 典型问题 |
|---|---|---|---|
| NULL | 宏,展开为 (void*)0 或 0 |
C、C++ | 在 C++ 中可能被隐式转换为整型,导致重载歧义 |
| 0 | 整型字面量 | C、C++ | 可以赋值给指针,但语义不清晰,容易混淆 |
| nullptr | 关键字,类型为 std::nullptr_t |
C++11 起 | 只能隐式转换为指针类型,不会与整型混淆 |
说白了,nullptr 是 C++ 专门为了解决 NULL 的歧义而引入的。C 语言没有 nullptr,所以 C 里你只能用 NULL 或 0。
为什么 C++ 要搞个 nullptr 出来?
我给你看个例子,你就明白了。假设你有两个重载函数:
void func(int x) {
printf("调用了整型版本\n");
}
void func(char *p) {
printf("调用了指针版本\n");
}
然后你调用:
func(NULL); // 猜猜会调用哪个?
在 C++ 里,如果 NULL 被定义为 0,那么 func(NULL) 会调用 func(int),而不是 func(char *)!
为什么?因为 0 是整型,编译器优先匹配整型版本。你明明想传一个空指针,结果却调了整型函数。这种 bug 非常隐蔽,我当年在做一个通信协议栈的时候就被坑过一次,排查了整整一个下午。
而如果用 nullptr:
func(nullptr); // 明确调用 func(char *)
nullptr 的类型是 std::nullptr_t,它只能隐式转换为指针类型,不会和整型混淆。所以编译器会正确匹配指针版本。
C 语言里怎么办?
C 语言没有 nullptr,那是不是就只能忍着?
其实也不是。我个人习惯在 C 项目里统一用 NULL,并且确保 NULL 被定义为 (void *)0。这样至少能保证 NULL 是一个指针类型,而不是整型 0。
你可以这样检查:
#ifndef NULL
#define NULL ((void *)0)
#endif
不过要注意,C 语言里 void * 可以隐式转换为其他指针类型,所以 (void *)0 赋值给 int * 是合法的。但在 C++ 里不行,C++ 要求显式转换。这也是为什么 C++ 需要 nullptr 的另一个原因。
避坑指南
我曾经在嵌入式项目里见过这样的代码:
#define NULL 0
int *p = NULL;
if (p == 0) {
// 处理空指针
}
这段代码在 32 位平台上能跑,但移植到 64 位平台时,指针是 8 字节,而整型 0 是 4 字节。虽然大多数编译器会做隐式转换,但严格来说,这种写法是不安全的。
我的建议是:
- C 语言项目:统一用 NULL,并确保 NULL 定义为
(void *)0 - C++ 项目:永远用 nullptr,不要再用 NULL 或 0 表示空指针
- 混合项目:C 部分用 NULL,C++ 部分用 nullptr,接口处注意类型转换
memset 或 0 来初始化指针数组!memset(ptr_array, 0, sizeof(ptr_array)); 这种写法在大多数平台上能工作,但严格来说,全零的位模式不一定等于空指针。虽然现代平台基本都支持,但标准并没有强制要求。
知识体系图
下面这张图帮你理清三者之间的关系:
实际项目中的选择
我这些年做嵌入式项目,总结了一套自己的规则:
- 纯 C 项目(比如裸机程序、RTOS 驱动):用 NULL,并且在头文件里显式定义
#define NULL ((void *)0) - 纯 C++ 项目(比如用 STM32CubeIDE 写 C++ 应用):用 nullptr,禁用 NULL
- C/C++ 混合项目:在 C 代码里用 NULL,在 C++ 代码里用 nullptr,接口处用
static_cast<T*>(nullptr)做显式转换
总结
空指针这件事,说白了就是:C 语言里用 NULL,C++ 里用 nullptr,别混用。
0 虽然也能用,但语义不清晰,容易让人误解。你想想看,看到 int *p = 0; 和 int *p = nullptr;,哪个更直观?显然是后者。
嗯,今天就聊到这里。记住一点:写代码不只是给机器看的,更是给人看的。用正确的空指针表示方式,既是对自己负责,也是对后来维护代码的人负责。
公众号:蓝海资料掘金营,微信deep3321