C++ NULL与nullptr

Share on:
300 Words | Read in about 2 Min | View times

Overview

在C语言中,我们使用NULL来代表一个空指针,而这种做法在现代C++是不允许的。在现代C++中,NULL本质上是一个类型为long int的数值0,而空指针需要用C++11新引入的nullptr来表示。

那么在现代C++中,NULLnullptr使用上有什么区别?为什么会有nullptr?本节内容将一一解答。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

nullptr

nullptr是C++11引入的一个新关键字,用来替代以前的NULLnullptr提供了一个类型安全的指针值,用来表示一个空指针。nullptr的实际类型为std::nullptr_t

在现代C++的最佳实践中,只要过去是使用NULL的场景,就应该开始改用nullptr

NULL

那么,我们的老朋友NULL怎么了?为什么到了现代C++就被嫌弃了呢?

NULL是一个宏,不是关键字,在C语言中它被定义为void*类型指针,而在C++中它只是数值0

1//<stddef.h>
2#undef NULL
3#ifndef __cplusplus
4#define NULL ((void *)0) //C语言
5#else
6#define NULL 0 //C++
7#endif

在C++中NULL不过是0,在传统C++把它当成空指针只是权宜之计。C++不允许将void*类型的指针隐式转换成其他指针类型,所以C语言对NULL的定义方式不适合C++。

cppreference指出:

The macro NULL is an implementation-defined null pointer constant, which may be

an integral constant expression rvalue of integer type that evaluates to zero (until C++11)

an integer literal with value zero, or a prvalue of type std::nullptr_t (since C++11)

翻译过来,就是NULL宏是由实现定义的空指针常量,但从C++11开始,NULL既可以是数值为0的整型字面量,也可以是std::nullptr_t类型的纯右值prvalue

NULL vs nullptr

我们先看一下这个例子来看二者有哪些相通之处:

1int* x = nullptr;
2int* y = NULL;
3int* z = 0;
4
5std::cout << x << ", " << y << ", " << z << std::endl; //0, 0, 0

从运行结果可以看出,在赋值指针方面,nullptrNULL0的结果都是一样的。

cppference指出:

A null pointer constant may be implicitly converted to any pointer and pointer to member type; such conversion results in the null pointer value of that type. If a null pointer constant has integer type, it may be converted to a prvalue of type std::nullptr_t.

大致意思是说,空指针常量可以隐式转换为任意指针类型,这种转换会产生该类型的空指针值。如果空指针常量具有整型类型,那么它可以转换为std::nullptr_t类型的纯右值。

在这个例子中,NULL0都是整型类型,从这个意义上看,由它们转换而来的空指针yz可以转换为std::nullptr_t纯右值nullptr,所以xyz是等价的。

所以,在指针赋值方面,NULLnullptr是等价的。

那么既然对于指针赋值方面,NULLnullptr是等价的,为什么还要区分这两者呢?

函数重载

C++是强类型语言,在函数重载的类型检查中更加严格。我们还是看一下例子来分析二者的不同:

1int foo(int x);
2int foo(int* p);
3
4foo(NULL); //调用的是foo(int x)还是foo(int* p)?产生二义性错误error: call of overloaded ‘foo(NULL)’ is ambiguous
5foo(nullptr); //明确调用foo(int* p)

从上面的例子可以看出,NULL作为参数传入函数中时,既可以当作整型数值0,又可以作为空指针常量隐式转换为int*类型的指针,那么两个重载函数都可以成功匹配,因此产生二义性错误。

模板

使用nullptr的另一个好处是在模板中可以用来特化某些版本的模板函数,而NULL不能用来特化模板:

 1template<typename T>
 2void bar(T* p) {
 3    std::cout << "bar(T* p)" << std::endl;
 4}
 5
 6//全特化版本
 7void bar(std::nullptr_t) {
 8    std::cout << "bar(std::nullptr_t)" << std::endl;
 9}
10
11int* x = nullptr;
12int* y = NULL;
13int* z = 0;
14
15bar(nullptr); //bar(std::nullptr_t)
16bar(NULL); //bar(std::nullptr_t)
17bar(x); //bar(T* p)
18bar(y); //bar(T* p)
19bar(z); //bar(T* p)

所以在这种情况下,可以在模板函数中区分出来nullptr(其类型为std::nullptr_t)和其他类型的指针。

注意到上面例子中bar(NULL)调用的版本是bar(std::nullptr_t)NULL作为实参传递给形参时,发生了一次指针赋值,前面我们说过了,NULLnullptr在指针赋值方面是等价的,所以bar(NULL)bar(nullptr)调用的结果也是等价的。

我们再来看一个例子:

 1void foo(int* p) {
 2    std::cout << "foo(int* p)" << std::endl;
 3}
 4
 5template<typename T>
 6void baz(T ptr) {
 7    foo(ptr);
 8}
 9
10baz(nullptr); //foo(int* p)
11baz(NULL); //编译错误,error: invalid conversion from ‘long int’ to ‘int*’ [-fpermissive]

从运行结果可以看出,NULL在模板参数中被推导为long int类型,而不是int*,最终导致函数参数类型不匹配,nullptr就没有这种问题。

总结

  • 在现代C++的最佳实践中,只要过去是使用NULL的场景,就应该开始改用nullptr

  • 从C++11开始,NULL既可以是数值为0的整型字面量,也可以是std::nullptr_t类型的纯右值prvalue,所以使用NULL容易引发二义性问题,建议全部使用nullptr

  • nullptr可以使代码更加健壮,减少对NULL具体实现的依赖。

Prev Post: 『C++智能指针』
Next Post: 『C++移动语义、万能引用、引用折叠、完美转发』