【问题标题】:What are the advantages of using nullptr?使用 nullptr 有什么好处?
【发布时间】:2012-11-28 18:39:31
【问题描述】:

这段代码概念上对三个指针做了同样的事情(安全指针初始化):

int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;

那么,分配指针nullptr 与分配值NULL0 相比有什么优势?

【问题讨论】:

  • 一方面,使用intvoid * 的重载函数在使用nullptr 时不会选择int 版本而不是void * 版本。
  • 嗯,f(nullptr)f(NULL) 不同。但就上述代码而言(分配给局部变量),所有三个指针都是完全相同的。唯一的优点是代码可读性。
  • 我赞成将此作为常见问题解答,@Prasoon。谢谢!
  • NB NULL 历史上不保证为 0,但与 oc C99 一样,就像一个字节不一定长 8 位,真假是体系结构相关的值。这个问题的重点是nullptr,但这就是0和NULL之间的区别

标签: c++ c++11 null c++-faq nullptr


【解决方案1】:

在该代码中,似乎没有优势。但请考虑以下重载函数:

void f(char const *ptr);
void f(int v);

f(NULL);  //which function will be called?

将调用哪个函数?当然,这里的本意是调用f(char const *),但实际上会调用f(int)!这是个大问题1,不是吗?

所以,此类问题的解决方法是使用nullptr

f(nullptr); //first function is called

当然,这不是nullptr 的唯一优势。这是另一个:

template<typename T, T *ptr>
struct something{};                     //primary template

template<>
struct something<nullptr_t, nullptr>{};  //partial specialization for nullptr

由于在模板中,nullptr 的类型推导出为nullptr_t,所以可以这样写:

template<typename T>
void f(T *ptr);   //function to handle non-nullptr argument

void f(nullptr_t); //an overload to handle nullptr argument!!!

1.在C++中,NULL被定义为#define NULL 0,所以基本上就是int,这就是f(int)被调用的原因。

【讨论】:

  • 正如 Mehrdad 所说,这类过载非常罕见。 nullptr 还有其他相关优势吗? (不。我不要求)
  • @MarkGarcia,这可能会有所帮助:stackoverflow.com/questions/13665349/…
  • 您的脚注似乎倒退了。标准要求NULL 具有整数类型,这就是为什么它通常定义为00L。我也不确定我是否喜欢nullptr_t 重载,因为它捕获only 调用nullptr,而不是使用不同类型的空指针,如(void*)0。但我相信它有一些用途,即使它所做的只是让您将自己的单值占位符类型定义为“无”。
  • 另一个优点(尽管是次要的)可能是nullptr 具有明确定义的数值,而空指针常量则没有。空指针常量被转换为该类型的空指针(无论是什么)。要求相同类型的两个空指针比较相同,布尔转换将空指针转换为false不需要其他任何东西。 因此,编译器(愚蠢,但可能)可以使用例如0xabcdef1234 或空指针的其他数字。另一方面,nullptr 需要转换为数字零。
  • @DeadMG:我的回答有什么不正确的地方? f(nullptr) 不会调用预期的函数?动机不止一种。在接下来的几年里,程序员自己可以发现许多其他有用的东西。所以你不能说nullptr只有一个真正的用法
【解决方案2】:

C++11 引入了nullptr,它被称为Null 指针常量,它提高了类型安全解决了歧义情况 不同于现有的实现依赖空指针常量NULL。能够了解nullptr的优势。我们首先需要了解NULL是什么以及与之相关的问题。


NULL 到底是什么?

Pre C++11 NULL 用于表示没有值的指针或不指向任何有效内容的指针。与流行的概念相反,NULL 不是 C++ 中的关键字。它是标准库头文件中定义的标识符。简而言之,如果不包含一些标准库头文件,您就不能使用NULL。考虑 Sample program

int main()
{ 
    int *ptr = NULL;
    return 0;
}

输出:

prog.cpp: In function 'int main()':
prog.cpp:3:16: error: 'NULL' was not declared in this scope

C++ 标准将 NULL 定义为在某些标准库头文件中定义的实现定义的宏。 NULL的起源来自C,C++继承自C。C标准将NULL定义为0(void *)0。但在 C++ 中存在细微差别。

C++ 不能接受这个规范。与 C 不同,C++ 是一种强类型语言(C 不需要从 void* 显式转换为任何类型,而 C++ 要求显式转换)。这使得 C 标准指定的 NULL 定义在许多 C++ 表达式中毫无用处。例如:

