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
具体实现的依赖。