使用nullptr有什么好处?
这段代码在概念上对三个指针(安全指针初始化)做同样的事情:
int* p1 = nullptr; int* p2 = NULL; int* p3 = 0;
那么,分配指针nullptr
的优点是分配NULL
或0
值呢?
在那个代码中,似乎没有优势。 但是请考虑以下重载函数:
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_t
types的一个前值/右值。 存在从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
被传递给lockAndCall
, ptr
的types被推断为std::nullptr_t
。 当ptr
被传递给f3
, std::nullptr_t
被隐式转换为int*
,因为std::nullptr_t
隐式转换为所有的指针types。
build议,每当你想引用一个空指针,使用nullptr,而不是0或NULL
。