std::string * str = NULL;         //Case 1
void (A::*ptrFunc) () = &A::doSomething;
if (ptrFunc == NULL) {}           //Case 2

如果 NULL 被定义为(void *)0,上述表达式都不起作用。

  • 案例 1: 无法编译,因为需要从 void *std::string 的自动转换。
  • 案例 2: 无法编译,因为需要从 void * 转换为指向成员函数的指针。

因此与 C 不同,C++ 标准要求将 NULL 定义为数字文字 00L


那么当我们已经有了NULL 时,还需要另一个空指针常量吗?

尽管 C++ 标准委员会提出了适用于 C++ 的 NULL 定义,但该定义也有其自身的问题。 NULL 几乎适用于所有场景,但不是全部。对于某些罕见的情况,它给出了令人惊讶和错误的结果。 For example

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    doSomething(NULL);
    return 0;
}

输出:

In Int version

显然,其意图似乎是调用以char* 作为参数的版本,但正如输出显示的那样,调用了以int 版本为参数的函数。这是因为 NULL 是数字文字。

此外,由于 NULL 是 0 还是 0L 是实现定义的,因此在函数重载决策中可能会出现很多混淆。

示例程序:

#include <cstddef>

void doSomething(int);
void doSomething(char *);

int main()
{
  doSomething(static_cast <char *>(0));    // Case 1
  doSomething(0);                          // Case 2
  doSomething(NULL)                        // Case 3
}

分析上面的sn-p:

  • 案例 1: 按预期调用 doSomething(char *)
  • 案例 2: 调用 doSomething(int) 但可能需要 char* 版本,因为 0 也是一个空指针。
  • 案例 3: 如果NULL 定义为0,则在可能打算使用doSomething(char *) 时调用doSomething(int),可能会导致运行时出现逻辑错误。如果NULL定义为0L,则调用不明确,导致编译错误。

因此,根据实现,相同的代码可能会产生不同的结果,这显然是不希望的。自然,C++ 标准委员会想要纠正这个问题,这就是 nullptr 的主要动机。


那么nullptr是什么,如何避免NULL的问题呢?

C++11 引入了一个新的关键字nullptr 作为空指针常量。与 NULL 不同,它的行为不是实现定义的。它不是宏,但它有自己的类型。 nullptr 的类型为std::nullptr_t。 C++11 适当地定义了 nullptr 的属性以避免 NULL 的缺点。总结一下它的属性:

Property 1:它有自己的类型std::nullptr_t,并且
Property 2:它是隐式可转换的,可与任何指针类型或pointer-to相比较- 成员类型,但
属性 3:它不能隐式转换或与整数类型相比较,bool 除外。

考虑以下示例:

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    char *pc = nullptr;      // Case 1
    int i = nullptr;         // Case 2
    bool flag = nullptr;     // Case 3

    doSomething(nullptr);    // Case 4
    return 0;
}

在上述程序中,

  • 案例 1: OK - 属性 2
  • 案例 2: 不好 - 属性 3
  • 案例 3: OK - 属性 3
  • 案例 4: 没有混淆 - 调用 char * 版本,属性 2 和 3

因此引入 nullptr 避免了旧 NULL 的所有问题。

你应该如何以及在哪里使用nullptr

C++11 的经验法则是,只要您过去使用 NULL,就只需开始使用 nullptr


标准参考:

C++11 标准:C.3.2.4 宏 NULL
C++11 标准:18.2 类型
C++ 11 标准:4.10 指针转换
C99 标准:6.3.2.3 指针

【讨论】:

  • 自从我知道nullptr 以来,我已经在练习你的最后一个建议,尽管我不知道它对我的代码有什么真正的影响。感谢您的出色回答,尤其是您的努力。让我对这个话题有了很多了解。
  • "在某些标准库头文件中。" -> 为什么不从头开始写“cstddef”?
  • 为什么我们应该允许 nullptr 可以转换为 bool 类型?你能详细说明一下吗?
  • ...用来表示没有值的指针...变量总是有值。可能是噪音,也可能是0xccccc....,但是,无值变量是一个固有的矛盾。
  • “案例 3:OK - 属性 3”(行 bool flag = nullptr;)。不,不行,我在使用 g++ 6 编译时收到以下错误:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
【解决方案3】:

这里真正的动机是完美的转发

考虑:

