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*)00 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,接口处注意类型转换
特别注意:不要用 memset0 来初始化指针数组!
memset(ptr_array, 0, sizeof(ptr_array)); 这种写法在大多数平台上能工作,但严格来说,全零的位模式不一定等于空指针。虽然现代平台基本都支持,但标准并没有强制要求。

知识体系图

下面这张图帮你理清三者之间的关系:

空指针表示方式对比 NULL 0 nullptr 宏定义 展开为 (void*)0 或 0 C/C++ 均可用 ⚠ 重载歧义风险 整型字面量 类型为 int 可隐式转指针 ⚠ 语义不清晰 关键字 类型为 nullptr_t 仅 C++11+ ✅ 类型安全 核心结论 C 语言用 NULL(确保为 void* 类型) | C++ 语言用 nullptr

实际项目中的选择

我这些年做嵌入式项目,总结了一套自己的规则:

  • 纯 C 项目(比如裸机程序、RTOS 驱动):用 NULL,并且在头文件里显式定义 #define NULL ((void *)0)
  • 纯 C++ 项目(比如用 STM32CubeIDE 写 C++ 应用):用 nullptr,禁用 NULL
  • C/C++ 混合项目:在 C 代码里用 NULL,在 C++ 代码里用 nullptr,接口处用 static_cast<T*>(nullptr) 做显式转换
一个小技巧:如果你在 C++ 项目里不小心用了 NULL,编译器通常会给出警告。我建议你把警告级别开到最高(-Wall -Wextra -Werror),这样 NULL 的歧义问题在编译阶段就能暴露出来。

总结

空指针这件事,说白了就是:C 语言里用 NULL,C++ 里用 nullptr,别混用

0 虽然也能用,但语义不清晰,容易让人误解。你想想看,看到 int *p = 0;int *p = nullptr;,哪个更直观?显然是后者。

嗯,今天就聊到这里。记住一点:写代码不只是给机器看的,更是给人看的。用正确的空指针表示方式,既是对自己负责,也是对后来维护代码的人负责。


公众号:蓝海资料掘金营,微信deep3321