nullptr 解决函数重载的二义性

面试官: 在 C++11 之前,我们通常使用一个宏 NULL 来表示空指针。而在 C++11 中,引入了一个新的关键字 nullptr 来专门表示空指针。

请问,我们为什么需要 nullptr?或者说,使用宏 NULL 在某些情况下,会带来什么样的问题或歧义,是 nullptr 能够解决的?

回答:

  • NULL 是一个宏,它除了表示空指针之外,更常用的含义是表示 0,一个整数。
  • 由于 NULL 既能表示空指针,又能表示 0,因此它会引发歧义。
  • 比如,某个函数重载了两个版本,一个的参数类型是 int,而另一个参数类型是 char*。当输入 NULL 时,通常会调用参数为 int 的那个版本。
  • 而使用 nullptr 则规避了这一风险。nullptr 是一个真正的空指针常量,它是一个特殊的类型 std::nullptr_t,可以隐式地转换为任何的指针类型,而不会和 int 类型混淆。

面试官 (总结与肯定): 你说的完全正确。我们来把这个过程再详细地梳理一遍。nullptr 诞生的核心原因解决函数重载时的二义性

NULL 的“前世今生” NULL 是一个定义在 <cstddef> 等头文件中的预处理宏。它的具体定义取决于编译器,但通常是下面两种之一:

1
2
3
4
5
#define NULL 0
// 或者
#define NULL 0L
// 或者在 C++ 编译器中,有时甚至是
#define NULL nullptr // 这是现代编译器的“向后兼容”行为

在 C++98/03 的时代,你可以认为 NULL 就是一个值为 0整型常量

问题所在:函数重载 正如你所举的例子,当我们有这样的函数重载时:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void func(int i) {
    std::cout << "Called func(int)\n";
}

void func(char* p) {
    std::cout << "Called func(char*)\n";
}

int main() {
    func(NULL); // 问题出在这里
}
  1. func(NULL) 被调用。
  2. 预处理器将 NULL 直接文本替换0。代码变成了 func(0);
  3. 编译器现在需要为 func(0) 选择一个最佳的重载版本。
  4. 0 是一个整型字面量 (integer literal)
  5. 在 C++ 的重载决议规则中,int 版本的匹配优先级,要高于“整型 0 转换为空指针”的匹配优先级
  6. 因此,编译器选择了 func(int)

输出Called func(int) 这完全违背了程序员“我想传递一个空指针”的意图。

nullptr 如何解决问题

  • nullptr 是 C++11 引入的一个关键字,它不是宏。
  • 它有一个特殊的类型,叫做 std::nullptr_t
  • 这个 std::nullptr_t 类型,可以隐式地转换任何指针类型int*, char*, TreeNode*…),但它不能隐式地转换为任何整型(如 int, long)。

所以,如果我们调用 func(nullptr);

  • 编译器看到 nullptr
  • 它尝试匹配 func(int) -> 失败!nullptr 不能转成 int
  • 它尝试匹配 func(char*) -> 成功!nullptr 可以转成 char*
  • 最终,编译器毫无歧义地选择了 func(char*)

黄金法则:

在现代 C++ 代码中,永远使用 nullptr,绝不使用 NULL

你对这个核心区别的理解已经非常到位了。


类型推导中的 nullptrNULL

面试官 (深入提问): 好的,我们知道了 nullptr 在函数重载时的巨大优势。

我们再来看一个与模板类型推导相关的问题。auto 和模板函数在推导类型时,会如何处理 NULLnullptr

请看下面这段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <typeinfo> // 用于查询类型信息

template<typename T>
void print_type(T arg) {
    std::cout << typeid(arg).name() << std::endl;
}

int main() {
    auto p1 = NULL;
    auto p2 = nullptr;

    print_type(NULL);
    print_type(nullptr);

    // 请问,p1 和 p2 的类型分别是什么?
    // print_type 输出的两行,又分别是什么?
}

(提示:typeid(...).name() 的输出结果是编译器相关的,不完全标准,但足以区分类型。比如 int 可能是 ilong 可能是 lstd::nullptr_t 可能是 Dn。)

回答:

  • 我认为,p1 的类型被推导为 intp2 的类型被推导为 std::nullptr_t
  • 输出的两行分别是 iDn

面试官 (解释与确认):

你的判断非常精准。这个例子进一步揭示了 NULLnullptr 在类型系统中的本质区别。

1. auto p1 = NULL;

  • NULL 在大多数实现中,就是一个值为 0 的宏。
  • auto 在推导 0 这个整型字面量时,会根据 0 的具体类型来决定。0 本身是 int 类型。
  • 因此,p1 的类型被推导为 int
  • print_type(NULL) 也就是 print_type(0),模板参数 T 被推导为 int。输出结果在不同编译器上可能是 i (GCC/Clang) 或 int (MSVC)。

2. auto p2 = nullptr;

  • nullptr 的类型是 std::nullptr_t
  • 因此,p2 的类型被推导为 std::nullptr_t
  • print_type(nullptr),模板参数 T 被推导为 std::nullptr_t。输出结果可能是 Dn (GCC/Clang) 或 std::nullptr_t (MSVC)。

这个区别为什么重要? 因为它再次表明,NULL 在 C++ 的类型系统中,其本质就是一个整数。如果你把它用在泛型代码(模板、auto)中,它就会暴露出自己“整数”的真面目,导致意想不到的类型推导错误。

nullptr 则始终保持其“空指针常量”的纯粹身份,无论是在重载决议还是模板推导中,都能保证正确的、符合指针语义的行为。

你对 nullptr 的理解已经非常深入了。


nullptr 是纯右值

面试官 (最终问题): 好的,最后一个小问题,作为一个知识点的补充。

nullptr 既然有自己的类型 std::nullptr_t,那么它是一个左值还是右值?

1
2
// 这行代码能编译通过吗?
auto ptr = &nullptr; 

回答:

  • 回顾左值和右值的区别:左值可以取地址,而右值不行。
  • nullptr 是不能取地址的,它只是一个作为空指针而存在的临时量,所以,nullptr 是右值。

面试官 (总结与肯定):

你说得对极了。nullptr 是一个纯右值 (prvalue)

因此,auto ptr = &nullptr; 这行代码是无法编译通过的,因为我们不能对一个右值取地址。

这进一步加强了 nullptr 作为一种特殊字面量 (literal) 的概念,它和 10, true, 'a' 这些字面量一样,都是存在于代码中的值,而不是存储在特定内存位置的具名对象。


总结

今天我们系统地辨析了 nullptrNULL 的区别,这也是现代 C++ 程序员必须掌握的基础知识。

  • 我们理解了 NULL 作为宏,在函数重载类型推导中存在的二义性问题。
  • 我们学习了 nullptr 作为带类型的关键字,如何通过更严格的类型规则来解决这些问题。
  • 我们还确认了 nullptr 是一个右值

你今天的表现非常出色,能够快速理解概念,并进行准确的逻辑推理。继续保持!