C++的顶层const和底层const的理解

按照c++ primer里面的描述习惯,认为对象为“具有某种数据类型的内存空间”。本文使用IDE为VS2017 社区版。

const与引用:对常量的引用(reference to const)

	const int ci = 0;//常量int对象
	int &a = ci;//报错

在这里插入图片描述
第二个提示说得很清楚,将 “int &” 类型的引用绑定到 “const int” 类型的初始值设定项时,限定符被丢弃,这是因为引用的类型必须与其所引用对象的类型一致。
结论:非常量引用不能绑定到常量上(第二个提示),无法将“const int”类型转换为“int &”(转换方向从右往左,即第一个提示)。

	int i = 0;//非常量int对象
	const int ci = 0;//常量int对象

	const int &a = i;//指向常量的引用(一般称为常量引用),绑定到非常量
	const int &b = ci;//指向常量的引用,绑定到常量

在这里插入图片描述
虽然上面提到,引用的类型必须与其所引用对象的类型一致,但也有两种例外情况(本文将讲第一种例外情况):只要等号右边的表达式的结果,能转换为引用的类型。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至一般表达式。
结论:
1)如上两个引用都是常量引用(reference to const,对常量的引用)。
2)如上代码const int &a = i,可以把int类型转换为const int &类型,即可以把常量引用绑定到非常量对象上,但基本类型得一样。
3)如上代码const int &b = ci,把常量引用绑定到常量对象,这是正常操作,类型一样其中没有转换过程。
4)如上四个量,只有i可以进行赋值操作,因为只有它是非常量,赋值后,它的引用a的值也随着改变。换句话说,不能通过常量引用a来赋值,但由于a绑定的是非常量i,所以i可以进行赋值操作。

	double dval = 3.14;
	int $vi = dval;
	const int $vii = dval;
	std::cout << vi << std::endl;
	std::cout << vii << std::endl;

在这里插入图片描述
初始化引用时,如果等号两边基本类型不一样,就可能会丢失数据。
int $vi = dval,如提示信息所示,会创建一个临时量,从“double”转换到“int”。
const int $vii = dval,如提示信息所示,会创建一个临时量,从“double”转换到“int”,再转换到“const int”。
所以判定这两个标识符都是未声明的。

	int a = 1;
	double &b = a;//报错
	const double &c = a;//不报错

在这里插入图片描述
从int转double不会丢失数据,但一样这里会产生临时量,临时量是一个prvalue右值,不能绑定给non-const reference,但可以绑定给const reference,同时其生命周期延长至这个const reference的生命周期。

const与指针:指向常量的指针(pointer to const)

	const int ci = 0;//常量int对象
	int *a = &ci;//报错

在这里插入图片描述
结论:与引用一样,非常量指针不能指向常量对象。

	int i = 1;//非常量int对象
	const int ci = 5;//常量int对象

	const int *a = &i;//指向常量的指针,指向了非常量
	const int *b = &ci;//指向常量的指针,指向了常量
	i = 10;

在这里插入图片描述
指针的类别必须与其所指对象的类型一致,但有两种例外情况(本文将讲第一种):指向常量的指针,指向了非常量对象。
结论:
1)如上两个指针都是指向常量的指针(pointer to const)。
2)如上代码const int *a = &i,可以把指向常量的指针,指向非常量对象。
3)如上代码const int *b = &ci,可以把指向常量的指针,指向常量对象,这是正常操作。
4)如上四个量,只有i可以进行赋值操作,因为只有它是非常量。换句话说,不能通过指向常量的指针a来赋值,但由于a指向的是非常量i,所以i可以进行赋值操作。

常量引用,和指向常量的指针一样,虽然字面上都是说的指向常量,但没有规定其所指对象必须是一个常量。只是因为它们认为自己指向的是常量,所以不能通过常量引用或指向常量的指针,来改变指向的对象的值。但如果指向的对象是非常量,那么这个非常量本身进行赋值操作就是正常操作。

const与指针:常量指针(const pointer)

指针是对象,因此可以把指针本身定为常量。常量指针必须初始化,初始化完成后,它的值(指针存的地址)就不能改变了。

	int a = 0;//非常量
	int *const ai = &a;//常量指针,指向了非常量

	const double pi = 3.14;//常量
	const double *const api = &pi;//常量指针,指向了常量

	int * const * aii = &ai;//常量指针的二级指针
	const double * const * aapi = &api;//常量指针的二级指针

如果只是对常量指针解引用,那么解引用后得到指针指向的对象,根据指向对象为常量或者非常量,来决定常量指针解引用后可不可以赋值操作:

	*ai = 1;//可运行
	*api = 3.0;//报错,表达式必须是可修改的左值

