C++ 语法 const限定符

const限定符是常用且容易混淆的概念,很多书中讲解不清晰,甚至《C++ primer》中都有一些错误的论断或者不明确的区分,本文对const限定符做归纳总结。

一、在类型中

顶层const和底层const是一个非常容易让人遗忘和混淆的概念,这里包括常量词性(即使名词常量,也是形容词常量的)和C++类型定义从右向左的释义顺序与日常语言从左向右理解的顺序差异造成的混淆。

首先将常量指针和指向常量的指针加以区分:

1.1 const pointer V.S. pointer to const

a) const pointer 常量指针 (其实英文语义很明显,没有歧义,中文隐藏了“的”,即常量的指针)

这里的const和常量是形容词,其实表示的是常量的指针。指的是指针本身是常量。但C++语法的释义并非是正常的从左向右读,而是从右向左解释,例如:

int errNumb=0;
int *const ptr=& errNumb; //从右向左释义:ptr是常量的*(指针),指针指向int类型的对象,即常量指针指向int类型的对象

即我们说的常量指针const pointer在C++语法中实际是写成* const

禁止修改常量指针的值。

b) pointer to const指向常量的指针

这里的const是名词。

const double pi=3.14;
const double *cptr=& pi; //从右向左释义:cptr是*(指针),指针指向double类型的对象,double类型的对象是const的,即指针指向double类型的常量

对于指向常量的指针,禁止通过指针修改指向的变量的值。

 c) const pointer & pointer to const

const double pi=3.14;
const double *const cptr=& pi; //从右向左释义:cptr const的*(指针),指针指向double类型的对象,double类型的对象是const的,即常量指针指向double类型的常量。其中第一个const是pointer to const(即底层const),第二个const是const pointer(即顶层const)

1.2 顶层const与底层const

  • 顶层const可以表示任何数据类型是(自身)常量,例如:int , double, *, class等;
  • 底层const表示指针和引用的复合类型的基本类型部分是常量。

指针是对象,因此既有顶层const(指针本身),也有底层const(指针所指的对象)。 

引用不是对象,因此只存在reference to const / 对常量的引用,即底层const, 不存在顶层const/常量引用。(注意:《C++ primer》中文版中2.4.1的标题写的是const的引用,显然是错误的,引文版中的题目是references to const,大家不要被常量引用的说法误导,并没有常量引用,只有对常量的引用)

总结:reference to cosnt 和 pointer to const是底层const, const pointer等是顶层const。

1.3 拷贝时能否添加或去除const限定符?

这是一个非常重要的问题,常量成员函数的使用等、函数重载中const的形参和非const的形参属不属于不同类型的形参很多应用都是基于这个问题演化的。

a) 基本内置类型:

  • 可以用常量初始化一个非常量,也可以用非常量初始化常量,这是因为拷贝初始化后新的对象与原来的对象没有关系,const限定符不发挥作用。
int i=38;
const int j=i; //正确:用非常量初始化常量
int k=j;  //正确:用常量初始化非常量

b) 复合类型: 

  • 不能让普通指针指向常量(必须使用指向常量的指针);
const double pi=3.14;
double *ptr = π //错误:不能让普通指针指向常量,必须使用指向常量的指针,防止通过普通指针修改指向的常量导致的冲突( 不是指针试图修改常量时再去禁止,而是直接禁止将普通指针绑定到常量上,从源头避免问题的发生。这在更高层面是C++的语法设计的思想:不会等到冲突发生是再判断造成冲突的操作违法,而是直接在源头禁止可能造成冲突的操作。)
  • 不能让普通引用指向常量对象。
const int i= 38;
int &r=i; //错误:不能让普通引用指向常量。

PS: 《C++ primer》中2.4.3节中陈述道:“当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响。”这句话是错误的!!!对于以上两个例子,常量都是顶层const,但拷贝时不能忽略。

  • 可以让指向常量的指针指向非常量(只是不能通过指向常量的指针修改其指向的变量,但可以使用其他方式修改非常量);
double fraudster=38;
const double *cptr=&fraudster; //正确: 指针“认为”其指向的是常量,所以指针不通过自己修改其指向的量,但不妨碍指针指向的不是一个常量,这与其他定义和操作不冲突,认为是合法的
  • 可以让对常量的引用指向非常量
int i=38;
const int &r=i; //正确:可以用对常量的应用指向非常量。

  PS:这种应用常见与函数参数传递时,如果不想在函数中修改传入的参数,在形参中加上const。

  • 可以添加或去除指针的顶层const:
