使用 nullptr 有什么好处?

What are the advantages of using nullptr?

提问人:Mark Garcia 提问时间:12/11/2012 最后编辑:VisruthMark Garcia 更新时间:7/18/2018 访问量:60317

问:

这段代码在概念上对三个指针执行相同的操作(安全指针初始化):

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

那么,与为指针赋值或相比,赋值指针的优势是什么?nullptrNULL0

C C++11 null c++-faq nullptr

评论

40赞 chris 12/11/2012
首先,重载函数在使用 时会选择版本而不是版本。intvoid *intvoid *nullptr
2赞 balki 12/12/2012
嗯是不同的。但就上面的代码(分配给局部变量)而言,所有三个指针都是完全相同的。唯一的优点是代码的可读性。f(nullptr)f(NULL)
2赞 sbi 12/12/2012
@Prasoon,我赞成将其作为常见问题解答。谢谢!
1赞 awiebe 9/4/2018
NB NULL 在历史上不保证为 0,但与 oc C99 一样,就像字节不一定是 8 位长,true 和 false 是依赖于架构的值一样。这个问题的重点是,但这就是 0 和nullptrNULL

答:

194赞 Nawaz 12/11/2012 #1

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

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

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

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

因此,此类问题的解决方案是使用: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

由于在模板中,类型被推导为 ,所以你可以这样写:nullptrnullptr_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) 的原因。

评论

1赞 Mark Garcia 12/11/2012
正如Mehrdad所说,这种超载是非常罕见的。还有其他相关优势吗?(不。我没有要求)nullptr
2赞 chris 12/11/2012
@MarkGarcia,这可能会有所帮助:stackoverflow.com/questions/13665349/......
9赞 Steve Jessop 12/11/2012
你的脚注似乎是倒退的。 标准要求具有整数类型,这就是为什么它通常被定义为 或 .另外,我不确定我是否喜欢这种重载,因为它捕获带有 的调用,而不是使用不同类型的空指针(如 .但我可以相信它有一些用途,即使它所做的只是让你将自己定义的单值占位符类型定义为“无”。NULL00Lnullptr_tnullptr(void*)0
1赞 Damon 12/11/2012
另一个优点(尽管公认是次要的)可能是具有定义明确的数值,而空指针常量则没有。null 指针常量将转换为该类型的 null 指针(无论该类型是什么)。要求两个相同类型的空指针以相同的方式进行比较,布尔转换将空指针转换为 。不需要其他任何内容。因此,编译器(愚蠢,但可能)可以使用例如 或空指针的其他数字。另一方面,需要转换为数值零。nullptrfalse0xabcdef1234nullptr
2赞 Nawaz 12/11/2012
@DeadMG:我的答案有什么错误?不会调用预期的函数?动机不止一种。在未来几年里,程序员自己可以发现许多其他有用的东西。因此,您不能说 .f(nullptr)nullptr
4赞 iammilind 12/11/2012 #2

以您展示示例的方式没有直接的好处。
但是考虑一下您有 2 个同名函数的情况;1 次拍摄,另一次拍摄
nullptrintint*

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

如果要通过传递 NULL 来调用,则方法是:foo(int*)

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

nullptr使其更加简单直观

foo(nullptr);

来自 Bjarne 网页的附加链接
无关紧要,但在 C++11 旁注:

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

评论

3赞 chris 12/11/2012
作为参考,是 .decltype(nullptr)std::nullptr_t
2赞 chris 12/11/2012
@MarkGarcia,据我所知,这是一个成熟的类型。
5赞 chris 12/11/2012
@MarkGarcia,这是一个有趣的问题。cppreference 具有:。我想我可以看看标准。啊,找到了: 注意:std::nullptr_t 是一种不同的类型,它既不是指针类型,也不是指向成员类型的指针;相反,此类型的 prvalue 是 null 指针常量,可以转换为 null 指针值或 null 成员指针值。typedef decltype(nullptr) nullptr_t;
2赞 Nawaz 12/11/2012
@DeadMG:动机不止一个。在未来几年里,程序员自己可以发现许多其他有用的东西。因此,您不能说 .nullptr
2赞 Nawaz 12/12/2012
@DeadMG:但你说这个答案“非常不正确”,因为它没有谈到你在回答中谈到的“真正的动机”。不仅如此,这个答案(以及我的答案)得到了你的反对票。
4赞 Angew is no longer proud of SO 12/11/2012 #3

正如其他人已经说过的那样,它的主要优势在于超载。虽然显式重载与指针重载可能很少见,但请考虑标准库函数,例如(在 C++03 中不止一次地咬了我):intstd::fill

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

不编译:。Cannot convert int to MyClass*

2赞 leftaroundabout 12/11/2012 #4

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

24赞 Puppy 12/11/2012 #5

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

考虑:

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

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

还有其他一些极端情况可以使生活更轻松——但这不是核心情况,因为演员可以解决这些问题。考虑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); }

评论

