C++ 空指针 NULL 和 nullptr 区别

在 C/C++ 开发中,空指针是经常听到的概念,用于表示指针当前不指向任何有效的对象或函数。在实际使用时,很多初学者会发现,表示空指针的东西有两个:NULL 和 nullptr。于是很容易认为,NULL 和 nullptr 是等价的,只是写法不同,可以随意混用。实际上,二者存在本质区别。下面,我们从 C 语言开始,逐步理解 NULL 的由来,以及 C++ 为什么最终又引入了 nullptr。

1. NULL

C 语言标准规定,空指针常量有两种合法形式:整数常量 0,或者 (void *)0。因此,用这两种形式给指针变量赋值,都可以得到空指针。所以,C 语言标准并没有强制规定 NULL 必须采用哪一种形式,它既可以定义为 0,也可以定义为 (void *)0。实际编译器或标准库实现通常会选择 (void *)0,主要原因是更符合“NULL 是指针”的直觉。

为什么是 void*,而不是其他类型的,比如:(char*)0 呢?

因为 NULL 需要能够赋值给任意类型的对象指针。如果把 NULL 定义为 (char *)0,那么 int *p = NULL 实际上就是把 char* 赋给 int*。虽然 C 语言允许不同对象指针类型之间进行转换,但这种转换可能产生编译器警告。而 void * 是 C 语言中的通用对象指针类型,C 语言允许 void * 与任意对象指针类型之间进行隐式转换,通常不需要显式类型转换。所以,(void *)0 比 (char *)0 更适合作为通用的空指针表示。


到了 C++98/03 中,规则发生了变化。void* 是无类型指针,把带类型的指针存入 void*,只是丢掉类型标记,风险很小。反过来从 void* 还原成带类型指针时,编译器已经不知道 void* 当初存放的是什么类型,无法校验类型是否匹配,很容易产生隐蔽的内存错误。所以,C++ 禁止将 void* 隐式转换为其他类型的对象指针,必须由程序员写出显式强制转换,主动承担风险。

如果继续沿用 C 的方式,把 NULL 定义成 (void*)0,那么 int* p = NULL 就等价于 int* p = (void*)0,需要执行 void* 到 int* 的隐式转换,会直接编译报错。因此,在 C++98/03 中,(void*)0 已经不属于空指针常量,只有值为 0 的整型常量表达式才是空指针常量,即:#define NULL 0


#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

2. nullptr

在 C++98/03 中,NULL 既可以用于表示空指针,又具有整数的身份,这种设计在普通的指针赋值中通常没有问题,但在一些需要区分整数和指针的场景中,就会产生歧义。比如函数重载、函数模板的类型推导等。

#include <iostream>
using namespace std;

void process(int value)
{
    cout << "函数重载:整数" << endl;
}
void process(int* ptr)
{
    cout << "函数重载:指针" << endl;
}

int main()
{
    process(NULL);
    return 0;
}
函数重载:整数

C++98/03 中,如何规避 NULL 的问题?

严格来说,C++98/03 并没有从语言层面真正解决 NULL 带来的问题。为了避免整数 0 和空指针之间的歧义,只能通过一些编码方式进行规避,例如显式类型转换、自定义空指针类型,或者通过宏对 NULL 进行一些封装。

这些方式虽然能够在特定场景下规避问题,但要么使用比较繁琐,要么缺乏通用性,都无法从语言层面解决整数 0 与空指针之间的歧义。因此,C++11 干脆从语言层面引入了专门表示空指针的 nullptr,从根本上解决了这个问题。

nullptr 是 C++11 引入的专用空指针字面量,它拥有独立的 std::nullptr_t 类型,可以转换为任意指针类型,但不能隐式转换为整数类型,从而从语言层面彻底解决了 0 既表示整数又表示空指针所带来的歧义。

在现代 C++ 中,如果需要明确表示空指针,推荐使用 nullptr,而不是 NULL。