C++11/14/17 nullptr

nullptr的引入

为什么要引入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的掩护下被隐藏了而已。因此尽可能不以整型和指针重载函数仍是一个良好的习惯。

std::nullptr_t

nullptr也有其自身的类型,它的类型为std::nullptr_tstd::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::nullptr_t>::value == true);
assert(std::is_null_pointer std::nullptr_t>::value == true);
assert(std::is_null_pointer std::nullptr_t>::value == true);
assert(std::is_null_pointer volatile std::nullptr_t>::value == true);
assert(std::is_null_pointer *>::value == false);

auto nilp = std::is_null_pointer::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::value;

用法:

assert(std::is_null_pointer_v::nullptr_t> == true);

总结

  • 在需要空指针的场合使用nullptr替代0和NULL。
  • 不以整型和指针重载函数是一个良好的习惯。
  • nullptr的类型是std::nullptr_t。
  • c++11与C++14引入了is_null_pointer和is_null_pointer_v判断一个类型是否是std::nullptr_t。

你可能感兴趣的:(c++开发,c++11)