7赞 Nawaz 12/12/2012
有趣。答案的一半与其他两个答案相同,根据您的说法,这是“非常不正确”的答案!!
1赞 jcsahnwaldt Reinstate Monica 10/28/2018
转发问题也可以通过强制转换来解决。 工程。我错过了什么吗?forward((int*)0)
92赞 Alok Save 1/2/2013 #6

C++11引入了,它被称为指针常量,它提高了类型安全性并解决了与现有实现依赖的空指针常量不同的模棱两可的情况。为了能够理解 .我们首先需要了解什么是以及与之相关的问题是什么。nullptrNullNULLnullptrNULL


究竟是什么?NULL

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

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 定义为 或 。但在 C++ 中有一个微妙的区别。0(void *)0

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

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

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

  • 案例一:不会编译,因为需要从 到 自动强制转换。void *std::string
  • 案例二:不会编译,因为需要从到指针强制转换为成员函数。void *

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


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

尽管 C++ 标准委员会提出了一个适用于 C++ 的 NULL 定义,但这个定义有其自身相当多的问题。NULL 适用于几乎所有方案,但不是所有方案。对于某些罕见的情况,它给出了令人惊讶和错误的结果。例如

#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

显然,其意图似乎是调用作为参数的版本,但是当输出显示时,调用了接受版本的函数。这是因为 NULL 是数字文本。char*int

此外,由于 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
}

分析上面的片段:

  • 案例 1:按预期调用。doSomething(char *)
  • 情况 2:调用,但可能需要版本,因为 IS 也是一个空指针。doSomething(int)char*0
  • 案例三:如果定义为 ,则在可能有意调用时调用,这可能会导致运行时出现逻辑错误。如果定义为 ,则调用不明确,并导致编译错误。NULL0doSomething(int)doSomething(char *)NULL0L

因此,根据实现的不同,相同的代码可能会给出不同的结果,这显然是不希望的。当然,C++标准委员会想要纠正这一点,这是nullptr的主要动机。


那么它是什么以及如何避免问题呢?nullptrNULL

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

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

请看以下示例:

#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;
}

在上面的程序中,

  • 案例一:OK - 房产 2
  • 案例二:不正常 - 房产 3
  • 案例三:确定 - 房产 3
  • 案例四:无混淆 - 调用版本、属性 2 和 3char *

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

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

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


标准参考:

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

评论

0赞 Mark Garcia 1/4/2013
自从我知道以来,我已经在实践您的最后建议,尽管我不知道它对我的代码到底有什么区别。感谢您的精彩回答,尤其是您的努力。让我对这个话题有了很大的了解。nullptr
0赞 mxmlnkn 4/14/2016
“在某些标准库头文件中。” ->为什么不从头开始写“cstddef”呢?
0赞 Robert Wang 8/26/2016
为什么我们应该允许 nullptr 转换为 bool 类型?你能详细说明一下吗?
0赞 3Dave 11/11/2016
。用于表示没有值的指针...变量总是有一个值。它可能是噪声,也可能是 ,但是,无值变量是一个固有的矛盾。0xccccc....
0赞 Georg 12/8/2018
“案例 3:正常 - 属性 3”(行)。不,不行,我在使用 g++ 6 编译时出现以下错误:bool flag = nullptr;error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
5赞 Ajay yadav 12/28/2015 #7

nullptr 的基础知识

std::nullptr_t是 null 指针文本 nullptr 的类型。它是 类型的 prvalue/rvalue。存在从 nullptr 到任何指针类型的 null 指针值的隐式转换。std::nullptr_t

文字 0 是整数,而不是指针。如果 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 调用 NULL 的乐趣——空指针”)和它的实际含义(“我用某种整数来调用乐趣——而不是空指针”)之间的矛盾。

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) {
...
}

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

优势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 & lockAndCallF3lockAndCalllockAndCallF1, lockAndCallF2 & 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) 和 lockAndCall(f3, f3m, nullptr) 编译失败,而不是 lockAndCall(f3, f3m, nullptr)

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

问题在于,当 0 传递给 lockAndCall 时,模板类型推导会启动以找出其类型。0 的类型是 int,因此这是此调用 lockAndCall 的实例化中的参数 ptr 的类型。不幸的是,这意味着在对 lockAndCall 中的 func 的调用中,传递了一个 int,这与预期的参数不兼容。在调用中传递的 0 旨在表示 null 指针,但实际传递的是 int。尝试将此 int 作为 a 传递给 f1 是类型错误。对 with 0 的调用失败,因为在模板内部,将 int 传递给需要 .std::shared_ptr<int>f1lockAndCallstd::shared_ptr<int>lockAndCallstd::shared_ptr<int>

涉及的调用的分析基本相同。当传递给 时,会为参数 ptr 推导出一个整型类型,当 (int 或类似 int 的类型) 传递给 时,会发生类型错误,该类型期望得到一个 .NULLNULLlockAndCallptrf2std::unique_ptr<int>

相比之下,涉及的通话没有问题。当传递给 时,类型 for 被推导出为 。当传递给 时,有一个从 to 的隐式转换,因为隐式转换为所有指针类型。nullptrnullptrlockAndCallptrstd::nullptr_tptrf3std::nullptr_tint*std::nullptr_t

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