为什么要引入nullptr呢?任何事物的产生都有其缘由和意义,nullptr也不例外。一切还得从0开始说。C++98中的0即表示整数字面值0,又表示指针的0值:
class Foo
{
public:
void bar() {}
};
int *p = 0; // 0表示普通指针的0值
void (Foo::*pBar)() = 0; // 0表示指向成员函数的指针的0值
std::shared_ptr<int> pi = 0; // 0表示智能指针的0值
int x = 0; // 0表示整数0
就算是用NULL表示空指针,也是换汤不换药,C++98规定NULL是一个整型0值,根据不同的编译器实现,可能是0或0L。更尴尬的是,可以拿NULL当合法的整型(int或者long)来使用:
int *q = NULL; // 看起来不错,NULL表示指针的0值
int y = NULL; // 尴尬
long z = NULL * NULL; // 尴尬
另外NULL还是个宏,宏的缺点很多,C++并不建议使用宏,而建议使用const变量代替。
关键的问题在于C++98没有一种方法可以区别整数0值和指针0值。硬生生的让0一人分饰两角。在重载整型和指针的场合就会遇到麻烦了:
void foo(int) {}
void foo(int*) {}
foo(0); // 调用foo(int)而不是foo(int*)
foo(NULL); // 如果NULL是0,则调用foo(int); 如果NULL是0L,则编译错误
foo(0)调用的是foo(int),因为0是int值,与foo(int)严格匹配,当NULL为0时,foo(NULL)也是同样的情况。如果NULL为0L时,则编译错误,因为没有一个函数是严格匹配,0L又同时可以转型为int和int*,编译器无法决定调用哪个函数。因此在C++98中,尽可能不以整型和指针重载函数是一个良好的习惯。
而我们调用foo(NULL)
的意图是调用foo(int*)
,却无论如何也做不到。这就造成了表象(用指针调用)和实质(用整数调用)的不一致,让人混乱且费解。如果需要调用foo(int*)
,必须将0显示转换成指针:
foo(static_cast<int*>(NULL)); // 调用foo(int*)
这样做麻烦至极。如果能有一个指针字面值表示空指针,就可以避免这样的转换。我们可以自己定义一个类,以实现这个目的(以下代码出自Scott Meyers教授的《Effective C++》第二版):
const // It is a const object...
class nullptr_t
{
public:
template<class T>
inline operator T*() const // convertible to any type of null non-member pointer...
{ return 0; }
template<class C, class T>
inline operator T C::*() const // or any type of null member pointer...
{ return 0; }
private:
void operator&() const; // Can't take address of nullptr
} nullptr = {};
关键的地方在于这个类可以通过operator T*
和operator T C::*
转换成任何类型的指针,却不能转换成int。引入了这个类和这个类的一个实例nullptr之后,就可以区分整型0值和指针0值了:
foo(0);
foo(nullptr);
这样做很好,但仍需做一些额外的工作,而且C++语言本身没有表示指针0值的东西,这不免有些缺憾。
于是,为了卸下0的重担,为了区别对待整数0值和指针0值,为了填补缺憾, C++11引入了nullptr。
nullptr
C++引入了新的关键字nullptr,表示指针的0值,虽然它不是指针类型,但是它能够转换成任何指针,而且它不能转换成int。
class Foo
{
public:
void bar() {}
};
int *p = nullptr; // nullptr表示普通指针的0值
void (Foo::*pBar)() = nullptr; // nullptr表示指向成员函数的指针的0值
std::shared_ptr<int> pi = nullptr; // nullptr表示智能指针的0值
int x = 0; // 0表示整数0
//int y = nullptr; // 错误!不能将nullptr转换为int值
这样就将整数0值和指针0值区分开了。也可以解决整型和指针重载的问题。
void foo(int) {}
void foo(int*) {}
foo(0); // 调用foo(int)
foo(nulptr); // 调用foo(int*)
虽然nullptr的引入可以解决整型和指针重载函数的问题,但是C++11没有消除0的空指针性质,因此该问题依然存在,只是在nullptr的掩护下被隐藏了而已。因此尽可能不以整型和指针重载函数仍是一个良好的习惯。
typedef decltype(nullptr) nullptr_t;
std::nullptr_t
nullptr也有其自身的类型,它的类型为std::nullptr_t
。std::nullptr_t
本身不是指针类型,但是它可以转换为任何指针类型,这也就是nullptr可以看做任何指针类型的原因。它的定义如下:
typedef decltype(nullptr) nullptr_t;
上面是一个很有趣的类型定义。通常情况是先有类型,然后才有该类型的变量。而上面的定义则是,先有一个某类型的字面值,再由这个字面值定义它的类型。妙哉!
可以定义以nullptr_t类型作为形参的函数,该函数只能接指针0值:
void f(std::nullptr_t) {}
f(0);
f(nullptr);
f(NULL);
int *p = nullptr;
//f(p); // 错误,p不是指针0值
int x = 0;
//f(x); // 错误,x不是指针0值
判断是否是std::nullptr_t
c++14提供了一个类模板来判断一个类型是否是std::nullptr_t:
template< class T >
struct is_null_pointer;
该模板提供了一个bool类型的类静态常量value,表示T是否为std::nullptr_t。并且重载了operator bool和operator()用来返回value的值。
assert(std::is_null_pointer<std::nullptr_t>::value == true);
assert(std::is_null_pointer<const std::nullptr_t>::value == true);
assert(std::is_null_pointer<volatile std::nullptr_t>::value == true);
assert(std::is_null_pointer<const volatile std::nullptr_t>::value == true);
assert(std::is_null_pointer<int *>::value == false);
auto nilp = std::is_null_pointer<std::nullptr_t>();
assert(nilp == true);
assert(nilp() == true);
c++17定义了一个常量值is_null_pointer_v以简化操作:
template< class T >
inline constexpr bool is_null_pointer_v = is_null_pointer<T>::value;
用法:
assert(std::is_null_pointer_v<std::nullptr_t> == true);
总结
- 在需要空指针的场合使用nullptr替代0和NULL。
- 不以整型和指针重载函数是一个良好的习惯。
- nullptr的类型是std::nullptr_t。
- c++11与C++14引入了is_null_pointer和is_null_pointer_v判断一个类型是否是std::nullptr_t。