C++ 中 nullptr 与 NULL 的区别:为什么现代 C++ 必须使用 nullptr

📅 2026/7/25 12:22:44 👁️ 阅读次数 📝 编程学习
C++ 中 nullptr 与 NULL 的区别:为什么现代 C++ 必须使用 nullptr

C++ 中 nullptr 与 NULL 的区别:为什么现代 C++ 必须使用 nullptr


一、引言:一个看似微小的改变


在 C++11 之前,程序员使用NULL宏来表示空指针。C++11 引入了nullptr关键字作为空指针的专用字面量。这个变化看似只是语法糖,实际上解决了NULL在 C++ 中由来已久的类型安全问题函数重载歧义问题


许多开发者仍然习惯性地使用NULL,但在现代 C++ 代码中,nullptr是唯一正确的选择。理解为什么需要这个新关键字,对于写出类型安全、行为可预测的 C++ 代码至关重要。


二、核心区别速览


| 维度 | NULL | nullptr |

|------|------|---------|

| 本质 |宏定义(通常定义为0(void*)0) |关键字std::nullptr_t类型的字面量 |

| 类型 |整数类型(C++ 中) |std::nullptr_t(专门的空指针类型) |

| 与整数比较 | 等于 0(隐式转换) | 等于 0(可比较,但不是整数) |

| 函数重载 |歧义:可能匹配int版本 |精确:始终匹配指针版本 |

| 类型安全 |:可隐式转换为 int |:只能转换为指针或 bool |

| 模板推导 | 推导为int| 推导为std::nullptr_t|

| 可读性 | 不明确(是整数还是指针?) | 明确(一眼看出是指针相关) |


三、NULL 的本质与问题


3.1 NULL 是什么


// C++ 标准库中 NULL 的典型定义: #define NULL 0 // C++ 中的常见定义 // 或 #define NULL ((void*)0) // C 中的常见定义(C++ 不允许 void* 隐式转换到其他指针) #include <iostream> int main() { // NULL 实际上是整数 0 std::cout << "NULL value: " << NULL << std::endl; // 0 std::cout << "sizeof(NULL): " << sizeof(NULL) << std::endl; // 4 (sizeof(int)) // 这是一个普通的整数,可以参与算术运算 int x = NULL + 5; // 完全合法!x = 5 std::cout << "NULL + 5: " << x << std::endl; }


3.2 问题一:类型不安全


#include <iostream> void processPointer(int* ptr) { std::cout << "Processing pointer" << std::endl; } void processInteger(int value) { std::cout << "Processing integer: " << value << std::endl; } int main() { // 使用 NULL:意图是传空指针 processPointer(NULL); // 输出: Processing pointer(多数编译器如此) processInteger(NULL); // 输出: Processing integer: 0 // 问题:NULL 是整数 0,两者都可以匹配 // 编译器可能选择任意一个(取决于实现) // 调用者意图不明确 // 使用 nullptr:意图明确 processPointer(nullptr); // 输出: Processing pointer(明确) // processInteger(nullptr); // 编译错误!nullptr 不能隐式转为 int }


3.3 问题二:模板推导歧义


#include <iostream> #include <memory> template<typename T> void deduceType(T value) { std::cout << "Deduced type: " << typeid(T).name() << std::endl; } int main() { deduceType(NULL); // 输出: int(或 long)—— 推导为整数! deduceType(nullptr); // 输出: std::nullptr_t —— 正确推导为空指针类型 // 这导致模板代码中的严重问题: // auto result = someFunction(NULL); // result 推导为 int! // auto result = someFunction(nullptr); // result 推导为 std::nullptr_t }


3.4 问题三:指针上下文以外的滥用


#include <iostream> class MyClass { public: void process(int x) { std::cout << "process(int): " << x << std::endl; } void process(int* ptr) { if (ptr) { std::cout << "process(int*): " << *ptr << std::endl; } else { std::cout << "process(int*): nullptr" << std::endl; } } }; int main() { MyClass obj; int value = 42; obj.process(NULL); // 歧义!编译器可能选择 process(int) 或 process(int*) // 不同编译器行为可能不同 // 有些编译器报错,有些选择 int 版本 obj.process(nullptr); // 明确:调用 process(int*) }


四、nullptr 的优势


4.1 精确的类型:std::nullptr_t


#include <iostream> #include <type_traits> int main() { // nullptr 有自己的类型 std::cout << std::is_same_v<decltype(nullptr), std::nullptr_t> << std::endl; // true // nullptr_t 可以隐式转换到任意指针类型 int* pi = nullptr; // OK double* pd = nullptr; // OK void (*fp)() = nullptr; // OK // nullptr_t 可以隐式转换到 bool bool b = nullptr; // OK: b = false // nullptr_t 不能隐式转换到整数 // int x = nullptr; // 编译错误! int x = 0; // 这才是正确的 // 可以进行比较 if (pi == nullptr) { } // OK if (pi == 0) { } // OK(但应该用 nullptr) if (nullptr == 0) { } // OK(但语义模糊) // sizeof std::cout << sizeof(nullptr) << std::endl; // 通常 8(与指针大小相同) }


4.2 函数重载的明确性


#include <iostream> // 重载函数:处理不同的参数类型 void handle(int value) { std::cout << "handle(int): " << value << std::endl; } void handle(double value) { std::cout << "handle(double): " << value << std::endl; } void handle(const char* str) { std::cout << "handle(const char*): " << (str ? str : "null") << std::endl; } void handle(void* ptr) { std::cout << "handle(void*): " << ptr << std::endl; } int main() { handle(42); // handle(int): 42 handle(3.14); // handle(double): 3.14 handle("Hello"); // handle(const char*): Hello // 使用 NULL:歧义! // handle(NULL); // 编译错误或警告:int vs void* 歧义 // 使用 nullptr:明确 handle(nullptr); // handle(void*): 0x0 (精确匹配 void* 版本) }


