使用nullptr有什么优势?


163

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

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

那么,分配指针nullptr比给它们分配值NULLor有0什么好处?


39
一方面,重载函数采取intvoid *不会选择int版本在void *使用时的版本nullptr
克里斯,2012年

2
好吧f(nullptr),与f(NULL)。但是就上述代码而言(分配给局部变量),所有三个指针完全相同。唯一的优点是代码可读性。
balki 2012年

2
我赞成将其设为FAQ,@ Prasoon。谢谢!
2012年

1
NB NULL从历史上不能保证为0,但就像oc C99一样,以相同的方式,字节不一定是8位长,true和false是与体系结构有关的值。这个问题的重点是,nullptr但0和NULL
awiebe

Answers:


180

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

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)要调用它的原因。


1
正如Mehrdad所说的那样,这类重载非常罕见。还有其他相关优势nullptr吗?(不,我没有要求)
Mark Garcia

2
@MarkGarcia,这可能会有所帮助:stackoverflow.com/questions/13665349/…–
chris

9
您的脚注似乎向后。NULL标准要求它具有整数类型,这就是为什么通常将其定义为0或的原因0L。此外,我不喜欢这样肯定我nullptr_t超载,因为它捕获与调用nullptr,而不是与不同类型的空指针一样(void*)0。但是我可以相信它有一些用处,即使它所做的只是省去了将自己定义的单值占位符类型定义为“无”的麻烦。
史蒂夫·杰索普

1
另一个优点(虽然公认是次要的)可能是nullptr具有定义明确的数值,而空指针常量则没有。空指针常量将转换为该类型的空指针(无论是哪种类型)。要求两个相同类型的空指针进行相同比较,并且布尔转换将一个空指针转换为false没有其他要求。因此,编译器可以(例如,愚蠢但可行)0xabcdef1234将null指针使用例如或其他一些数字。另一方面,nullptr需要转换为数字零。
戴蒙

1
@DeadMG:我的答案有什么错误?那f(nullptr)不会调用预期的功能?有多种动机。未来几年,程序员自己会发现许多其他有用的东西。所以你不能说有只有一个真正使用nullptr
纳瓦兹2012年

87

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


到底是NULL什么?

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

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 ++ Standard要求将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

显然,其意图似乎是调用以版本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
}

分析以上代码段:

  • 情况1:doSomething(char *)按预期方式拨打电话。
  • 情况2:调用,doSomething(int)但可能char*希望使用版本,因为0IS还是一个空指针。
  • 情况3:如果NULL将定义为0,则doSomething(int)可能doSomething(char *)在预期的时候调用,这可能会在运行时导致逻辑错误。如果NULL定义为0L,则该调用不明确,并导致编译错误。

因此,取决于实现方式,相同的代码可以给出各种结果,这显然是不希望的。自然,C ++标准委员会希望纠正这一点,这是nullptr的主要动机。


那么什么是nullptr它又如何避免出现问题NULL呢?

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

属性1:它具有自己的类型std::nullptr_t
属性2:它可以隐式转换,并且可以与任何指针类型或指针到成员类型进行比较;但是
属性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:确定-属性2
  • 情况2:不好-物业3
  • 情况3:确定-属性3
  • 情况4:不混淆-通话char *版本,媒体资源2和3

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

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

nullptr只要您过去曾经使用NULL,就可以开始使用C ++ 11的经验法则。


标准参考:

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


自从知道以来,我已经在练习您的最后一个建议nullptr,尽管我不知道它对我的代码有什么实际影响。感谢您的出色回答,尤其是付出的努力。给我带来了很多关于这个话题的启发。
Mark Garcia

“在某些标准库头文件中。” ->为什么不从一开始就写“ cstddef”?
mxmlnkn

为什么我们应该允许nullptr转换为布尔类型?您能详细说明一下吗?
罗伯特·王

...用来表示没有值的指针...变量总是有一个值。可能是噪音,或者0xccccc....,但是,无值变量是一个固有的矛盾。
3Dave

“情况3:确定-属性3”(行bool flag = nullptr;)。不,不是,我在使用g ++ 6进行编译时收到以下错误:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg

23

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

考虑:

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

简而言之,0是一个特殊,但是值不能通过仅系统类型传播。转发功能是必不可少的,0不能处理它们。因此,绝对有必要引入nullptr,其中类型是特殊的,并且类型确实可以传播。实际上,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); }

7
滑稽。答案的一半与其他两个答案相同,根据您的说法,这是“相当不正确”的答案!!!
纳瓦兹

转发问题也可以通过强制转换解决。forward((int*)0)作品。我想念什么吗?
jcsahnwaldt恢复莫妮卡

5

nullptr的基础

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

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

优点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作为返回类型时的nullptrover的另一个优点NULL(0)

例如,假设您在代码库中遇到此问题:

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

如果您不知道(或不容易发现)findRecord返回什么,则可能不清楚结果是指针类型还是整数类型。毕竟,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 & lockAndCallF3。因此可以用模板来概括。我编写了模板函数,lockAndCall而不是lockAndCallF1, 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,这与期望的std::shared_ptr<int>参数不兼容f1。调用中传递的0 lockAndCall旨在表示空指针,但实际传递的是int。尝试将此int作为a传递给f1 std::shared_ptr<int>是类型错误。对lockAndCall0 的调用失败,因为在模板内部将int传递给需要的函数std::shared_ptr<int>

涉及呼叫的分析NULL基本相同。当NULL传递给时lockAndCall,将为参数ptr推导一个整数类型,并且在将ptrint或类似int的类型传递给时f2,会产生类型错误,该类型期望得到a std::unique_ptr<int>

相反,涉及的呼叫nullptr没有问题。当nullptr被传递到lockAndCall,该类型ptr推断为std::nullptr_t。当ptr传递给时f3,存在从std::nullptr_t到的隐式转换int*,因为std::nullptr_t隐式转换为所有指针类型。

建议您在每次要引用空指针时,都使用nullptr而不是0或NULL


4

nullptr演示示例的方式并没有直接的优势。
但是考虑一下您有两个同名函数的情况;1个int,另一个int*

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

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

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
供参考,decltype(nullptr)std::nullptr_t
克里斯

2
@MarkGarcia,据我所知,这是一个成熟的类型。
克里斯,2012年

5
@MarkGarcia,这是一个有趣的问题。cppreference具有:typedef decltype(nullptr) nullptr_t;。我想我可以看看标准。啊,找到了它:注意:std :: nullptr_t是一个既不是指针类型也不是成员类型指针的独特类型;相反,此类型的prvalue是空指针常量,并且可以转换为空指针值或空成员指针值。
克里斯,2012年

2
@DeadMG:动机不止一个。未来几年,程序员自己会发现许多其他有用的东西。所以你不能说有只有一个真正使用nullptr
纳瓦兹2012年

2
@DeadMG:但是您说这个答案“很不正确”,只是因为它没有提到您在答案中提到的“真正动机”。不仅这个答案(还有我的答案)收到了您的反对。
纳瓦兹2012年

4

正如其他人已经说过的那样,它的主要优势在于过载。尽管显式int和指针重载很少见,但请考虑使用标准库函数,例如std::fill(在C ++ 03中已经使我痛苦不止一次):

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

无法编译:Cannot convert int to MyClass*


2

IMO比那些重载问题更为重要:在深层嵌套的模板构造中,很难不对类型进行跟踪,并且给予显式签名是相当努力的。因此,对于您使用的所有内容,将其更精确地聚焦于预期的目的会更好,它将减少对显式签名的需求,并允许编译器在出现问题时产生更深入的错误消息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.