int i=38;
int *const r1=&i;  //正确: 添加指针的顶层const,定义常量指针
int* r2= r1; //正确:去除指针的顶层const,通过r2可以改变i

编程久了就养成习惯,不用再过脑逻辑推理了,以上五条总结为:在拷贝赋值初始化时,可以(为初始化的量)任意添加const限制,包括顶层或底层const。对于复合类型,如果基本类型前没有const,指针或引用的基本类型前可以加const,表示不通过指针或引用修改指向的对象;如果*后没有const,可以添加const,表示常量指针。但是,如果去除原const限制符,必须保证不能通过新创造的指针或变量修改属于常量的量,任何可能造成冲突的去除const限制符的操作是不被允许的。对于复合类型,基本类型前如果有const(底层const),指针或引用的基本类型前也必须有const;*后的const,即指针的顶层const是可以去除的。  

思考题:

int i=38;
int *const r1=&i;  
int **r2=&r1; //错误:r1是常量指针,r3必须是指向常量指针的指针, 应写为: int *const*r3=&r1;? (逻辑上应该这样写,但程序中还没见过这种应用和写法,暂且存疑) 

一句话总结:拷贝初始化或拷贝赋值时,添加const是任意的;在不造成冲突的前提下可以去除const。

 二、修饰类的成员函数

表示常量成员函数,不能通过该函数修改调用该成员函数的对象的内容。

class Book{
  void write();
  void read() const;
}

Book book1;
book1.write(); //正确,成员函数write()可以修改book的内容,相当于Book *const this = & book1, this默认是常量指针
book1.read(); //正确,book是非常量对象,可以调用常量成员函数,常量成员函数read()不能修改book的内容(trick:相当于const Book *const this = & book1,这与拷贝对象时能否添加或去除const限定符的思想是一致的,只需要把成员函数看成拷贝初始化的对象,添加const总是合法的,这里为this指针添加了底层const,即第一个const)

const Book book2; 
book2.read(); //正确: book2是常量对象,相当于const Book *const this = & book2
book2.write(); //错误:book2是常量对象,不能调用非常量成员函数,防止非常量成员函数试图修改常量对象产生冲突 (trick:这与拷贝对象时能否去除const限定符的思想是一致的,只需要把成员函数看成拷贝初始化的对象,相当于Book *const this = & book2 ,book2是const对象,拷贝初始化给this时不能去除const)

这里涉及到隐式形参this, this默认是常量指针,即默认是*const this,永远指向调用成员函数的对象的地址。即使用对象的地址初始化*const this指针,相当于 Book *const this = &book1。

但是默认情况下,this指向的对象是非常量的版本,即对象内容可以被修改。通过在成员函数的参数列表后加上const,将成员函数定义为常量成员函数,不能改变调用它的对象的内容;相当于将原本的常量指针this变成了指向常量的常量指针,即const Book *const this=&book1,在拷贝初始化时,为this指针添加const(即变为常量成员函数)总是合法的。

常量对象,以及常量对象的引用和指针只能调用常量成员函数。例如book2是常量对象,只能调用常量成员函数。如果是Book *const this=&book2 则是错误的,因为book2是const的,使用的指针也必须是指向常量的指针,即使用常量成员函数,等价于const Book *const this=&book2. 

Trick:判断能否/必须使用常量成员函数与拷贝对象时能否添加/去除const限定符的思想是一致的,只需要把成员函数看成拷贝初始化的对象(实质是其对应的隐式常量指针this被拷贝初始化),为this指针添加const(即变为常量成员函数)总是合法的;如果对象是const的,this指针也必须是指向常量的指针,即必须使用常量成员函数,不能去除const。

三、函数重载

重载函数要求函数名字相同,但形参列表不同。如果两个函数的形参相同,则犯了重复声明函数的错误。

如果同时定义了const版本的形参和非const版本的形参,如何判断是属于函数重载还是重复声明?

  • 因为函数实参传给形参相当于对形参进行拷贝初始化,所以,假设用const版本的实参初始化非const版本的形参,如果const可以去掉也不影响初始化的话,那么定义的另一个const版本形参的函数就属于重复声明;
  • 相反,如果用const版本的实参初始化非const版本的形参,const如果去掉就不能拷贝(造成冲突)的话,定义的另一个const版本版本形参的函数就属于函数重载。这种函数重载的情况下,如果传递const版本的实参给函数,只能调用const版本形参的函数;如果传递非const版本的实参给函数,虽然非const的实参也能拷贝给const版本的形参,但编译器会优先选择调用非常量版本形参的函数。
