使用nullptr有什么好处?

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

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

那么,分配指针nullptr的优点是分配NULL0值呢?

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

 void f(char const *ptr); void f(int v); f(NULL); //which function will be called? 

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

所以,这样的问题的解决scheme是使用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的types被推导为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)原因。

C ++ 11引入了nullptr ,它被称为Null指针常量,它改善了types的安全性,并且解决了与现有的实现相关的空指针常量NULL不同的不确定情况 。 为了能够理解nullptr的优点。 我们首先需要了解什么是NULL以及与之相关的问题。


什么是NULL

Pre C ++ 11 NULL被用来表示没有值的指针或指向任何有效的指针。 与stream行的概念相反, 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定义为在某些标准库头文件中定义的实现定义的macros。 NULL的起源是从C和C ++inheritance它的C. C标准定义NULL为0或(void *)0。 但在C ++中有一个微妙的差异。
C ++不能接受这个规范。 与C不同,C ++是一种强types语言(C从void*到任何types都不需要显式强制转换,而C ++强制显式强制转换),这使得C标准中定义的NULL定义在很多C ++expression式中是无用的:

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

如果NULL被定义为(void *)0。 上述两个expression式都不起作用。

  • 情况1:不会编译,因为从void *std::string需要自动转换。
  • 情况2:不会编译,因为需要从void *为指向成员函数的指针。

因此,与C不同,C ++ Standard要求将NULL定义为数字文字0或0L。


那么当我们有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,在函数重载parsing中可能会有很多混淆。

示例程序:

 #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*版本,因为0也是一个空指针。
  • 情况3:如果NULL被定义为0
    调用doSomething(int)时,也许doSomething(char *)意图,也许在运行时导致逻辑错误。
    而如果NULL被定义为0L
    调用不明确,导致编译错误。

所以根据实现,相同的代码可以给出各种结果,这显然是不希望的。 当然,C ++标准委员会想纠正这个问题,这是nullptr的主要动机。


那么什么是nullptr ,以及它如何避免NULL问题?

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

属性1:它有它自己的typesstd::nullptr_t
属性2:它可以隐式转换,并且可以与任何指针types或指针成员types相比,但是
属性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

C ++ 11的经验法则是,只要你以前使用过NULL,就可以开始使用nullptr了。


标准参考:

C ++ 11标准:C.3.2.4macrosNULL
C ++ 11标准:18.2types
C ++ 11标准:4.10指针转换
C99标准:6.3.2.3指针

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

考虑:

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

简而言之,0是一个特殊的 ,但是值不能通过仅用于系统的types传播。 转发function是必不可less的,0不能处理它们。 因此,引入nullptr是绝对必要的,其中types是特殊的,types确实可以传播。 事实上,MSVC团队在实施了右值引用之后,必须提前引入nullptr ,然后为自己发现这个陷阱。

还有一些其他的angular落案例, 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); } 

以你所展示的例子的方式来使用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); 

来自Bjarne的网页的其他链接 。
不相关,但在C + + 11方面注意:

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

正如其他人已经说过,其主要优势在于超载。 虽然明确的int与指针重载可能很less见,但考虑标准库函数,如std::fill (它在C ++ 03中多次咬过我):

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

不能编译: Cannot convert int to MyClass*

国际海事组织比那些超载问题更重要:在深度嵌套的模板结构中,很难不丢失types,给出明确的签名是相当大的努力。 因此,对于您所使用的所有内容,更精确地集中于预期目的,效果会越好,这样就会减less对显式签名的需求,并且在出现问题时允许编译器生成更深入的错误消息。

基本的nullptr

std::nullptr_t是空指针文字nullptr的types。 它是std::nullptr_ttypes的一个前值/右值。 存在从nullptr到任何指针types的空指针值的隐式转换。

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

优点1 – 在指针和整数types上重载时消除歧义

在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而不是null指针”)。

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

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

使用nullptr而不是0或NULL从而避免重载解决scheme的意外。

当使用auto作为返回types时, nullptr超过NULL(0)另一个优点

例如,假设你在代码库中遇到这个问题:

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

如果你不知道findRecord返回的是什么(或者不容易find),那么结果是指针types还是整数types可能并不清楚。 毕竟,0(什么结果是testing对)可以去任何一个方面。 另一方面,如果您看到以下内容,

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

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

优势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的时候,模板types的扣除就会发现它的types。 0的types是int,所以在这个调用lockAndCall的实例化中,这就是参数ptr的types。 不幸的是,这意味着在调用lockAndCall函数时,会传递一个int,这与f1期望的std::shared_ptr<int>参数不兼容。 在调用lockAndCall时传递的0是为了表示一个空指针,但实际得到的是int。 试图将这个int作为std::shared_ptr<int>传递给f1是一个types错误。 使用0调用lockAndCall失败,因为在模板中,int正在传递给需要std::shared_ptr<int>的函数。

涉及NULL的调用的分析基本相同。 当将NULL传递给lockAndCall ,会为参数ptr推导一个整型,当ptr -int或inttypes的types传递给f2 ,会出现types错误,而f2会得到std::unique_ptr<int>

相反,涉及nullptr的调用没有问题。 当nullptr被传递给lockAndCallptr的types被推断为std::nullptr_t 。 当ptr被传递给f3std::nullptr_t被隐式转换为int* ,因为std::nullptr_t隐式转换为所有的指针types。

build议,每当你想引用一个空指针,使用nullptr,而不是0或NULL