const与constexpr

const与constexpr

c++开发中,常量属性是避免不了要接触的。如果运用不好,函数或变量的常量属性会给你造成麻烦。其中,把const和constexpr这两个关键字弄混是一大原因。(当然还有其他原因引起困惑。。)本文我们试图解决以下2个问题:

  • const与constexpr的区别?
  • 常函数的使用建议?

一、const与constexpr的区别

《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!

这么看是不是更容易搞明白二者的区别?

  1. constexpr修饰的变量同样具有**“常量”属性**,与const一样,不可修改。
  2. constexpr修饰的变量,只接受编译期就已经确定好的值或表达式,一般可以是立即数或者带有constexpr修饰的变量或函数,即,常量表达式
//注意,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);

如此,就产生了上面的报错信息。明白了上述道理后,修改这个问题并不需要上网查查,先查看一下常量对象调用的成员函数是不是常函数就行了!

最后,总结一下常函数的作用和建议:

  1. 常函数只能调用常成员函数,并且不能修改成员变量,否则将无法通过编译。
  2. 常量对象只能调用常成员函数,调用非常成员函数将导致指针类型转换拒绝。
  3. 基于以上两点,设计一个类时,尽量将不会改变成员变量的函数设置为常函数,除非你咬定你的类将来绝不可能以常量实例化,或者保证常量实例化绝不可能调用该成员函数。

最后,我们加点料!在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) 
    

你可能感兴趣的:(C++学习笔记,c++)