ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

nullptr 取代 NULL 和 0:空指针的三种写法和唯一正确解

nullptr 取代 NULL 和 0:空指针的三种写法和唯一正确解 0是整数NULL在多数实现里也只是0的宏而nullptr是 C11 引入的、专门表示空指针的独立类型std::nullptr_t。这一个差别在函数重载和模板推导里能悄无声息地选错版本。这篇用static_assertstd::is_same把三种写法的真实类型钉出来并说清那个经典陷阱f(NULL)。官方文档nullptr、std::nullptr_t、C Core GuidelinesES.47 用 nullptr1. 引子f(0) 调到了 f(int)不是 f(char*)先来看为什么「用 0 当空指针」是隐患voidf(int){/* 处理整数 */}voidf(char*){/* 处理指针 */}// f(0); // 你以为是「传空指针」其实选中了 f(int)0是int自然去匹配f(int)。想传「空指针」却进了对整数的分支——这正是nullptr要解决的。2. NULL 为何是陷阱重载决议选中 intNULL的底层类型是实现定义的implementation-defined在把NULL定义成0的编译器里它会静默选中f(int)在把NULL定义成内建空指针常量的 gcc/clang 里它甚至会让重载直接歧义ambiguous。两种结果都说明同一件事NULL不可靠。先看0和nullptr的路由// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostream#includecstddefvoidf(int){std::coutf(int)\n;}voidf(char*){std::coutf(char*)\n;}intmain(){f(0);// 0 是 int选中 f(int)f(nullptr);// nullptr 是 std::nullptr_t正确选中 f(char*)}f(int) f(char*)那f(NULL)呢在本文用的 gcc-13.2.0 上它编译失败——调用歧义prog.cc: 错误对 f(NULL) 的调用有歧义ambiguous candidate: void f(int) candidate: void f(char*)而在把NULL定义成0的老编译器里f(NULL)会静默选中f(int)比歧义更隐蔽。0和NULL都让「本想传空指针」的调用跑偏只有nullptr这种独立的指针类型能正确路由到f(char*)。3. 模板推导下的差异这才是 nullptr 的真正优势重载还能靠「写对类型」规避模板推导里NULL的坑几乎无法绕开。把实参交给模板T的推导结果说明一切#includeiostream#includetype_traits#includetypeinfo#includecstddeftemplatetypenameTvoiddeduce(T){std::cout T 是 int std::is_sameT,int::value\n;std::cout T 是 nullptr_t std::is_sameT,std::nullptr_t::value\n;std::cout T 实际类型名 typeid(T).name()\n;}intmain(){std::cout传入 0:\n;deduce(0);std::cout传入 NULL:\n;deduce(NULL);std::cout传入 nullptr:\n;deduce(nullptr);}传入 0: T 是 int 1 T 是 nullptr_t 0 T 实际类型名 i 传入 NULL: T 是 int 0 T 是 nullptr_t 0 T 实际类型名 l 传入 nullptr: T 是 int 0 T 是 nullptr_t 1 T 实际类型名 Dntypeid(T).name()打印的是编译器内部的「mangled 名」i即intDn即decltype(nullptr)也就是std::nullptr_tl即long。注意NULL既不是i也不是Dn——在本编译器上它的类型是longgcc 把内建__null暴露成long。铁证0推导成int、nullptr推导成std::nullptr_t而NULL推导成long——一个整数类型。既然NULL是整数f(NULL)在重载里既可能匹配int、又因__null可转指针而歧义。如果模板里想根据「是不是空指针」做偏特化或分支0/NULL会让你整个分支失效。4. 用 static_assert 把类型钉死不靠打印也能在编译期强制证明nullptr的类型#includeiostream#includetype_traits#includecstddefintmain(){static_assert(std::is_samedecltype(nullptr),std::nullptr_t::value,);std::coutnullptr 的类型是 nullptr_t std::is_samedecltype(nullptr),std::nullptr_t::value\n;}nullptr 的类型是 nullptr_t 1注NULL的底层类型依赖实现多为int或编译器的内建空指针常量所以不能用std::is_samedecltype(NULL), std::nullptr_t来假设——这恰恰说明它不可靠。5. 为什么标准不删掉 NULL向后兼容既然NULL有坑为什么 C 不干脆删掉因为删除会破坏海量已有代码。无数老项目、老库、老头文件里写着NULL删掉它们就编译不过。C 的设计哲学是「不轻易破坏已有代码」所以NULL被保留但新代码一律用nullptr。标准甚至保留了NULL的「实现定义空指针常量」语义只是明明白白地建议你别再用它见 Core Guidelines ES.47。三种空指针写法对比 ┌─────────┬───────────────────┬───────────────────────────────┐ │ 写法 │ 真实类型 │ 后果 │ ├─────────┼───────────────────┼───────────────────────────────┤ │ 0 │ int │ 重载/模板里误选 int 版本 │ │ NULL │ 多为 int实现相关│ 同上且跨编译器行为不一致 │ │ nullptr │ std::nullptr_t │ 专为空指针设计正确选中指针 │ └─────────┴───────────────────┴───────────────────────────────┘6. 完整示例函数参数用 nullptr 才类型安全把前面所有点落到一个真实接口reset接收指针用nullptr判空才清晰、类型安全。#includeiostream#includecstddefvoidreset(int*p){if(pnullptr){std::coutp 是空指针\n;}else{std::coutp 指向 *p\n;}}intmain(){intx5;reset(x);// 非空reset(nullptr);// 正确表达「无」// reset(0); // 反例不要这么写0 当空指针类型意图模糊// reset(NULL); // 反例不要这么写NULL 实现相关易踩重载坑}p 指向 5 p 是空指针7. 空指针判空的实践规范把「空指针」写对只是一半判空怎么写是另一半。if (p)、if (p ! nullptr)、if (!p)三者都能用但表达力和适用范围并不相同写法适用对象实际语义建议if (p ! nullptr)裸指针显式比较地址意图最直白首选团队统一写法if (p)裸指针靠指针到bool的转换等价于上面可用短表达式里更简洁if (!p)裸指针「非空取反」容易读成「p 为假」少用if (p 0)—又把0请回来当空指针常量禁止NULL的坑原样重演if (sp)智能指针调用explicit operator bool推荐if (sp ! nullptr)智能指针同样合法语义最直白推荐智能指针之所以能直接if (sp)是因为unique_ptr/shared_ptr都提供了explicit operator bool。这个explicit是关键它只在语境转换contextual conversion——也就是if、while、、||、!的操作数位置——才会被调用不能隐式转成int。于是if (sp 0)这种「指针和整数比较」的老事故在智能指针上根本写不出来编译器直接拒绝。这也顺带解释了为什么explicit operator bool要设计成 explicit不是不让用而是只在「当条件」这一个语境里放行。还有一点值得记住p ! nullptr和!p并不总是同义。裸指针上两者等价都判地址是否为零但如果类型自定义了operator bool或operator!——比如某些句柄类、延迟初始化类——!p会走类型自己的逻辑「非空」之外还可能带上「已初始化」「已打开」等额外含义。碰到这类类型判空一律用它自己提供的语义别退回去写 nullptr。8. nullptr 与模板、与 std::optional 的衔接模板是NULL的坑最难绕开的地方第 3 节已经实证NULL会被推导成long这样的整数类型泛型代码里一旦按「是不是空指针」做分支或偏特化用NULL传参的那条路就会整条失效。写泛型接口时把「可能为空」的指针形参默认值一律写成nullptrtemplatetypenameTvoiduse(T*ptrnullptr);// 正确默认值是真正的空指针类型// template typename T// void use(T* ptr NULL); // 反例不要这么写NULL 实现相关模板里更不可控想在编译期判断「某个类型是不是空指针类型」C14 起有专门的std::is_null_pointer比手写std::is_sameT, std::nullptr_t更达意设施版本作用典型用途std::nullptr_tC11空指针的独立类型形参限定「只接受空指针」decltype(nullptr)C11同一个类型少打几个字符模板里的类型标注std::is_null_pointerTC14编译期判断T是否为空指针类型类型分派、if constexpr分支那「可能没有值」这件事到底该用空指针还是std::optionalT原则很简单「不拥有对象的、可选的引用」用指针 nullptr「可能缺失的值」用std::optional。optional把「有没有值」编进了类型里取值必须先判has_value()不会像裸指针那样一不小心就解引用而指针天然表达「指向某个已存在的对象也可能什么都不指」还能直接参与多态。两者职责不同不是替代关系。把重载、模板推导、is_null_pointer三件事放进一个程序里验证一遍// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostream#includetype_traits#includecstddef// 重载三个版本分别接收「整数」「字符指针」「空指针」voidroute(int){std::coutroute(int)\n;}voidroute(char*){std::coutroute(char*)\n;}voidroute(std::nullptr_t){std::coutroute(nullptr_t)\n;}// 模板看 T 到底被推导成了什么templatetypenameTconstchar*kind(){if(std::is_null_pointerT::value)return空指针类型;if(std::is_sameT,int::value)return整数类型;return其他类型;}templatetypenameTvoiddeduce(T){std::cout T 的类别: kindT()\n;}intmain(){route(0);// 0 是 int精确匹配 route(int)route(nullptr);// nullptr 是 nullptr_t有专属重载时精确匹配胜出std::cout模板推导 0:\n;deduce(0);std::cout模板推导 nullptr:\n;deduce(nullptr);}route(int) route(nullptr_t) 模板推导 0: T 的类别: 整数类型 模板推导 nullptr: T 的类别: 空指针类型注意route(nullptr)的结果这里同时存在route(int)、route(char*)和route(std::nullptr_t)三个重载nullptr精确匹配std::nullptr_t恒等转换比「转成char*」的指针转换更优所以胜出的是第三个。而0依旧一头扎进route(int)——哪怕旁边就住着一个char*版本。同一件事写0和写nullptr走的是完全不同的两条路。9. 延伸阅读nullptr — cppreferencenullptr与std::nullptr_t的语义std::nullptr_t — cppreference这个独立类型的可 convertible 行为C Core GuidelinesES.47 用 nullptr 而非 0 或 NULL官方为什么推荐Compiler Explorer对比f(NULL)与f(nullptr)在重载下的汇编差异std::is_null_pointer — cppreferenceC14 起的编译期空指针类型判断std::unique_ptr — cppreference看它的operator bool为什么是explicit10. 一句话总结0是int、NULL在多数实现里也只是0所以重载和模板推导里都会误选int版本nullptr是独立的std::nullptr_t类型能正确选中指针重载、推导成std::nullptr_t。标准保留NULL只为向后兼容——新代码一律写nullptr别再用0或NULL当空指针。
返回列表