按照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 = π//常量指针,指向了常量
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看似应该使得指针成为一个常量指针,但并没有起到作用。