void test(int);
void test(const int);  //错误,可以用const int对象初始化int对象,即去除const也可以,属于重复声明;

void test(int*);
void test(int*const);   //错误:可以用int*const对象初始化int*对象,即去除const也可以,属于重复声明;

void test(int*);
void test(const int*);   //正确:1. 不能用const int*对象初始化int*对象,即不能去除const,属于函数重载;2. 如果传入的实参是指向常量对象的指针,则调用下面这个常量版本; 3. 如果传入的实参是指向非常量的指针,虽然指向非常量的指针也可以转化为指向常量的指针(在没有非常量版本时会用非常量版本的实参为const版本形参初始化,并调用const形参版本的函数),,但是在同时存在非const版本和const版本的函数重载时,编译器会优先选择上面的非常量版本的函数。

void test(int&);
void test(const int&);   //正确:1. 不能用const int*对象初始化int*对象,即不能去除const,属于函数重载;2. 如果传入的实参是对常量对象的引用,则调用下面这个常量版本;3. 如果传入的实参是对非常量的引用,虽然对非常量的引用也可以转化为对常量的引用(在未找到非const版本形参的函数时会将实参赋给常量const的版本形参并调用const版本的函数),但是编译器会优先选择上面的非常量版本的函数。

简而言之,形参中const可以去掉的属于重复声明;只有const不能去掉的才是函数重载。这与第一大节中判断拷贝时能否去掉const限定符的方法是一致的。 

类似地,利用const限定符修饰的成员函数也可以实现函数重载:

class Book{
  void read();
  void read() const;
}

Book book1;
book1.read(); //调用非常量成员函数版本
const Book book2;
book2.read(); //调用常量成员函数版本

调用成员函数相当于拷贝初始化*const this指针,如果是调用成员函数的对象时常量,只能选择常量成员函数匹配,因为相当于const Book*const this=&book2;如果调用成员函数的对象时非常量,会优先匹配普通成员函数,相当于Book *const this=&book1,实现函数重载 (只有在未找到普通成员函数的时候才转化为const Book *const this=&book1,调用常量成员函数的版本)。

 

总结:由于调用成员函数相当于拷贝初始化*const this指针、函数重载中实参传递参数给形参相当于拷贝初始化形参,所以这些应用中关于const限定符的使用都可以归结为拷贝初始化时能否去除或添加const限定符。

四、const_cast

const_cast只能用来改变对象底层const(换而言之,是针对指针和引用的)。

既可以添加const性质,也可以去除const性质。

4.1 函数重载

const_cast往往用于函数重载,比如已经有了非const的形参版本的函数,想要重载另一个非const形参版本的函数,只需要以const版本为壳,函数里面还是调用const形参版本的函数。

const int &test(const int &i){
    return i;
}


int &test(int &i){
    auto &r = test(const_cast<const int&>(i));
    return const_cast<int&> r;
}

4.2 去除const性质

由于在拷贝初始化时添加const总是合法的,将指向常量的指针绑定到非常量、或者将对常量的引用绑定到非常量都是允许的,通过去除指针或引用的const性质,就可以通过指针或引用修改非常量。

i=38;
const int *r=&i;
int *r_tmp=const_cast<int *>(r); //去掉const性质,编译器不再阻止通过r_tmp指针对i的修改。由于i是变量,所以通过r_tmp指针修改i不会出问题。

在去除const性质时需要注意:原本编译器禁止将普通指针绑定到常量上、或者将普通引用指向常量,以防止通过普通指针或者引用修改所绑定的常量,造成为定义的后果。实质在将普通指针绑定到常量上、或者将普通引用指向常量这一步并未造成冲突操作,实质直到试图通过普通指针或者引用修改所绑定的常量那一步才会发生冲突。正如之前讨论到的,C++编译器不会等到冲突发生是再判断造成冲突的操作违法,而是直接在源头禁止可能造成冲突的操作。使用const_cast去除const仿佛是在对编译器说:“放心!当我把普通指针或者引用绑定到常量上时我清楚地知道自己在干什么(我有一些特殊的操作或目的),我不会通过普通指针或者引用修改所绑定的常量的,我艺高人胆大,所以不用禁止我可能造成将来违法的操作,我现在就只要这样做(现在实质上并未有违法),如果将来违法我后果自负!”

const j=38;
const int *r2=&j;
int *r2_tmp=const_cast<int *>(r2); //去掉const性质,编译器不再阻止通过r2_tmp指针对j的修改。但由于j是常量,通过r2_tmp修改j会造成未定义的后果。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

yuyuelongfly

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

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

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

打赏作者

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

抵扣说明:

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

余额充值