const与constexpr_zkccpro的博客-程序员宅基地_const和constexpr

技术标签: c++  C++学习笔记  

const与constexpr

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

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

一、const与constexpr的区别

《c++ primer》中有对这个问题的详细介绍,但我一开始没怎么注意他嘛!那么我是怎么注意到这个问题的呢?实际开发中,经常会使用stl中的array容器来代替c风格静态数组:

int size=2;
array<int,size> arr;//第一次使用array容易或许你容易“天真”地写出这样的代码

然后你就会发现编译器报出这样的错误[GNU]:

[email protected]:~/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);//导致编译出错
}

上述代码运行结果如下:

[email protected]:~/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()

    [email protected]:~/mytest$ g++ -std=c++11 const_test.cc -o const_test
    # 目录中产生了const_test二进制目标文件
    
    # 接下来读取elf文件,筛选符号关键字:
    [email protected]:~/mytest$ readelf const_test -a |grep MayConstObj
        62: 0000000000001288    15 FUNC    WEAK   DEFAULT   16 _ZN11MayConstObj10handleD
        66: 0000000000001298    15 FUNC    WEAK   DEFAULT   16 _ZNK11MayConstObj6doWorkE
    # 或者可以用nm命令直接获取符号表然后筛选:
    [email protected]:~/mytest$ nm const_test |grep MayConstObj
    0000000000001288 W _ZN11MayConstObj10handleDataEv
    0000000000001298 W _ZNK11MayConstObj6doWorkEv
    
    # 拿到符号表种的符号后,你会发现GNU产生的符号你是看不懂的(《mordern c++》一书提到过VC的比较好看懂),需要用c++filt命令解析:
    [email protected]:~/mytest$ c++filt _ZN11MayConstObj10handleDataEv
    MayConstObj::handleData()
    
    

    你会发现,这个类成员函数的解析,缺少了默认的this指针参数啊(这个函数也确实不是静态啊!)。。

    确实,这就需要用到第二种方法了。

  • 利用gdb的栈轨迹来帮我们查看函数原型,可以解析出类成员函数的this指针实现:

    [email protected]:~/mytest$ g++ -g std=c++11 const_test.cc -o const_test# 加上-g,生成调试信息
    [email protected]:~/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) 
    
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42923076/article/details/124595073

智能推荐

Android王者荣耀模拟金牌,荣耀战区修改方法 轻松获得省级金牌银牌_weixin_39917291的博客-程序员宅基地

今天再给大家讲解一下王者荣耀修改战区的方法,相信很多小伙伴还是比较看重农药战区排名的,如果你通过修改荣耀战区的位置,定位到一些比较偏远的地区,拿下一个初级甚至是中级荣耀称号或者高级荣耀称号还是很简单的,可以让你轻松在好友面前秀一秀!比如定位到偏远的地区的话,你玩冷门英雄,几百一千战力可能是铜牌,二千战力可能就是银牌,玩到三千分左右就能拿到省级金牌,然后可以设置让它显示在自己的主页面!战区推荐战区需...

C++ 关于书上说的“编译的时候分配内存”_snakorse的博客-程序员宅基地_c++中函数在编译时就分配内存吗

1、所谓在编译期间分配空间指的是静态分配空间(相对于用new动态申请空间),如全局变量或静态变量(包括一些复杂类型的常量),它们所需要的空间大小可以 明确计算出来,并且不会再改变,因此它们可以直接存放在可执行文件的特定的节里(而且包含初始化的值),程序运行时也是直接将这个节加载到特定的段中,不 必在程序运行期间用额外的代码来产生这些变量。 其实在运行期间再看“变量”这个概念就不再具备编译期间那

FileManager文件管理器(总结)_北极熊的微笑的博客-程序员宅基地

