Item 8: 比起0和NULL更偏爱nullptr

本文翻译自《effective modern C++》,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢!

博客已经迁移到这里啦

先让我们看一些概念:字面上的0是一个int,不是一个指针。如果C++发现0在上下文中只能被用作指针,它会勉强把0解释为一个null指针,但这只是一个应变的方案。C++的主要规则还是把0视为int,而不是一个指针。

实际上,NULL也是这样的。NULL的情况,在细节方面有一些不确定性,因为C++标准允许它的实现不管是不是int只需要给出一个数值类型(比如,long)。虽然这和0不一样,但是事实上没有关系,因为这里的问题不是NULL的具体类型,而是0和NULL都不是指针类型。

在C++98中,这个问题造成的最主要的影响就是指针和数值类型的重载会导致意外情况。传入一个0或者NULL给这样的重载,则永远不会调用指针版本的重载:

void f(int);        //f的三个重载
void f(bool);
void f(void*);

f(0);               //调用f(int), 而不是f(void*)

f(NULL);            //可能不能通过编译,但是通常会调用f(int),
                    //永远不会调用f(void*)

关于f(NULL)行为的不确定性反映了:关于NULL类型的实现(编译器)拥有自由的权利。比如,如果NULL被定义为0L(也就是long类型的0),那么对函数的调用引起歧义,因为把long转换为int,bool,void*都是同样可行的。关于调用f的一个有趣的事情是源代码表面上的意义(我使用NULL—null指针,调用f)和它实际的意义(我使用某种整形—不是一个指针,调用f)之间的矛盾。这种反直觉的行为导致了C++98程序员指导方针让我们避免重载指针类型和整形类型。这个方针在C++11中仍然有效,因为,就算有这条Item的建议,尽管nullptr是更好的选择,有些开发者还是会继续使用0和NULL。

nullptr的优点不只是它不是一个整形类型。老实说,它也不是一个指针类型,但是你能把它想成是一个指向所有类型的指针。nullptr的实际类型是std::nullptr_t,在一个完美的循环定义(circular definition)中,std::nullptr_t被定义为nullptr的类型。std::nullptr_t类型能隐式转换到所有的原始(raw)指针类型,就是这个原因,让nullptr表现得像它是所有类型的指针。

用nullptr调用f的重载函数,实际会调用void*版本的函数(也就是指针版本的重载),因为nullptr不能被视为任何数值类型:

f(nullptr);         //调用f(void*)版本的重载函数

因此使用nullptr来代替0和NULL避免了意外的重载解析,但是这不是他唯一的优点。它还能提升代码的可读性,尤其是在涉及auto变量时。举个例子,假设你遇到下面的代码:

auto result = findRecord(/* arguments*/);

if(result == 0){
    ...
}

如果你碰巧不知道(或者很难找出)findRecord返回的类型,这里对于result是指针类型还是整形类型就不明确了。总之,0(用来和result进行比较)可以是两个中的任意一个。但是,如果你看到下面的代码:

auto result = findRecord(/* arguments*/);

if(result == nullptr){
    ...
}

这里将没有歧义:result肯定是一个指针类型。

当template涉及后,nullptr将变得更加耀眼。假设你有一些函数,它们只能在适当的互斥量被锁住之后才能调用。每个函数使用不同类型的指针:

int     f1(std::shared_ptr<Widget> spw);
double  f2(std::unique_ptr<Widget> upw);
bool    f3(Widget* pw);

想要传入null指针来调用函数的代码看起来像这样:

std::mutex f1m, f2m, f3m;

using MuxGuard =                //C++11 的typedef,看Item 9
    std::lock_guard<std::mutex>;
...

{
    MuGuard g(f1m);             //锁住f1的互斥量
    auto result = f1(0);        //传入0作为null指针给f1
}                               //解锁互斥量

...

{
    MuGuard g(f2m);             //锁住f2的互斥量
    auto result = f2(NULL);     //传入NULL作为null指针给f2
}                               //解锁互斥量

...

{
    MuGuard g(f3m);             //锁住f3的互斥量
    auto result = f3(nullptr);  //传入nullptr作为null指针给f3
}                               //解锁互斥量 

