46 / 80 · C++11 · 约 7 分钟
用 nullptr 表达空指针
nullptr 是具有独立类型的空指针字面量,不是整数零,也不是某种通用指针。它让重载选择和模板传参保留空指针语义,但不会让解引用变安全,更不能代替对象生命周期管理。
先区分值、类型与表示
C++11 引入的 nullptr 是一个纯右值,类型为 std::nullptr_t;这个类型本身既不是指针类型,也不是成员指针类型。它可以转换成各种指针类型的空值,包括对象指针、函数指针和成员指针。需要显式写出类型名时包含 <cstddef>。
空指针表示不指向对象或函数的特殊状态,而不是承诺机器上的每个比特都为零。因此初始化应写 int* p = nullptr,不能把清零内存当成可移植的指针初始化方式。判断空值用比较或条件判断,不应依赖地址打印格式。
为什么重载和模板更需要它
假设同时存在 choose(int) 与 choose(int*),传入字面量 0 会优先匹配整数重载,传入 nullptr 则选择指针重载。传统 NULL 的具体定义由实现决定,不能作为可靠的重载意图标记。现代代码中用 nullptr 表示空指针,用 0 表示数值零。
模板按值接收再转交参数时,整数零会被推导为整数;函数内部那个普通整数变量不是空指针常量。nullptr 被推导为 std::nullptr_t 后仍可转换为空指针。这不是模板的特殊魔法,而是独立类型保留下来的转换能力。
空值检查不是所有权检查
例子先断言两个重载的选择,再将 nullptr 经模板传给只接受指针的函数,最后给指针绑定一个仍存活的局部对象。所有解引用都发生在已知对象有效的范围内,因此不需要用危险代码演示错误。
接口允许缺省对象时,指针与 nullptr 能清楚表达可选借用;接口必须拿到对象时,引用往往更直接。但非空指针也可能悬空,if (p) 只检查空值,不证明对象存活。资源所有权应交给 RAII 或智能指针,不能靠调用前补一个空判断来修复。
容易答错的地方
- nullptr 不能解决两个不同指针重载之间的歧义;同时有 f(int*) 与 f(double*) 时,f(nullptr) 仍可能无法选择。
- 将已悬空的指针保留为非空值后,空值检查不会恢复其有效性;只有明确的所有权与生命周期关系才能避免悬空。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <cstddef>
#include <iostream>
#include <type_traits>
int choose(int) { return 1; }
int choose(int*) { return 2; }
bool is_empty(int* p) { return p == nullptr; }
template<class T>
bool relay(T value) { return is_empty(value); }
int main() {
static_assert(std::is_same<decltype(nullptr), std::nullptr_t>::value, "null type");
assert(choose(0) == 1);
assert(choose(nullptr) == 2);
assert(relay(nullptr));
int n = 7;
int* p = nullptr;
assert(!p);
p = &n;
assert(*p == 7);
std::cout << choose(nullptr) << ' ' << *p << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread modern-nullptr.cpp -o example && ./example预期结果
2 7
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
给 choose 增加 choose(double*) 后,如何仍明确调用 int* 重载?为什么不能直接继续传 nullptr?
查看参考答案
调用 choose(static_cast<int*>(nullptr)),或者先声明 int* p = nullptr 再调用 choose(p)。两个指针重载都能从 nullptr 获得同等级的空指针转换,没有更优候选;显式目标类型把选择依据补全,而不是改变空值本身。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。