/** * File文件管理器 */public class FileManager { private static final String TAG = "FileManager"; private static List&lt;File&gt; fileList; private static int index = -1; /** *...

Apache Doris Binlog Load使用方法及示例_hf200012的博客-程序员宅基地

Binlog Load提供了一种使Doris增量同步用户在Mysql数据库的对数据更新操作的CDC(Change Data Capture)功能,使用户更方面的完成Mysql数据的导入注意:该功能需要在0.15及以后的版本里使用1. 安装配置 Mysql安装Mysql 快速使用Docker安装配置Mysql,具体参照下面的连接https://segmentfault.com/a/1190000021523570 如果是在物理机上安装可以参考下面的连接:在 CentOS 7 中安装 MySQ

java数组定义错误_JAVA定义数组 int a[]=new int[100000] 错误_面壁者小湛的博客-程序员宅基地

我用JAVA定义了一个1W的数组可以使用,但是定义一个10W的数组提示Exceptioninthread"main"java.lang.ArrayIndexOutOfBoundsException:-2147479015atJavaapplication1.JavaApplication...我用JAVA定义了一个1W的数组可以使用,但是定义一个10W的数组提示Exception in threa...

随便推点

最近搞Hibernate遇到的问题_诸葛明亮的博客-程序员宅基地

在Eclipse利测试Hibernate时总是遇到了莫名其妙的错误,只用了Hibernate,没用Spring,对于SSH可能不适用,这里记录下来,以便于下次碰到能方便的解决。1. 用了Hibernate注解,也添加了映射,却总是报unknowentity:com.xxxx.xxxx 解决办法:在Hibernate.cfg.xml里增加配置: ,配置实体类      C

怎么理解new Promise(function (resolve, reject){})中函数的参数resolve和reject_梓沂的博客-程序员宅基地_new promise resolve

先总结:参数resolve和reject的作用是将Promise中函数要传递的值,作为参数传给后面的then和catch中函数。resolve(值1)把值1传给promise,然后再由promise把值1传给then(function(值1));reject(值2)把值2给promise,然后再由promise把值2传给catch(function(值2))。理解的过程:开始学习廖雪峰...

SpringMVC接收ajax提交的application/x-www-form-urlencoded;以及application/json;@RequestBody和@RequestParam注解_有点不想努力了的博客-程序员宅基地

想要了解怎么使用Ajax提交的application/x-www-form-urlencoded;charset=UTF-8;以及application/json;charset=UTF-8;charset=UTF-8;首先需要了解@RequestParam注解和@RequestBody注解@RequestParam注解:意思是获取表单提交的数据,如//伪代码public void...

华为交换机配置telnet登陆_超凡脫俗的博客-程序员宅基地_华为交换机telnet配置

网络拓扑通过Console口首次登录设备后,对设备进行基本配置并配置通过Telnet远程登录的0~4号用户的级别为15级,认证方式为AAA认证,通过Console口登录设备,对设备进行基本配置。设置设备名称和管理IP地址。u t m system-view[HUAWEI] sysname Server[Server] vlan 10[Server-vlan10] quit[Server] interface vlanif 10[Server-Vlanif10] ip address 19

人机大战之AlphaGo的硬件配置和算法研究_葡萄城技术团队的博客-程序员宅基地_人机大战中机器人使用什么算法

AlphaGo的硬件配置最近AlphaGo与李世石的比赛如火如荼,关于第四盘李世石神之一手不在我们的讨论范围之内。我们重点讨论下AlphaGo的硬件配置: AlphaGo有多个版本,其中最强的是分布式版本的AlphaGo。根据DeepMind员工发表在2016年1月Nature期刊的论文,分布式版本(AlphaGo Distributed)使用了1202个CPU和176个GPU,同时可以有40个搜

8G优盘为何不能容纳6G文件?_pukede的博客-程序员宅基地_6g的iso文件为什么刻录不到8g优盘里

优盘默认存储文件的容量:1、U盘如果是Fat系统,单个文件最大为2G  2、如果是fat32系统, 单个文件最大为4G3、如果是ntfs系统, 单个文件的大小可为6G及大于6G的文件<span class="t_tag" onclick="function onclick(){tagshow(event)}">解决方法为:    打开我的<span class="t_tag" oncl

推荐文章

热门文章

相关标签