C++ NULL与nullptr
300 Words | Read in about 2 Min | View times
Overview
在C语言中,我们使用NULL来代表一个空指针,而这种做法在现代C++是不允许的。在现代C++中,NULL本质上是一个类型为long int的数值0,而空指针需要用C++11新引入的nullptr来表示。
那么在现代C++中,NULL和nullptr使用上有什么区别?为什么会有nullptr?本节内容将一一解答。
本系列文章将包括以下领域:
本章其他内容请见 《现代C++》
nullptr
nullptr是C++11引入的一个新关键字,用来替代以前的NULL。nullptr提供了一个类型安全的指针值,用来表示一个空指针。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
从运行结果可以看出,在赋值指针方面,nullptr、NULL和0的结果都是一样的。
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类型的纯右值。
在这个例子中,NULL和0都是整型类型,从这个意义上看,由它们转换而来的空指针y、z可以转换为std::nullptr_t纯右值nullptr,所以x、y和z是等价的。
所以,在指针赋值方面,NULL和nullptr是等价的。
那么既然对于指针赋值方面,NULL和nullptr是等价的,为什么还要区分这两者呢?
函数重载
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作为实参传递给形参时,发生了一次指针赋值,前面我们说过了,NULL和nullptr在指针赋值方面是等价的,所以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具体实现的依赖。