void f(int* p);
template<typename T> void forward(T&& t) {
    f(std::forward<T>(t));
}
int main() {
    forward(0); // FAIL
}

简单地说,0 是一个特殊的,但是值不能通过系统传播——只有类型可以。转发功能是必不可少的,0处理不了。因此,绝对有必要引入nullptr,其中type 是特殊的,并且该类型确实可以传播。事实上,MSVC 团队在实现右值引用后不得不提前引入nullptr,然后自己发现了这个陷阱。

nullptr 可以让生活更轻松还有一些其他极端案例,但这不是核心案例,因为演员可以解决这些问题。考虑

void f(int);
void f(int*);
int main() { f(0); f(nullptr); }

调用两个单独的重载。另外,考虑

void f(int*);
void f(long*);
int main() { f(0); }

这是模棱两可的。但是,使用 nullptr,您可以提供

void f(std::nullptr_t)
int main() { f(nullptr); }

【讨论】:

  • 有趣。答案的一半与其他两个答案相同,根据您的说法是“非常不正确”答案!!!
  • 转发问题也可以通过强制转换来解决。 forward((int*)0) 有效。我错过了什么吗?
【解决方案4】:

nullptr 的基础知识

std::nullptr_t 是空指针字面量 nullptr 的类型。它是std::nullptr_t 类型的纯右值/右值。存在从 nullptr 到任何指针类型的空指针值的隐式转换。

文字 0 是一个 int,而不是一个指针。如果 C++ 发现自己在只能使用指针的上下文中查看 0,它会不情愿地将 0 解释为空指针,但这是一个后备位置。 C++ 的主要策略是 0 是一个 int,而不是一个指针。

优势 1 - 重载指针和整数类型时消除歧义

在 C++98 中,这主要意味着指针和整数类型的重载可能会导致意外。将 0 或 NULL 传递给此类重载永远不会称为指针重载:

   void fun(int); // two overloads of fun
    void fun(void*);
    fun(0); // calls f(int), not fun(void*)
    fun(NULL); // might not compile, but typically calls fun(int). Never calls fun(void*)

这个调用的有趣之处在于源代码的表面含义(“我用 NULL 调用 fun——空指针”)和它的实际含义(“我用某种整数调用 fun——不是空指针”)。

nullptr 的优点是它没有整数类型。 用 nullptr 调用重载函数 fun 调用 void* 重载(即指针重载),因为 nullptr 不能被视为任何整数:

fun(nullptr); // calls fun(void*) overload 

使用 nullptr 代替 0 或 NULL 从而避免重载决议意外。

使用 auto 作为返回类型时,nullptr 相对于 NULL(0) 的另一个优势

例如,假设您在代码库中遇到这种情况:

auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}

如果你碰巧不知道(或不能轻易找出) findRecord 返回的内容,可能不清楚 result 是指针类型还是整数类型。毕竟,0(测试的结果)可以是任何一种方式。另一方面,如果您看到以下内容,

auto result = findRecord( /* arguments */ );
if (result == nullptr) {
...
}

没有歧义:结果必须是指针类型。

优势 3

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

void lockAndCallF1()
{
        MuxtexGuard g(f1m); // lock mutex for f1
        auto result = f1(static_cast<int>(0)); // pass 0 as null ptr to f1
        cout<< result<<endl;
}

void lockAndCallF2()
{
        MuxtexGuard g(f2m); // lock mutex for f2
        auto result = f2(static_cast<int>(NULL)); // pass NULL as null ptr to f2
        cout<< result<<endl;
}
void lockAndCallF3()
{
        MuxtexGuard g(f3m); // lock mutex for f2
        auto result = f3(nullptr);// pass nullptr as null ptr to f3 
        cout<< result<<endl;
} // unlock mutex
int main()
{
        lockAndCallF1();
        lockAndCallF2();
        lockAndCallF3();
        return 0;
}

以上程序编译执行成功,但是lockAndCallF1, lockAndCallF2 & lockAndCallF3有多余的代码。如果我们可以为所有这些lockAndCallF1, lockAndCallF2 &amp; lockAndCallF3 编写模板,那么编写这样的代码是很可惜的。所以可以用模板泛化。我已经为冗余代码编写了模板函数lockAndCall 而不是多个定义lockAndCallF1, lockAndCallF2 &amp; lockAndCallF3

