nullptr 与空指针
定义与作用
C++11 引入了空指针字面量 nullptr,类型为 std::nullptr_t。它的出现解决了 C 风格空指针常量 NULL 和 0 的两大痛点:
- 类型歧义:
NULL通常是0的宏定义,在重载决议中会被解析为int而非指针 - 模板推导混乱:
NULL作为模板参数时被推导为int,与指针预期不符
nullptr 可以隐式转换为任何指针类型或成员指针类型,但不能隐式转换为整型——这从根本上消除了歧义。
核心原理
重载决议对比
重载决议示例
void f(int); // 重载 1:整型
void f(char*); // 重载 2:字符指针
f(NULL); // 调用 f(int) — 大概率不是你的意图
f(nullptr); // 调用 f(char*) — 这就是你要的
nullptr_t 类型关系
完整示例
示例一:空指针重载陷阱与 nullptr 的解决方案
场景说明:一个命令行处理系统,有指针版本和整型版本的函数重载。用 NULL 会误调用整型版本。
#include <iostream>
#include <string>
class CommandProcessor {
public:
// 整型重载:处理数值型命令 ID
void execute(int cmd_id) {
std::cout << "[整型版本] 执行命令 ID: " << cmd_id << std::endl;
}
// 指针重载:通过字符串命令名执行
void execute(const char* cmd_name) {
if (cmd_name) {
std::cout << "[指针版本] 执行命令: " << cmd_name << std::endl;
} else {
std::cout << "[指针版本] 空命令,执行默认行为" << std::endl;
}
}
// double 重载(添加更多歧义场景)
void execute(double value) {
std::cout << "[浮点版本] 处理值: " << value << std::endl;
}
};
template <typename T>
void log_pointer_type(T* ptr) {
if (ptr == nullptr) {
std::cout << "空指针(nullptr)" << std::endl;
} else {
std::cout << "有效指针: " << static_cast<const void*>(ptr) << std::endl;
}
}
int main() {
CommandProcessor proc;
std::cout << "===== 重载决议对比 =====" << std::endl;
std::cout << "调用 execute(NULL):" << std::endl;
proc.execute(NULL); // 危险!大概率调用 execute(int),传入 0
// 输出: [整型版本] 执行命令 ID: 0
std::cout << "调用 execute(nullptr):" << std::endl;
proc.execute(nullptr); // 正确!调用 execute(const char*)
// 输出: [指针版本] 空命令,执行默认行为
std::cout << "\n===== 显式调用对比 =====" << std::endl;
const char* str = "status";
proc.execute(str); // 明确调用指针版本
int cmd = 100;
proc.execute(cmd); // 明确调用整型版本
// NULL 和 nullptr 的模板推导差异
std::cout << "\n===== 模板推导 =====" << std::endl;
log_pointer_type(nullptr); // T 推导为某种类型,指针值为空
// sizeof 差异
std::cout << "\n===== 类型大小 =====" << std::endl;
std::cout << "sizeof(NULL): " << sizeof(NULL) << " (通常是 sizeof(int))" << std::endl;
std::cout << "sizeof(nullptr): " << sizeof(nullptr) << " (平台相关)" << std::endl;
// nullptr 的类型判断
std::cout << "nullptr 是指针? "
<< std::is_pointer<decltype(nullptr)>::value << std::endl;
std::cout << "nullptr 是 nullptr_t? "
<< std::is_null_pointer<decltype(nullptr)>::value << std::endl;
return 0;
}
预期输出:
===== 重载决议对比 =====
调用 execute(NULL):
[整型版本] 执行命令 ID: 0
调用 execute(nullptr):
[指针版本] 空命令,执行默认行为
===== 显式调用对比 =====
[指针版本] 执行命令: status
[整型版本] 执行命令 ID: 100
===== 模板推导 =====
空指针(nullptr)
===== 类型大小 =====
sizeof(NULL): 4 (通常是 sizeof(int))
sizeof(nullptr): 8 (平台相关)
nullptr 是指针? false
nullptr 是 nullptr_t? true
逐段分析:
proc.execute(NULL)调用的是execute(int),因为NULL展开为0(或__null),被优先解析为整型proc.execute(nullptr)调用execute(const char*),因为nullptr不能隐式转为 int,只能转指针- 模板函数
log_pointer_type中nullptr正确推导为指针 std::is_null_pointer可以检测std::nullptr_t类型(C++14 起)sizeof(nullptr)在 64 位平台通常为 8 字节
示例二:智能指针与 nullptr 的配合
#include <iostream>
#include <memory>
#include <vector>
struct Task {
int id;
std::string description;
Task(int i, std::string d) : id(i), description(std::move(d)) {}
void run() const {
std::cout << "运行任务 #" << id << ": " << description << std::endl;
}
};
// 用 nullptr 表示"未找到任务"
std::unique_ptr<Task> find_task(const std::vector<std::unique_ptr<Task>>& tasks,
int target_id) {
for (const auto& t : tasks) {
if (t && t->id == target_id) {
// 不能直接返回 t(它是 const& unique_ptr)
return nullptr; // 简化处理:实际项目中可能需特殊设计
}
}
return nullptr; // 清晰表达"未找到"
}
int main() {
std::vector<std::unique_ptr<Task>> tasks;
tasks.push_back(std::make_unique<Task>(1, "编译前端"));
tasks.push_back(std::make_unique<Task>(2, "代码生成"));
tasks.push_back(std::make_unique<Task>(3, "链接"));
// nullptr 用于检查
auto task = find_task(tasks, 2);
if (task == nullptr) {
std::cout << "任务未找到" << std::endl;
} else {
task->run();
}
// 安全的空指针检查
for (const auto& t : tasks) {
if (t != nullptr) { // 明确检查,替代 if(t) 也合法
t->run();
}
}
// nullptr 用于重置智能指针
auto ptr = std::make_unique<Task>(99, "测试");
std::cout << "\n重置前: " << (ptr ? "有值" : "空") << std::endl;
ptr = nullptr; // 释放资源并将 ptr 置空
std::cout << "重置后: " << (ptr ? "有值" : "空") << std::endl;
// shared_ptr 同理
auto sptr = std::make_shared<Task>(100, "共享任务");
if (sptr != nullptr) {
sptr->run();
}
return 0;
}
预期输出:
任务未找到
运行任务 #1: 编译前端
运行任务 #2: 代码生成
运行任务 #3: 链接
重置前: 有值
重置后: 空
运行任务 #100: 共享任务
逐段分析:
- 现代 C++ 中
nullptr与智能指针无缝配合:== nullptr、!= nullptr、= nullptr if (ptr)和if (ptr != nullptr)都可判断智能指针是否为空(推荐显式写法)ptr = nullptr会先释放智能指针持有的资源再置空nullptr让返回值语义明晰:函数返回空智能指针时直接return nullptr
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
NULL 在重载函数中误匹配 int | 调用了不该调用的重载版本 | 用 nullptr |
NULL 作为模板实参被推导为 int | 泛型代码行为异常 | 用 nullptr |
老代码中 if (ptr == NULL) | 混淆(但通常不会出错) | 改为 if (ptr == nullptr) |
用 0 或 '\0' 表示空指针 | 语义模糊 | 统一用 nullptr |
忘记检查 nullptr | 空指针解引用导致崩溃 | 智能指针用 if (ptr)、原始指针判空 |
常见面试问题
nullptr有什么优势于NULL和0?——类型安全:nullptr是std::nullptr_t类型,只能转为指针,不会意外匹配整型重载;避免模板推导错误。nullptr的类型是什么?能取其地址吗?——std::nullptr_t。nullptr是右值,不能取地址。if (ptr)和if (ptr != nullptr)的区别?——语义等价,if (ptr)依赖隐式bool转换。现代风格推荐显式!= nullptr更清晰表达意图。char* p = 0合法吗?int* p = NULL呢?——都合法。0是空指针常量,NULL通常定义为0或(void*)0。但这是历史写法,应替换为nullptr。C++11 之前如何实现类似
nullptr的类型安全?——通过自定义nullptr_t类 + 隐式转换运算符。标准std::nullptr_t解决了各库各自造轮子的问题。
小结
- 永远使用
nullptr替代NULL和0表示空指针 nullptr的类型是std::nullptr_t,只能隐式转换为指针类型- 在重载决议和模板推导中,
nullptr行为确定且正确 - 现代 C++ 中,空指针检查推荐显式
!= nullptr