不能用nullptr调用前面两个函数是不幸的,但是代码能正常工作,这一点很重要。然而,代码中重复的模式(加锁,调用函数,解锁)不仅不幸。这更是令人不安的。这种源代码的重复问题就是template被设计来解决的其中一种,所以让我们模板化这个模式:

template<typename FuncType,
         typename MuxType,
         typename PtrType>
auto lockAndCall(FuncType func,
                 MuxType& mutex,
                 PtrType ptr) -> decltype(func(ptr))
{
    MuxGuard g(mutex);
    return func(ptr);
}

如果这个函数的返回类型(auto … ->decltype(func(ptr)))轰击了你的大脑,请问你的大脑经历并赞成过Item 3(解释了发生了什么)吗?你也能看到C++14版本的代码,返回类型能被简化成一个简单的decltype(auto):

template<typename FuncType,
         typename MuxType,
         typename PtrType>
decltype(auto) lockAndCall(FuncType func,   //C++14
                           MuxType& mutex,
                           PtrType ptr) 
{
    MuxGuard g(mutex);
    return func(ptr);
}

给出lockAndCalltemplate(每个版本)的调用函数,能这么写:

auto result1 = lockAndCall(f1, f1m, 0);         //错误

...

auto result2 = lockAndCall(f2, f2m, NULL);      //错误

...

auto result3 = lockAndCall(f3, f3m, nullptr);   //正确

很好,它们能这么写,但是,正如注释说的,前面两种情况,代码无法通过编译。第一种调用情况的问题是当0被传入lockAndCall时,template类型推导会找出它的类型。0的类型过去是,现在是,未来也永远是int,所以这就是在实例化的lockAndCall中,ptr参数的类型。不幸地,这意味着在lockAndCall中,调用func时,会传入一个int类型,然而它和f1需要的std::shared_patr参数不兼容。在lockAndCall调用中,尝试用传入0来代替一个null指针,但是实际上传入的是普通的int。尝试传入一个int作为std::shared_ptr给f1将产生类型错误。对于用0调用lockAndCall会失败,是因为在template中,一个int被传入一个需要std::shared_ptr的函数。(译注:理解起来很简单,这里没有int 到 指针类型的转换,只有0视为指针这一情况。所以调用会失败。)

对于调用涉及NULL的分析在本质上和0是一样的。当NULL被传入lockAndCall时,一个整形类型被推导为ptr参数的类型,并且当一个int或一个类int类型的参数被传入f2(需要一个std::unique_ptr类型的参数)时,类型错误就发生了。

作为对比,当调用涉及nullptr时没有问题。当nullptr被传入lockAndCall时,ptr的类型被推导为std::nullptr_t。当ptr被传入f3时,这里发生了一个从std::nullptr_t到Widget*的隐式转换,因为std::nullptr_t能隐式转换到任何类型的指针。

事实上,当你想使用一个null指针时,对于0和NULL,template类型推导推导出“错误”的类型
(也就是,它们的“对的”类型本应该是代表着一个null指针的),这是用nullptr代替0或NULL最重要的原因。使用nullptr,template不会面临特殊的挑战。结合nullptr不会遭受意外的重载解析(0和NULL很可能遭受)的事实,情况很明了了。当你想使用一个null指针的时候,使用nullptr,而不是0或NULL。

            你要记住的事
  • 比起0和NULL更偏爱nullptr

  • 避免重载整形类型和指针类型。