代码重构如下:

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
//decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr)
{
        MuxtexGuard g(mutex);
        return func(ptr);
}
int main()
{
        auto result1 = lockAndCall(f1, f1m, 0); //compilation failed 
        //do something
        auto result2 = lockAndCall(f2, f2m, NULL); //compilation failed
        //do something
        auto result3 = lockAndCall(f3, f3m, nullptr);
        //do something
        return 0;
}

详细分析为什么lockAndCall(f1, f1m, 0) &amp; lockAndCall(f3, f3m, nullptr)编译失败而不是lockAndCall(f3, f3m, nullptr)

为什么 lockAndCall(f1, f1m, 0) &amp; lockAndCall(f3, f3m, nullptr) 编译失败?

问题是当 0 被传递给 lockAndCall 时,模板类型推导开始计算它的类型。 0 的类型是 int,所以这就是这个 lockAndCall 调用的实例化中的参数 ptr 的类型。不幸的是,这意味着在 lockAndCall 内部对 func 的调用中,传递了一个 int,这与 f1 所期望的 std::shared_ptr&lt;int&gt; 参数不兼容。在对lockAndCall 的调用中传递的 0 旨在表示一个空指针,但实际上传递的是 int。试图将此 int 作为 std::shared_ptr&lt;int&gt; 传递给 f1 是类型错误。使用 0 调用 lockAndCall 失败,因为在模板内部,一个 int 被传递给需要 std::shared_ptr&lt;int&gt; 的函数。

对涉及NULL的调用的分析基本相同。当NULL 传递给lockAndCall 时,会为参数ptr 推导出一个整数类型,当ptr(一个int 或类似int 的类型)传递给f2 时会出现类型错误,它期望得到std::unique_ptr&lt;int&gt;

相比之下,涉及nullptr的电话就没有问题了。当nullptr 被传递给lockAndCall 时,ptr 的类型被推断为std::nullptr_t。当ptr 被传递给f3 时,有一个从std::nullptr_tint* 的隐式转换,因为std::nullptr_t 隐式转换为所有指针类型。

建议,每当要引用空指针时,使用 nullptr,而不是 0 或 NULL

【讨论】:

    【解决方案5】:

    nullptr 在您展示示例的方式中没有直接优势。
    但是考虑一个情况,你有两个同名的函数; 1 接受 int 和另一个 int*

    void foo(int);
    void foo(int*);
    

    如果你想通过传递一个NULL来调用foo(int*),那么方法是:

    foo((int*)0); // note: foo(NULL) means foo(0)
    

    nullptr 使其更加简单直观

    foo(nullptr);
    

    Additional link 来自 Bjarne 的网页。
    无关紧要,但在 C++11 旁注:

    auto p = 0; // makes auto as int
    auto p = nullptr; // makes auto as decltype(nullptr)
    

    【讨论】:

    • 供参考,decltype(nullptr)std::nullptr_t
    • @MarkGarcia,据我所知,这是一个成熟的类型。
    • @MarkGarcia,这是一个有趣的问题。 cppreference 有:typedef decltype(nullptr) nullptr_t;。我想我可以看看标准。啊,找到了:注意:std::nullptr_t 是一个独特的类型,既不是指针类型也不是指向成员类型的指针;相反,这种类型的纯右值是一个空指针常量,可以转换为空指针值或空成员指针值。
    • @DeadMG:动机不止一个。在接下来的几年里,程序员自己可以发现许多其他有用的东西。所以你不能说nullptr只有一种真正的用法
    • @DeadMG:但你说这个答案是“非常不正确”,只是因为它没有谈到你所说的“真正的动机”关于你的回答。不仅这个答案(以及我的答案)收到了你的反对票。
    【解决方案6】:

    正如其他人已经说过的,它的主要优势在于重载。虽然显式 int 与指针重载比较少见,但请考虑像 std::fill 之类的标准库函数(在 C++03 中不止一次困扰我):

    MyClass *arr[4];
    std::fill_n(arr, 4, NULL);
    

    无法编译:Cannot convert int to MyClass*

    【讨论】:

      【解决方案7】:

      IMO 比那些重载问题更重要:在深度嵌套的模板构造中,很难不忘记类型,并且给出明确的签名是一项相当大的努力。因此,对于您使用的所有内容,越准确地关注预期目的越好,这将减少对显式签名的需求,并允许编译器在出现问题时生成更深入的错误消息。

      【讨论】:

        猜你喜欢
        • 2010-09-21
        • 2011-04-28
        • 1970-01-01
        • 2011-09-16
        • 2011-04-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多