对常量指针赋值,直接报错:

	ai = 0;//报错
	api = 0;//报错

在这里插入图片描述
最后两句,对两个常量指针取二级指针,二级指针的类型就是在一级指针的类型后面加个*。
对aii或者aapi解引用,其实就相当与去掉最后一个*号。
读法是从右往走读,const修饰const左边那个星号,若const左边没有星号,那么就是指的最底层的对象。
在这里插入图片描述
在这里插入图片描述
对aii解引用的过程如上。

在这里插入图片描述
在这里插入图片描述
对aapi解引用的过程如上。

顶层const和底层const(top-level const and low-level const)

对于一般对象来说,其实只有顶层const。而对于指针这种,本身是一个对象,又指向一个对象。所以,指针本身是不是常量,和指针指向对象是不是常量,是两个独立的问题。
用顶层top-level const表示指针本身是一个常量,用底层low-level const表示指针指向对象是一个常量。

引用

用于声明引用的const都是底层const。因为引用本身不是对象,所以不可能有顶层const。

	int i = 0;//非常量int对象
	const int ci = 0;//常量int对象

	const int &a = i;//指向常量的引用(一般称为常量引用),绑定到非常量
	const int &b = ci;//指向常量的引用,绑定到常量

在这里插入图片描述

指针

*const代表的是顶层const,指针存的地址不能改变。
const int代表的是底层const,指针指向一个常量,常量自然不能改变。

	int i = 0;
	int *const p1 = &i;
	//不能改变p1指针存的地址,顶层const
	const int ci = 42;
	//常量不能改变,也算是顶层const
	const int *p2 = &ci;
	//p2存的地址可以改变,但p2解引用后得到const int,不能改变,底层const
	const int *const p3 = p2;
	//分析p3类型,*const说明是顶层const,const int说明是底层const
	p2 = p3;

在执行对象的拷贝操作时,顶层const不会受影响:
1)const int *const p3 = p2中,p2没有顶层const,p3有顶层const。
2)p2 = p3中,执行前,p2没有顶层const,p3有顶层const,执行后,也是一样。
在这里插入图片描述
不管有么有执行p2 = p3,各个对象的类型一直都如上图所示,没有变过。

在执行对象的拷贝操作时,两个对象必须具有相同的底层const资格,或者能够转换(一般来说,非常量可以转换成常量,反之不行):
1)int *p = p2; int *p = p3;这两句都会报错,不管p2 p3是不是有顶层const,在拷贝时,都会只看成const int *,但是由于从“const int *”转换到“int *”是不可以的(常量不可以转换为非常量),所以报错。
在这里插入图片描述
2)p2 = p3可运行,p2 p3都是底层const,所以符合“两个对象必须具有相同的底层const资格”。虽然p3还是个常量指针。
3)p2 = &i可运行,&i后得到一个普通指针int *,可以从“int *”转换到“const int *”,符合“非常量可以转换成常量”。

小练习

来自C++ primer的2.4.3节练习。

	int i = 0;
	const int v2 = 0; int v1 = v2;
	int *p1 = &v1, &r1 = v1;
	const int *p2 = &v2, *const p3 = &i, &r2 = v2;

它们的类型如下,请先自行判断类型再与下表对照。
在这里插入图片描述

constexpr

constexpr即为const expression常量表达式。用于声明constexpr的类型一般为字面值类型literal type。指针、引用也属于字面值类型,但指针和引用定义成constexpr时,它们的初始值必须受到限制。
一个constexpr指针的初始值必须是一个存储在固定地址的对象,或者是nullptr或0。
一般来说,函数体内定义的变量不是存放在固定地址中的。但函数也允许定义一类有效范围超过函数自身的变量,这类变量也有固定地址。一个constexpr指针只能指向这些变量。

constexpr设置变量本身为顶层const:相当于在星号后面加个const。

	const int *p = nullptr;
	constexpr int *q = nullptr;
	constexpr const int *k = nullptr;

在这里插入图片描述

auto

auto自行推断类型,忽略顶层const,保留底层const。如果想用auto还保留顶层const,在auto前面加const即可,但只对非复合类型起作用。

	const int i = 42;

	auto j = i;
	const auto j2 = i;

	const auto &k = i;
	auto &k2 = i;

	auto *p = &i;
	const auto *p2 = &i;
	return 0;

在这里插入图片描述
const auto &k = i,使得i成为常量。
k2已经能推断出const int &,所以const auto &k = i前面的const没有起作用。
p已经能推断出const int *,const auto *p2 = &i前面的const看似应该使得指针成为一个常量指针,但并没有起到作用。

posted @ 2018-10-06 21:25  allMayMight  阅读(740)  评论(0)    收藏  举报