有一点需要明确:字面值0是int类型,并非指针类型。如果C++在只能使用指针的上下文中发现一个0,它会勉强的将0解释为空指针,但这是一个不得已而为之的行为。当然,C++的基本观点就是认为0是一个整形而不是指针。
实际上,NULL也是如此。因为标准允许不同的实现赋予NULL非int的整形(比如long),所以NULL在技术细节上有些不清不楚的成分。其实,这也不重要。关键在于0和NULL根本就不是指针类型。
在c++98中,这意味着指针和整数类型的重载可能会导致意外的结果。向下面这样的重载传递0或NULL从不会调用指针的重载:
void f(int); // three overloads of f
void f(bool);
void f(void*);
f(0); // calls f(int), not f(void*)
f(NULL); // might not compile, but typically calls
// f(int). Never calls f(void*)
f(NULL)的不确定,就是因为NULL的类型上没有明确的规定。假设NULL被定义为0L,那f(NULL)的调用就有多义性了,因为从long到int,long到bool和从0L到void*的类型转换都被认为是一样好的。这就尴尬的很了,所以在C++98中最好避免指针和整形的重载。这个指导原则同样适用于C++11,因为C++11伟大的兼容。
nullptr的优势在于,它不具备整形类型。事实上,它也不具备指针类型,但是你可以把它想象为任意类型的指针。nullptr的实际类型是std::nullptr_t,并且在一个非常棒的循环定义下,std::nullptr_t被定义为nullptr的类型。std::nullptr_t可以隐式的转换到所有原始指针类型,这就使nullptr可以扮演成所有类型的指针。
所以,下面这个调用就很好,也没什么歧义。
f(nullptr); // calls f(void*) overload
除此之外,nullptr还可以提升代码的可读性,尤其是涉及到auto变量的应用。如下:
auto result = findRecord( /* arguments */ );
// 1.
if (result == 0){
// ...
}
// 2.
if (result == nullptr){
// ...
}
如果我们不知道或者不太容易知道findRecord的返回类型,那么上面代码的第2种写法就可以很清晰的指出result是否为指针了。第一种就比较模糊,有可能是整数0,也有可能是空指针。
nullptr在有模板存在的情况下,变现的更好。假设有如下代码,仅当适当的mutex被锁定时,才会执行一些函数。每个函数的形参是不同类型的指针:
int f1(std::shared_ptr<Widget> spw); // call these only when
double f2(std::unique_ptr<Widget> upw); // the appropriate
bool f3(Widget* pw); // mutex is locked
std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxGuard = std::lock_guard<std::mutex>;
// ...
{
MuxGuard g(f1m); // lock mutex for f1
auto result = f1(0); // pass 0 as null ptr to f1
} // unlock mutex
// ...
{
MuxGuard g(f2m); // lock mutex for f2
auto result = f2(NULL); // pass NULL as null ptr to f2
}
// ...
// ...
{
MuxGuard g(f3m); // lock mutex for f3
auto result = f3(nullptr); // pass NULL as null ptr to f3
}
虽然前两个调用没有用nullptr,但是代码也能跑。当然,像上面这样写代码,有点冗余,我们用模板改造一下:
template<typename FuncType,
typename MuxType,
typename PtrType>
auto lockAndCall(FuncType func,
MuxType& mutex,
PtrType ptr) -> decltype(func(ptr))
{
MuxGuard g(mutex);
return func(ptr);
}
// C++14
template<typename FuncType,
typename MuxType,
typename PtrType>
decltype(auto) lockAndCall(FuncType func,
MuxType& mutex,
PtrType ptr)
{
MuxGuard g(mutex);
return func(ptr);
}
调用者可能会写出下面这样的代码:
auto result1 = lockAndCall(f1, f1m, 0); // error!
// ...
auto result2 = lockAndCall(f2, f2m, NULL); // error!
// ...
auto result3 = lockAndCall(f3, f3m, nullptr); // fine
auto result1 = lockAndCall(f1, f1m, nullptr); // also fine
auto result2 = lockAndCall(f2, f2m, nullptr); // also fine
但是,前两种根本就编译不过。在第一个调用中,当0被传给lockAndCall时,模板类型推导启动并推算出它的类型。0的类型一直是int,所以ptr的类型被定为int,这意味着lockAndCall内部的func调用时,传入的时int,与f1所期望的std::shared_ptr参数不兼容。lockAndCall调用传入的0,不会像最前那样被解释成空指针。NULL的情况也是一样。相反,nullptr就不会有这个问题。当nullptr被传给lockAndCall时,ptr的类型被推导为std::nullptr_t。当ptr再传给f3时,会有一个std::nullptr_t到Widget*的隐式转换,因为std::nullptr_t可以被隐式转换到所有指针类型。
由于模板的类型推导和nullptr不代表任何整型,这两点就有效的支撑了论点:乖乖的用nullptr表示空指针是个好习惯!