在C++的编程习惯中,我们声明一个变量的同时就需要记得在代码合适的位置对其进行初始化。对于指针类型的变量,这点尤为重要。在常见的程序bug中,出现频率比较高的就是指针未初始化,指针使用前未判空。
典型的指针初始化步骤是将指针指向一个“空”的位置0。如果程序员无意对该指针所指向的地址进行赋值,程序运行时就会异常退出,因为计算机系统不允许用户程序写地址为0的内存空间。
常见的指针初始化语法:
int *myPointer = 0;
int *myPointer = NULL;
一般情况下,NULL是一个宏定义。在头文件stddef.h中可以找到定义:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
NULL 在C语言中被定义为((void *)0), 在C++中被定义为字面量0
现有的空值指针的缺点:
#include
void func(char *p)
{
printf("invoke func(char *)...\n");
}
void func(int i)
{
printf("invoke func(int)...\n");
}
int main(void)
{
func(NULL);
func(0);
func((char*)0);
return 0;
}
上述程序在VS2015下输出结果为:
invoke func(int)…
invoke func(int)…
invoke func(char *)…
上述程序在linux下运行报错:
zbb@ubuntu:~/ProC$ g++ nullptr.cpp -o null
nullptr.cpp: In function ‘int main()’:
nullptr.cpp:15:11: error: call of overloaded ‘func(NULL)’ is ambiguous
func(NULL);
^
nullptr.cpp:3:6: note: candidate: void func(char*)
void func(char *p)
^
nullptr.cpp:8:6: note: candidate: void func(int)
void func(int i)
^
在这里VS2015采用了stddef.h头文件中NULL的定义,将NULL定义为0. 因此使用NULL作为参数调用和使用字面量零作为参数调用的结果相同, 都进入了func(int)的调用。这实际与我们编写代码的初衷相违。
引起这个问题的原因是:字面量0的二义性,在C++98中,字面常量0的类型就可以是一个整型,也可以是一个无类型的指针(void*). 所以要想调用到func(char *)这个版本函数,就必须对字面常量0进行强制类型转换【(char*)0】调用,否则编译器会优先把0解析成一个整型常量。
Linux平台下编译出错的原因:g++编译器直接将NULL转换为编译器内部标识(__null),并在编译时期做了一些分析,一旦遇到二义性就停止编译向用户报告错误。虽然这在一定程度上缓解二义性带来的麻烦,但是由于标准没有认定NULL为一个编译时期的标识,所以可能带来代码移植上面的限制。
Mordern C++的解决方案:
在C++11标准中,出于兼容性的考虑,字面常量0的二义性并没有被消除。但是标准为二义性带来了解决方案:nullptr.
在C++11中nullptr是一个“指针空值类型”的常量,指针空值类型被命名为nullptr_t.
nullptr_t在头文件(stddef.h)中的定义:
#if defined(__cplusplus) && __cplusplus >= 201103L
#ifndef _GXX_NULLPTR_T
#define _GXX_NULLPTR_T
typedef decltype(nullptr) nullptr_t;
#endif
#endif /* C++11. */
nullptr_t的定义方式与传统的先定义类型,在通过类型声明值的做法相反(充分利用了decltype返回值类型推导的功能)。
在现有的编译器情况下, 使用nullptr_t的时候必须#include(#include某些头文件的时候会间接的#include, 如#include).而nullptr则不用。这是由于nullptr是关键字,nullptr_t是通过推导得到的。
nullptr的优势:nullptr是有类型的,且仅可以被隐式类型转化为指针类型。
对上述代码略作修改:
#include
void func(char *p)
{
printf("invoke func(char *)...\n");
}
void func(int i)
{
printf("invoke func(int)...\n");
}
int main(void)
{
func(nullptr);
func(0);
func((char*)0);
return 0;
}
输出结果:
invoke func(char*)…
invoke func(int)…
invoke func(char *)…
在把NULL替换成nullptr后,我们将获得更加健壮的代码,不会出现gcc在编译时期给出错误提示不兼容问题。
nullptr是一个指针空值常量, nullptr_t是指针空值类型。通常情况下,可以通过nullptr_t来声明一个指针空值类型的常量(实际用处不大)。
除去nullptr与nullptr_t 外,C++中还有各种内置类型。C++11标准严格规定了数据间的关系。
#include
#include
using namespace std;
int main(void)
{
char* pBuffer = nullptr;//nullptr可以隐式转换为char*指针
//int iIndex1 = nullptr;//nullptr不能转换为整型
//int iIndex2 = reinterpret_cast(nullptr);//err
//nullptr与nullptr_t类型变量做比较
nullptr_t my_nullptr;
if (my_nullptr == nullptr)
cout << "my_nullptr == nullptr:" << (my_nullptr == nullptr) << endl;
else
cout << "my_nullptr == nullptr:" << (my_nullptr == nullptr) << endl;
if (my_nullptr < nullptr)
cout << "my_nullptr < nullptr " << endl;
else
cout << "my_nullptr >= nullptr " << endl;
if (0 == nullptr)
cout << "0 == nullptr" << endl;
if(nullptr)
cout << "nullptr" << endl;
//不可以进行算术运算,代码编译错误
//nullptr++;
//nullptr *= 5;
//下面操作可以正常执行
cout << sizeof(nullptr) <<" "<< sizeof(void*) << endl;
typeid(nullptr);
throw(nullptr);
return 0;
}
zbb@ubuntu:~/ProC$ g++ null1.cpp -o app -std=c++11
zbb@ubuntu:~/ProC$ ./app
my_nullptr == nullptr:1
my_nullptr >= nullptr
0 == nullptr
8 8
terminate called after throwing an instance of 'decltype(nullptr)'
已放弃 (核心已转储)
如果有的编译器可以编译if(nullptr)或者if(0==nullptr)语句,可能是由于编译器版本不够新。一些老版本还允许nullptr向bool做隐式类型转化。C++11标准不允许nullptr向bool做隐式类型转化。
笔者编译时使用编译器版本:gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11)
虽然nullptr_t看起来是一个指针类型,用起来更是。但是把nullptr_t应用在模板推导时,模板只把它当做一个普通的类型进行推导。而不会把他作为T*指针。
#include
using namespace std;
template void g(T* t){}
template void h(T t){}
int main(void)
{
//g(nullptr); //编译失败,nullptr的类型是nullptr_t, 不是指针
g((float*)nullptr);//推导出T= float
h(0);//推导出T= int
h(nullptr);//推导出T=nullptr_t
h((float*)nullptr);//推导出T=faloat*
return 0;
}
g(nullptr)不会被编译器推导为某种基本类型的指针(或者void*指针),因此要让编译器成功推导出nullptr的类型,必须做显示类型转换。
main.cpp
void foo()
{
//int *pbuffer1 = (void*)0;//err
int *pbuffer2 = nullptr;
}
#include
using namespace std;
int main(void)
{
nullptr_t my_nullptr;
printf("&my_nullptr: %p\n", &my_nullptr);
printf("my_nullptr == nullptr: %p\n", my_nullptr == nullptr);
const nullptr_t&& default_nullptr = nullptr;//default_nullptr是nullptr的一个右值引用
printf("&default_nullptr: %p\n", &default_nullptr);
return 0;
}
运行结果:
&my_nullptr: 0x7ffe9582f280
my_nullptr == nullptr: 1
&default_nullptr: 0x7ffe9582f288