4.3 模板编程中的准确性


#include <iostream> #include <memory> #include <type_traits> // 模板函数:根据指针是否为空执行不同逻辑 template<typename T> auto safeDereference(T ptr) { // 使用 nullptr 可以正确推断 T 是指针类型 if (ptr == nullptr) { return decltype(*ptr){}; // 返回默认值 } return *ptr; } // 类型萃取:检测类型是否可作为空指针 template<typename T> struct is_nullable : std::false_type { }; template<typename T> struct is_nullable<T*> : std::true_type { }; template<typename T> struct is_nullable<std::shared_ptr<T>> : std::true_type { }; template<typename T> struct is_nullable<std::unique_ptr<T>> : std::true_type { }; int main() { int value = 42; int* ptr1 = &value; int* ptr2 = nullptr; std::cout << safeDereference(ptr1) << std::endl; // 42 std::cout << safeDereference(ptr2) << std::endl; // 0(默认 int) // nullptr_t 在模板中保持精确类型 auto np = nullptr; // decltype(np) 是 std::nullptr_t,而不是 int std::cout << std::is_same_v<decltype(np), std::nullptr_t> << std::endl; // true }


4.4 条件表达式中的安全性


#include <iostream> #include <string> std::string getValueOrDefault(const char* str, const std::string& defaultValue) { // 使用 nullptr 清晰表达空指针语义 if (str == nullptr) { return defaultValue; } return std::string(str); } // 避免布尔上下文中的歧义 class SmartPointer { int* ptr; public: explicit SmartPointer(int* p = nullptr) : ptr(p) { } // 使用 nullptr 检查比直接 if (ptr) 更明确 bool isNull() const { return ptr == nullptr; } bool isNotNull() const { return ptr != nullptr; } int* get() { return ptr; } }; int main() { std::cout << getValueOrDefault(nullptr, "default") << std::endl; // default std::cout << getValueOrDefault("Hello", "default") << std::endl; // Hello SmartPointer sp; std::cout << std::boolalpha; std::cout << "isNull: " << sp.isNull() << std::endl; // true std::cout << "isNotNull: " << sp.isNotNull() << std::endl; // false }


五、决策流程图


C++11及以上必须兼容C++03指针初始化指针比较函数参数delete后置空智能指针

需要表示空指针

代码目标标准

使用 nullptr

被迫使用 NULL 或 0

类型安全
不匹配整数参数

重载明确
精确匹配指针版本

模板推导
推导为 std::nullptr_t

代码可读性
意图一目了然

注意: 不要混用 NULL 和 nullptr
保持代码风格一致

检查场景

int* p = nullptr;

if (p == nullptr)

func(nullptr);

delete p; p = nullptr;

shared_ptr sp = nullptr;


六、迁移指南:从 NULL 到 nullptr


6.1 代码迁移模式


// ========== 旧的写法(应该避免) ========== int* oldStyle() { int* p = NULL; // 初始化 if (p != NULL) { } // 比较 if (p) { } // 布尔检查(仍然可用但不推荐) if (!p) { } // 布尔检查(仍然可用但不推荐) // 函数调用 process(p, NULL); // 容易产生歧义 return NULL; // 返回空指针 } // ========== 新的写法(推荐) ========== int* newStyle() { int* p = nullptr; // 初始化 if (p != nullptr) { } // 比较(明确) // 函数调用 process(p, nullptr); // 清晰明确 return nullptr; // 返回空指针 }


6.2 特殊情况处理


// 与 C API 交互时 #include <cstring> #include <cstdio> void cApiExample() { // C 标准库函数通常接受 NULL // 但你仍然应该传递 nullptr(可以隐式转换) FILE* file = fopen("test.txt", "r"); if (file == nullptr) { // 不是 NULL std::cout << "File not found" << std::endl; } // 某些 C 库的宏可能使用 NULL // 不要被这些影响,在 C++ 代码中坚持使用 nullptr // 变参函数中的 nullptr(如 printf) // printf("%s", nullptr); // 未定义行为! printf("%s", "(null)"); // 正确做法 }


七、总结


nullptr替代NULL是 C++11 带来的一个看似微小但影响深远的变化:


  1. 类型安全的根本差异NULL在 C++ 中是整数常量0(或0L),本质上是一个整数;nullptrstd::nullptr_t类型,是一个真正的空指针字面量。这一差异是其他所有问题的根源。


  1. 解决的核心问题
  • 函数重载歧义NULL可能被解析为int而匹配错误的函数版本,nullptr精确匹配指针参数
  • 模板推导错误auto x = NULL推导为intauto x = nullptr推导为std::nullptr_t
  • 代码意图表达nullptr一目了然地表示“这里处理的是指针”,NULL可能是整数也可能是指针


  1. 现代 C++ 的坚定选择
  • 所有新代码应该完全使用nullptr,不使用NULL0表示空指针
  • 与旧代码交互时,nullptr可以无缝替代NULL
  • 使用nullptr而不是if (ptr)if (!ptr)进行指针判空,意图更明确
  • nullptr视为 C++ 类型系统走向更精确、更安全的一个标志性进步


一条简单的规则:在现代 C++ 中,任何时候你想表达“这个指针不指向任何东西”,就用nullptr。它是专门为此目的设计的,而NULL是 C 语言遗留的历史包袱。这个看似微小的选择,反映的是对类型安全和代码可维护性的认真态度。