前言:
c++关键字const,从字面意思上来说期望被const修饰的变量、对象、引用、类成员函数等拥有某种“保持不变”的特征、然而这些特征很多可以被破坏掉,甚至c++自身就提供破坏这些特征的手段。可以说const更类似于一种君子协定,比如函数设计者希望用户认为可以放心地使用函数,此时const X &obj保证不会让用户感知到obj前后变化;类设计者设计const成员函数告诉编译器可以让const对象安全地调用函数;编译器可能会根据const提示进行优化如常量替换(当然优化级别够高不const提示也可能会)。
一、c++ primer原话:如果对象是一个常量,使用const_cast执行写操作就会产生未定义的结果。例1:
#include<cstdio>
#include<iostream>
int main(){
const int i = 10,*pi = &i;
int j,k;
int &ri = const_cast<int &>(i);
ri = 20;
j = i;
k = ri;
std::cout<<i<<std::endl;
std::cout<<j<<std::endl;
std::cout<<*pi<<std::endl;
std::cout<<k<<std::endl;
std::cout<<ri<<std::endl;
return 0;
}
直接g++ test.cpp不加优化。
输出
10
10
20
20
20
看输出后的汇编代码(g++ -S)和实际输出结果可以得知:
1.有i的地方都被替换成了立即数10;
2.i的对应栈数值的确修改了。
结论:感觉编译器傲娇的说“你告诉人家是个const,那人家当然认为你不会变了”的感觉。这种感觉在类函数没有被声明virtual,但是子类却也实现同名函数时,父类指针或引用不会表现多态性也有。不是virtual,说明父类本来就不打算让子类重新实现。
例2:
#include<cstdio>
#include<iostream>
namespace{
const int gi = 100;
}
int main(){
int &ri = const_cast<int &>(gi);
ri = 200;
std::cout<<gi<<std::endl;
std::cout<<ri<<std::endl;
return 0;
}
结果是sigseg,段错误了。gdb看core发生在ri=200那一行。
还是看汇编代码发现ri被放到了rodata段,作为只读数据,ro--readonly。
如果觉得看汇编麻烦,也可以nm a.out |grep -i 'gi'来看,会得到‘080487e0 r _ZN12_GLOBAL__N_1L2giE’,可以看到name mangling后的gi是个只读的(r)。
结论:感觉编译器更傲娇了“认为你不会变心,就告诉操作系统哥哥把你的段属性设成了不能改变了”。
二、const对象只是宣称是const,真想变const编译器不仅不管,还会帮你
1.编译器明确的辅助手段
const_cast,上面例子已经体现。
mutable成员,例子:
#include<cstdio>
#include<iostream>
class MyInt{
public:
int j = 1000;
const int k = 1000;
};
void changeConst(const MyInt &temp,int count){
if(count == 0){
return;
}else if(count == -1){
temp.i = 50;
MyInt &temp2 = const_cast<MyInt &>(temp);
temp2.j = 50;
}
changeConst(temp,count - 1); //弄成递归时优化-O时不会跟进递归函数,-O3时会
}
int main(){
const MyInt cmi;
changeConst(cmi,20);
if(100 < cmi.i) //改成cmi.j效果一样,cmi.k也是
std::cout<<"yes"<<std::endl;
return 0;
}
这里有几个现象:
cmi尽管是const的,尽管cmi.k看上去也是const的(),但是100<cmi.k没有常量替换;
-O优化时,汇编代码反应都会进入changeConst递归五十次之后,按照i或者j实际的值输出“yes”;
-O3优化,汇编代码反应都不会进入changConst,并且也不会进入100 < cmi.i的条件语句,直接输出“yes”。
再翻书,得知c++11新支持的类成员初始化格式const int k = 1000其实是个默认值,为各种构造函数提供默认值,等效于初始化列k(1000),见例子:
#include<cstdio>
#include<iostream>
class MyInt{
public:
MyInt():i(2000){
std::cout<<i<<" "<<j<<" "<<k<<std::endl;
}
mutable int i = 1000;
int j = 1000;
const int k = 1000;
};
int main(){
MyInt mi;
}
2000 1000 1000,由此可见。
没有显式生成默认构造函数(就是不带参数这种),那么编译器会自动合成默认函数(不一定肯定会生成,例外见c++ primer)。按照c++ primer的说法,需要把构造函数显示声明为constexpr的,并且提供一个constexpr的k的getter函数,这样就能在编译时期确定k的值。
对于const和constexpr,按照stackoverflow上vote比较多的说法,const是对运行时的口头保证不变,constexpr是编译时期能确定的一种值用于编译时期使用。const是针对一块内存而言,大约指这块内存的值不会有变化;constexpr体现在代码,是否用立即数替换其他寻址。如下:
#include<cstdio>
#include<iostream>
class MyInt{
public:
constexpr MyInt():i(2000),k(2000){
}
inline constexpr int getK() const;
mutable int i = 1000;
int j = 1000;
const int k = 1000;
};
constexpr int MyInt::getK() const{
return k;
}
int main(){
const MyInt cmi;
if(100 < cmi.getK())
std::cout<<"yes"<<std::endl;
return 0;
}
但是结果,cmi.getK()仍然被调用,常量表达式没有在编译时计算得出。甚至都没有被inline...
综上,const类成员k没有被常量替换正常,因为它是在构造函数内初始化的。但是按标准的constexpr来也不能编译时确定就见下回分解了。
第二点是int j和mutable int i两个成员,现象看来不同级别的编译器优化对二者处理相同。本想const MyInt的非mutable成员会被认为不变而被特别优化,起码看来包含4.8.2版本gcc的g++是这样。
2.猜const对象位置
#include<cstdio>
#include<iostream>
int main(){
const int i = 100;
int &ri = const_cast<int &>(i);
int j = 200,*pj = &j;
ri = 101;
*(pj - 1) = 700;
std::cout<<i<<std::endl;
std::cout<<ri<<std::endl;
return 0;
}
结果输出100,700,100是常量替换,700是根据另一个int j的位置猜测i在栈中位置改变的。栈内的能猜,堆中的连续分配的诸如类、结构体这种夹杂const和非const的估计也能改。像之前的例子一样,放在常量区的const不用担心,比如:
main.cpp:
#include<cstdio>
#include<iostream>
#include"test.h"
#include"test2.h"
int main(){
innocentFun();
std::cout<<gi<<std::endl;
return 0;
}
test.h
#ifndef TEST_H
#define TEST_H
extern int gi;
#endif
test2.h
#ifndef TEST2_H
#define TEST2_H
#include "test.h"
void innocentFun();
#endif
test.cpp
#include"test.h"
const int gi = 50;
test2.cpp
#include "test2.h"
void innocentFun(){
int &i = const_cast<int &>(gi);
i = 5000;
}
g++ test.cpp test2.cpp main.cpp,执行会发生段错误。这样test2的开发者就没法用自己的纯洁无害的函数innocentFun恶意修改test的全局const变量了。
综上,const的破坏手段可以有编辑体提供的,以及也可以自己猜,因为const本身编译器只会对文本中的显示对const赋值进行干涉(mutable不管)。但是操作系统层次的段属性(看了文章说linux没有真正的段,但模拟的也是段)是没法变的,这种const从c++这个层次改不了。
三、成员函数的const
从某论坛c++版块的一个问题开始讲起。
#########################
能否判定对象是否是const?
void Object::function() {
do something
....
if (object is const) {
xxx
else {
xxx
}
}
Obj obj;
const Obj& const_obj = obj;
obj.function();
const_obj.function();
如何能实现那个if语句,使得两次调用function函数的时候走不同的分支?
本来可以实现 void function() 和void function()const;两个不同的函数,问题就解决了。
但是do_something那段代码特别长,copy两份代码不便于维护。
##########################
有一靠谱兄弟回答:
##########################
void object::function_impl(bool isconst);
void object::function()
{function(false);}
void object::function()const
{function(true);}
##########################
总结下:
1.const Obj obj要进入function,function必须是个const的。const成员函数可以看成是void Obj::function(const Obj * const this),非const的是个void Obj::function(Obj * const this),根据c++ primer中函数重载实参类型转化规则,const Obj obj的this指针const Obj * const this是不能够转化成后者的(底层const不能转化)。所以楼主的function必须是个const的,如果需要const Obj和Obj都能调用,当然用const和非const两个函数重载也可以;
2.如果只用一个const的function,c++ primer七百多页的内容也没讲从const成员函数内部能不能区分出当前对象的const属性。下面有一兄弟回了:
###########################
当然如果你一定要动态判断的话,还有std::is_const
我没有用过,我猜可以这么写:
if ( std::is_const<decltype(*this)>::value )
###########################
is_const看标准是类模板重载得来,下面给出一个类似的函数模板重载版本:
#include<cstdio>
#include<iostream>
class MyInt{
public:
int i = 1;
};
template<typename T>
bool is_const(T &obj){return false;}
template<typename T>
bool is_const(const T &obj){return true;}
int main(){
MyInt cmi;
if(is_const(cmi))
std::cout<<"ture"<<std::endl;
else
std::cout<<"false"<<std::endl;
return 0;
}
还有些可能的细节没关注,比如obj如果是右值或顶层指针啥的,但是这个简易版本能分出基本的const。
不过问题来了,如果是在const的function里面那么*this一定是const的...,因为已经经过实参向形参转化了。
#include<cstdio>
#include<iostream>
class MyInt{
public:
int i = 1;
void function() const;
};
template<typename T>
bool is_const(T &obj){return false;}
template<typename T>
bool is_const(const T &obj){return true;}
void MyInt::function() const{
if(is_const(*this))
std::cout<<"true"<<std::endl;
else
std::cout<<"false"<<std::endl;
}
int main(){
MyInt cmi;
cmi.function();
return 0;
}
再怎么都是true。
3.按照靠谱兄弟的回答。
void object::function_impl(bool isconst);
void object::function()
{function(false);}
void object::function()const
{function(true);}
能够根据const属性重载进不同的函数,const Obj 进function()const,Obj 进function(),但是调用实现函数function_impl也有坑。const 的function能调的只有const的function_impl,那么只能function_impl定义成const,在判断isconst的if语句中如果不是const,把this给const_cast成非const的。如:
#include<cstdio>
#include<iostream>
class MyInt{
public:
int i = 1;
void notconst() const{MyInt *pmi = const_cast<MyInt *>(this);}; //尽管notconst要干很多不是const该干的事,但是为了能被const的function调用只能定义成const。
void function() const{notconst();};
};
int main(){
MyInt cmi;
cmi.function();
return 0;
}
实际上,c++ primer建议用const_cast的地方有点类似:
#include<cstdio>
#include<iostream>
class MyInt{
public:
int i = 1;
};
const MyInt &smallerMyInt(const MyInt &lhs,const MyInt &rhs){
return (lhs.i < rhs.i)?lhs:rhs;
}
MyInt &smallerMyInt(MyInt &lhs,MyInt &rhs){
const MyInt &rmi = smallerMyInt(const_cast<const MyInt &>(lhs),const_cast<const MyInt &>(rhs)); //必须const_cast回const不然重载不到const版本,就一直循环重载了。
return const_cast<MyInt &>(rmi);
}
int main(){
MyInt mi,mi2;
mi2.i = 20;
smallerMyInt(mi,mi2).i = 500; //为了返回左值能够赋值
std::cout<<mi.i<<std::endl;
return 0;
}
总结,const成员函数提示符是我个人认为c++面向对象编程最为强大的辅助工具,像前言说的一样,能给用户像接口文档一样给予提示,能够自己按照const与否重载方法,能让编译器对自己的不经意行为做出干涉。和前面两节的讨论const实现的某些较底层细节比起来,作为马农可以不用关心const到底优化了多少性能,但是基于工程目的应该做到能const则const。当然,这不是我这个菜鸟的乱侃,这是我看了七八篇stackoverflow之后,众老哥一致同意的结果。