c++开发中,常量属性是避免不了要接触的。如果运用不好,函数或变量的常量属性会给你造成麻烦。其中,把const和constexpr这两个关键字弄混是一大原因。(当然还有其他原因引起困惑。。)本文我们试图解决以下2个问题:
《c++ primer》中有对这个问题的详细介绍,但我一开始没怎么注意他嘛!那么我是怎么注意到这个问题的呢?实际开发中,经常会使用stl中的array容器来代替c风格静态数组:
int size=2;
array arr;//第一次使用array容易或许你容易“天真”地写出这样的代码
然后你就会发现编译器报出这样的错误[GNU]:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ const_test.cc -o const_test && ./const_test
const_test.cc: In function ‘int main()’:
const_test.cc:15:15: error: the value of ‘size’ is not usable in a constant expression
如果看了上述代码你不知道咋回事,处于懵逼的状态,那么下面的代码更简单意识到问题:
int size=1;
const int c1=size;// OK
constexpr int c2=size;// the value of ‘size’ is not usable in a constant expression!
这么看是不是更容易搞明白二者的区别?
//注意,constexpr是修饰返回值的,而不是修饰函数的,写后面肯定不行啊!
//C++14!!!
constexpr int constFunc(){
int a=2;
return a;//尽管自动变量a不是立即数或常量表达式,但实际上函数的返回值还是会被认为是常量表达式
}
int main(){
constexpr int c3=2;
constexpr int c4=c3;
constexpr int c5=constFunc();
}
上面的代码[GUN,c++14]可以看的很清楚了,constexpr除了必须接受一个编译期确定的量之外,有一个功能就是:创造出一个“编译期确定值”的语义。用constexpr修饰的函数或者变量都将被看作其值是编译器确定的!
但这里还有一个关于语言标准的小细节需要注意:在c++11中,constexpr修饰的函数体必须只有1行;而在c++14中则取消了这个限制,可以有任意行:
//c++11
constexpr int constFunc(){
return 2;
}
//c++14
constexpr int constFunc(){
int a=2;
return a;
}
如果通读过《c++ primer》的话,会看到里面建议你:当你设计类时,应该尽可能地给一个不会修改成员变量的函数加上常函数修饰。
这个建议其实很对,但是我一开始并没有理解其中的必要性,因为我那时只考虑到常函数的性质之一:常函数里面只能调用常函数(成员函数)。所以我就觉得,即便不把它定义成常函数似乎也不会有什么影响啊,而且还对里面能调用的函数加以限制,这用起来岂不很麻烦:
class A{
public:
int data()const{return data_;}
int getData_1()const{
handleData();//常函数中不能调用非常成员函数,编译报错!
return data_;
}
int getData_2()const{//OK
doWork();
return data_;
}
void handleData(){}//do something without data_...
void doWork()const{}//do something without data_...
private:
int data_;
};
const修饰的函数,编译器将替你检查你是否有可能对成员变量做出修改,哪怕没有做出修改,但”有可能“修改,那也无法通过编译!比如上面的代码中,尽管handleData()函数中实际上并没有对data_做出修改,但只是因为没有const修饰,将会被认为是可能对成员变量做出修改的,无法通过编译!
基于此,可能会有初学者不是很喜欢为成员函数加上const,因为上述原因在代码量大的时候往往无法很快发现问题。。一度我也是这么想的,直到我拜读了侯捷老师的视频!侯捷老师向我们说明了一种情况,在这种情况下,就不得不尽可能地使用const了!
class MayConstObj{//可能被实例化为常量来使用的类
public:
void handleData(){}//do something without data_...
void doWork()const{}//do something without data_...
private:
int data_;
};
int main(){
MayConstObj c1;
c1.handleData();//ok
c1.doWork();//ok
const MayConstObj c2=c1;
c2.handleData();//导致编译出错!
c2.doWork();//ok
}
上述代码使用GNU,c++11测试过,报错信息为:
g++ -std=c++11 const_test.cc -o const_test && ./const_test
const_test.cc: In function ‘int main()’:
const_test.cc:21:19: error: passing ‘const MayConstObj’ as ‘this’ argument discards qualifiers [-fpermissive]
如果你对c++语言没有一个充分了解的话,相信看到这一串报错信息一定感到束手无策,然后就上网赶紧查查报错信息。。(曾经的我),但相信你用过c++一段时间后,可以轻易发现上面的语句为什么会导致这样的报错信息!
为了搞懂上面的报错信息,来举一个更直观的例子:
void func1(const int*){}
void func2(int*){}
int main(){
const int* a;
func1(a);//ok
func2(a);//导致编译出错
}
上述代码运行结果如下:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ -std=c++11 const_test.cc -o const_test && ./const_test
const_test.cc: In function ‘int main()’:
const_test.cc:29:11: error: invalid conversion from ‘const int*’ to ‘int*’ [-fpermissive]
这样看,原因就很显然了:常量指针(指向常量的指针)无法隐式转换为正常指针(所以才需要const_cast嘛!但要注意的是,常量却可以隐式转换成非常量、指针常量也可以隐式转换成正常指针)。
而非静态类成员函数的第一个参数[GNU实现]是一个this指针,非静态非常量类成员函数的第一个参数是一个const T*(常量指针)!也就是说,常量对象c2的handleData()类成员函数,编译期为其生成的函数原型应该是:
MayConstObj::handleData(MayConstObj*)
而doWork()类成员函数,编译期为其生成的函数原型是:
MayConstObj::handleData(const MayConstObj*)
所以,当类的使用者以常量形式调用非常类成员函数时,常量形式对象的this指针也是一个常量指针,就意味着传入成员函数的this指针都是const T*,相当于:
const MayConstObj c2=c1;
//c2.handleData();//这1句相当于下面2句
const MayConstObj* this=&c2;
MayConstObj::handleData(this);
如此,就产生了上面的报错信息。明白了上述道理后,修改这个问题并不需要上网查查,先查看一下常量对象调用的成员函数是不是常函数就行了!
最后,总结一下常函数的作用和建议:
最后,我们加点料!在c++中,我们有时需要确切的知道一个函数的原型,这个小技巧在这时会很实用,大概有2种方法,下面的命令均在GNU下测试过:
输出目标文件符号表+解析符号:
比如上面的类成员函数MayConstObj::handleData()
:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ -std=c++11 const_test.cc -o const_test
# 目录中产生了const_test二进制目标文件
# 接下来读取elf文件,筛选符号关键字:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ readelf const_test -a |grep MayConstObj
62: 0000000000001288 15 FUNC WEAK DEFAULT 16 _ZN11MayConstObj10handleD
66: 0000000000001298 15 FUNC WEAK DEFAULT 16 _ZNK11MayConstObj6doWorkE
# 或者可以用nm命令直接获取符号表然后筛选:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ nm const_test |grep MayConstObj
0000000000001288 W _ZN11MayConstObj10handleDataEv
0000000000001298 W _ZNK11MayConstObj6doWorkEv
# 拿到符号表种的符号后,你会发现GNU产生的符号你是看不懂的(《mordern c++》一书提到过VC的比较好看懂),需要用c++filt命令解析:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ c++filt _ZN11MayConstObj10handleDataEv
MayConstObj::handleData()
你会发现,这个类成员函数的解析,缺少了默认的this指针参数啊(这个函数也确实不是静态啊!)。。
确实,这就需要用到第二种方法了。
利用gdb的栈轨迹来帮我们查看函数原型,可以解析出类成员函数的this指针实现:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ -g std=c++11 const_test.cc -o const_test# 加上-g,生成调试信息
zkcc@LAPTOP-OHBI7I8S:~/mytest$ gdb const_test
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
# 省略了一些gdb信息输出
Reading symbols from const_test...
(gdb) b const_test.cc:11 # 在你想查看原型的函数处打上断点
Breakpoint 1 at 0x1298: file const_test.cc, line 11.
(gdb) r # run!
Starting program: /home/zkcc/mytest/const_test
Breakpoint 1, MayConstObj::doWork (this=0x7fffffffdd54) at const_test.cc:11
warning: Source file is more recent than executable.
11 int a=0;
(gdb) bt # 输出当前栈内轨迹
# 下面2行是当前栈里的内容,这里会显示出向dowork调用传递的所有参数,这里也可以验证GNU对类内函数this指针的传递是放在第一个参数的
#0 MayConstObj::doWork (this=0x7fffffffdd54) at const_test.cc:11
#1 0x00005555555551da in main () at const_test.cc:20
(gdb)