深入理解C++11:01指针空值类型常量nullptr

深入理解C++11:01指针空值类型常量nullptr

1.指针空值的演进

在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 <cstdio>

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 <cstdio>

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在编译时期给出错误提示不兼容问题。

2. nullptr与nullptr_t

nullptr是一个指针空值常量, nullptr_t是指针空值类型。通常情况下,可以通过nullptr_t来声明一个指针空值类型的常量(实际用处不大)。

除去nullptr与nullptr_t 外,C++中还有各种内置类型。C++11标准严格规定了数据间的关系。

  • 所有定义的nullptr_t类型的数据都是等价的,行为也是完全一致。
  • nullptr_t类型数据可以隐式转换为任意一个指针类型
  • nullptr_t不可以转换为非指针类型,即使使用reinterpret_cast()的方式
  • nullptr_t类型数据不适用于算术运算表达式
  • nullptr_t类型数据可以用于关系运算表达式,但仅能和nullptr_t类型数据和指针类型数据比较,当且仅当关系运算符为==,<=, >=时返回true
#include <iostream>
#include <typeinfo>

using namespace std;

int main(void)
{
	char* pBuffer = nullptr;//nullptr可以隐式转换为char*指针
	//int iIndex1 = nullptr;//nullptr不能转换为整型
	//int iIndex2 = reinterpret_cast<int>(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 <iostream>

using namespace std;

template<typename T> void g(T* t){}
template<typename T> 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的类型,必须做显示类型转换。

3.关于nullptr规则的讨论

  • 在C++标准中,nullptr类型数据所占用的内存大小和void*相同,即 sizeof(nullptr_t) == sizeof(void*)
  • 区别: nullptr是一个编译时期的常量,它的名字是一个编译时期的关键字,能够为编译器所认识。而(void*)0是一个强制类型转换表达式,返回的是一个void*指针类型。
  • 在C++语言中, nullptr到任何指针的转换是隐式的,而(void*)0则必须经过类型转换后才能使用。在C语言中void*指针可以隐式转换为任意指针的。
  • nullptr_t对象的地址可以被用户使用,这个规则有个例外:nullptr也是一个nullptr_t的对象,C++11标准规定用户不能获取nullptr的地址。不要对nullptr取地址。因为nullptr被定义为一个右值引用,取其地址没有意义。
main.cpp

void foo()
{
	//int *pbuffer1 = (void*)0;//err
	int *pbuffer2 = nullptr;
}
#include<iostream>

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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Erice_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值