<think>嗯,用户问的是C++0NULLnullptr的区别及使用场景。我需要先回忆一下这三者的背景各自的特点。根据之前的引用资料,用户已经提到过一些关键点,比如NULLC++中可能被定义为0,而nullptrC++11引入的新关键字,类型安全的空指针常量。首先,我需要明确这三个概念的历史背景。在C语言中,NULL通常被定义为(void*)0,但在C++中,由于类型系统的严格,NULL被定义为整数0。这可能引发一些问题,比如在函数重载时,传递NULL可能被当作整型0而不是指针,导致调用错误的函数。而nullptr的引入正是为了解决这种类型安全问题。接下来,用户可能想知道它们的具体区别。比如,0是一个整数字面量,而NULLC++中是0的宏定义,nullptr则是std::nullptr_t类型的实例。需要举例说明它们在不同场景下的行为差异,比如在函数重载时,使用NULL可能导致歧义,而nullptr能明确调用指针版本。然后,使用场景方面,用户需要知道什么时候应该使用哪个。比如在C++11及以上版本中,推荐使用nullptr代替NULL,以避免类型相关的问题。而0主要用于整数上下文,而不是指针。还需要注意类型安全的问题。nullptr不能被隐式转换为整数,而NULL0可以,这可能导致意外的错误。比如,当传递NULL到一个期望指针的函数时,如果存在重载的整型版本,可能会调用错误。 另外,用户可能关心它们的实际值。根据引用中的例子,nullptr的值在输出时显示为0,但它的类型是特殊的,不会被当作整数处理。而NULL作为宏,实际值也是0,但类型可能不同。最后,总结三者的区别,给出使用建议,并生成相关问题。确保回答结构清晰,分点明确,用代码示例说明问题,并正确引用提供的资料。</think>### C++0NULLnullptr的区别及使用场景 --- #### **一、基本概念与历史背景** 1. **`0`的本质** - 原始的整数字面量,在指针上下文中可隐式转换为空指针 - 示例: ```cpp int* ptr = 0; // 合法但存在风险 ``` 2. **`NULL`的定位** - C语言中定义为`((void*)0)`,但在C++中通常定义为整数`0` - 示例(C++标准库定义): ```cpp #define NULL 0 // 常见实现方式 ``` 3. **`nullptr`的引入** - C++11新增的关键字,类型为`std::nullptr_t` - 专用于表示空指针,无整型歧义 ```cpp int* ptr = nullptr; // 类型安全的空指针 ``` --- #### **二、核心差异对比** | 特性 | 0 | NULL | nullptr | |-----------------|--------------------|-------------------|-------------------| | **类型** | `int` | 整型(通常为`int`) | `std::nullptr_t` | | **隐式转换** | 可转整型或指针 | 同0 | 仅转指针类型 | | **重载歧义** | 可能引发歧义 | 同0 | 明确调用指针版本 | | **类型安全** | ❌ | ❌ | ✔️ | --- #### **三、典型问题场景** 1. **函数重载歧义** ```cpp void func(int); // 重载1:接受整型 void func(int*); // 重载2:接受指针 func(0); // 调用重载1(整型)[^3] func(NULL); // 可能调用重载1(C++NULL=0)[^2] func(nullptr); // 明确调用重载2(指针)[^3] ``` 2. **模板类型推导** ```cpp template<typename T> void check(T* ptr) { if (ptr == nullptr) { /*...*/ } } check(0); // 编译错误:无法推导T类型 check(nullptr); // 正确推导指针类型 ``` 3. **空指针验证** ```cpp int* p1 = 0; int* p2 = nullptr; if (p1 == p2) { /* 始终为true,但逻辑意图不明确 */ } ``` --- #### **四、使用场景建议** 1. **推荐使用`nullptr`的情况** - C++11及以上项目 - 需要明确指针语义的代码(如智能指针、模板) - 避免函数重载歧义时 2. **允许使用`0`的情况** - 历史遗留代码维护 - 明确需要整数`0`的上下文(如数组下标) 3. **避免使用`NULL`的情况** - 新C++项目中完全用`nullptr`替代 - 需要跨平台兼容性时(不同编译器对NULL的定义可能不同) --- #### **五、内存与值表现** - **指针值比较** ```cpp int* p = nullptr; cout << (p == 0); // 输出1(值相同,但语义不同) cout << (p == NULL); // 输出1(同上) ``` - **内存地址** ```cpp int* p = nullptr; cout << p; // 输出0x0(十六进制)或0(十进制)[^4] ``` --- #### **六、数学表达示例** 在指针运算中,空指针的偏移量计算为: $$ \text{ptr} + n = \text{ptr} + n \times \text{sizeof(T)} $$ 但若`ptr`为`nullptr`,此操作为**未定义行为**[^4] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值