条款8:优先选用 nullptr, 而非 NULL 或者0
首先, 我们需要开门见山地指出, 0 和 NULL 都不具备指针类型, 它们的本质是整数,只是依赖于上下文(Context)被“特殊看待”为空指针
- 0 的类型是 int:它是一个整数字面常量。C++ 只是在语言层面做一个不得已的“变通”:仅在上下文强制要求指针的语境中,才将 0 勉强解释为空指针。但在重载决议等情况下,它的“真实身份”——int——会优先被匹配。
- NULL 的类型是整型:NULL 是一个预处理宏,它在 C++ 中通常被定义为 0(一个 int)或 0L(一个 long)。因此,NULL 也不是指针类型,它是一个整型字面量。
不区分三者最大的隐患:重载决议失败
假设有三个重载函数:
1 | void f(int); // 重载版本 #1 |
- 调用 f(0):由于 0 的类型是 int,它与 f(int) 构成完美匹配。因此,编译器会毫不犹豫地选择调用重载版本 #1。它永远不会调用 f(void*)。
- 调用 f(NULL):由于 NULL 通常被定义为 0(一个 int),这与调用 f(0) 的情况完全相同。编译器依然会选择 f(int)。
这完全违背了程序员的意图。程序员传入 0 或 NULL 的本意是想传递一个“空指针”,希望调用版本 #3,但结果却调用了 int 版本。
C++11 引入的 nullptr 解决了这个问题。nullptr 的类型不是整型。它的实际类型是 std::nullptr_t, 并且 std::nullptr_t 可以隐式转换为所有裸指针类型(如 void, int, Widget* 等)。
当调用 f(nullptr) 时,nullptr 无法被匹配为 f(int) 或 f(bool)。但它可以被隐式转换为 void*,因此它精确地匹配并调用了重载版本 #3,这完全符合程序员的意图。
提升代码清晰度:auto 与 nullptr
在现代 C++ 中,auto 的广泛使用使得 0 和 NULL 的歧义性更加突出。例如:
1 | auto result = findRecord(/* ... */); |
分析 result == nullptr:这行代码则毫无歧义。它能通过编译的唯一前提是 result 必须是一个指针类型(或智能指针等支持与 nullptr 比较的类型)。这极大地提升了代码的清晰度。
保证模板类型推导的正确性
0 和 NULL 最大的失败在于模板类型推导。当它们被传递给模板时,编译器会推导它们的“真实”类型(即整型),而不是程序员“期望”的空指针。这种类型上的差异导致了在泛型代码中(如模板函数)传递空指针意图时,只有 nullptr 能够保证类型安全和编译成功。
???