7.1 指针空值——nullptr
类别:所有人
7.1.1 指针空值:从0到NULL,再到nullptr
在良好的C++编程习惯中,声明一个变量的同时,总是需要记得在合适的代码位置将其初始化。对于指针类型的变量,这一点尤其应当注意。未初始化的悬挂指针通常会是一些难于调试的用户程序的错误根源。
典型的初始化指针是将其指向一个“空”的位置,比如0。由于大多数计算机系统不允许用户程序写地址为0的内存空间,倘若程序无意中对该指针所指地址赋值,通常在运行时就会导致程序退出。虽然程序退出并非什么好事,但这样一来错误也容易被程序员找到。因此在大多数的代码中,我们常常能看见指针初始化的语法如下:
int*my_ptr=0;
或者是使用NULL:
int*my_ptr=NULL;
一般情况下,NULL是一个宏定义。在传统的C头文件(stddef.h)里我们可以找到如下代码:
undef NULL
if defined(__cplusplus)
define NULL 0
else
define NULL((void*)0)
endif
可以看到,NULL可能被定义为字面常量0,或者是定义为无类型指针(void*)常量。不过无论采用什么样的定义,我们在使用空值的指针时,都不可避免地会遇到一些麻烦。让我们先看一个关于函数重载的例子。这个例子我们引用自C++11标准关于nullptr的提案,并进行了少许修改,具体如代码清单7-1所示。
代码清单7-1
include <stdio.h>
void f(char*c){
printf("invoke f(char*)\n");
}
void f(int i){
printf("invoke f(int)\n");
}
int main(){
f(0);
f(NULL);//注意:如用gcc编译,NULL转化为内部标识__null,该语句会编译失败
f((char*)0);
}
//编译选项:xlC-+7-1-1.cpp
在代码清单7-1所示的例子当中,用户重载了f函数,并且试图使用f(NULL)来调用指针的版本。不过很可惜,当使用XLC编译器编译以上语句并运行时,会得到以下结果:
invoke f(int)
invoke f(int)
invoke f(char*)
在这里,XLC编译器采用了stddef.h头文件中NULL的定义,即将NULL定义为0。因此使用NULL做参数调用和使用字面量0做参数调用版本的结果完全相同,都是调用到了f(int)这个版本。这实际与程序员编写代码的意图相悖。
引起该问题的元凶是字面常量0的二义性,在C++98标准中,字面常量0的类型既可以是一个整型,也可以是一个无类型指针(void)。如果程序员想在代码清单7-1中调用f(char)版本的话,则必须像随后的代码一样,对字面常量0进行强制类型转换((void*)0)并调用,否则编译器总是会优先把0看作是一个整型常量。
虽然这个问题可以通过修改代码来解决,但为了避免用户使用上的错误,有的编译器做了比较激进的改进。典型的如g++编译器,它直接将NULL转换为编译器内部标识(__null),并在编译时期做了一些分析,一旦遇到二义性就停止编译并向用户报告错误。虽然这在一定程度上缓解了二义性带来的麻烦,但由于标准并没有认定NULL为一个编译时期的标识,所以也会带来代码移植性的限制。
注意 关于nullptr和void的翻译,void习惯被翻作无类型指针,我们这里把nullptr翻作指针空值。
在C++11新标准中,出于兼容性的考虑,字面常量0的二义性并没有被消除。但标准还是为二义性给出了新的答案,就是nullptr。在C++11标准中,nullptr是一个所谓“指针空值类型”的常量。指针空值类型被命名为nullptr_t,事实上,我们可以在支持nullptr的头文件(cstddef)中找出如下定义:
typedef decltype(nullptr)nullptr_t;
可以看到,nullptr_t的定义方式非常有趣,与传统的先定义类型,再通过类型声明值的做法完全相反(充分利用了decltype的功能)。我们发现,在现有编译器情况下,使用nullptr_t的时候必须#include<cstddef>(#include有些头文件也会间接#include<cstddef>,比如<iostream>),而nullptr则不用。这大概就是由于nullptr是关键字,而nullptr_t是通过推导而来的缘故。
而相比于gcc等编译器将NULL预处理为编译器内部标识__null,nullptr拥有更大的优势。简单而言,由于nullptr是有类型的,且仅可以被隐式转化为指针类型,那么对于代码7-1的例子,nullptr做参数则可以成功调用f(char*)版本的函数,而不是像gcc对NULL的处理一样,仅仅给出一个出错提示,好让程序员去修改代码。
我们来看看代码清单7-2所示的例子。
代码清单7-2
include <iostream>
using namespace std;
void f(char*p){
cout<<"invoke f(char*)"<<endl;
}
void f(int){
cout<<"invoke f(int)"<<endl;
}
int main()
{
f(nullptr);//调用f(char*)版本
f(0);//调用f(int)版本
return 0;
}
//编译选项:g++7-1-2.cpp-std=c++11
可以看到,在改为使用nullptr之后,用户能够准确表达自己的意图,也不会再出现在XLC编译器上调用了f(int)版本而在gcc上却在编译时期给出了错误提示的不兼容问题。因此,通常情况下,在书写C++11代码想使用NULL的时候,将NULL替换成为nullptr我们就能获得更加健壮的代码。