【C++】const与constexpr详解
1. constexpr:常量表达式
所谓常量表达式,指的就是由多个(≥1)常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。
实际开发中,我们经常会用到常量表达式。以定义数组为例,数组的长度就必须是一个常量表达式:
// 1)
int url[10];//正确
// 2)
int url[6 + 4];//正确
// 3)
int length = 6;
int url[length];//错误,length是变量
上述代码演示了 3 种定义 url 数组的方式,其中第 1、2 种定义 url 数组时,长度分别为 10 和 6+4,显然它们都是常量表达式,可以用于表示数组的长度;第 3 种 url 数组的长度为 length,它是变量而非常量,因此不是一个常量表达式,无法用于表示数组的长度。
常量表达式的应用场景还有很多,比如匿名枚举、switch-case 结构中的 case 表达式等,感兴趣的读者可自行编码测试,这里不再过多举例。
我们知道,C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
对于用 C++ 编写的程序,性能往往是永恒的追求。那么在实际开发中,如何才能判定一个表达式是否为常量表达式,进而获得在编译阶段即可执行的“特权”呢?除了人为判定外,C++11 标准还提供有 constexpr
关键字。
constexpr
关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr
可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
注意:获得在编译阶段计算出结果的能力,并不代表 constexpr修饰的表达式一定会在程序编译阶段被执行,具体的计算时机还是编译器说了算。
1.1 constexpr
修饰普通变量
C++11 标准中,定义变量时可以用constexpr
修饰,从而使该变量获得在编译阶段即可计算出结果的能力。
注意:使用 constexpr
修改普通变量时,变量必须经过初始化且初始值必须是一个常量表达式。举个例子:
#include <iostream>
using namespace std;int main()
{constexpr int num = 1 + 2 + 3;int url[num] = {1,2,3,4,5,6};couts<< url[1] << endl;return 0;
}
程序执行结果为:
2
读者可尝试将 constexpr
删除,此时编译器会提示“url[num] 定义中 num 不可用作常量”。
可以看到,程序第 6 行使用 constexpr
修饰 num 变量,同时将 “1+2+3” 这个常量表达式赋值给 num。由此,编译器就可以在编译时期对 num 这个表达式进行计算,因为 num 可以作为定义数组时的长度。
有读者可能发现,将此示例程序中的 constexpr
用const
关键字替换也可以正常执行,这是因为 num 的定义同时满足“num
是 const
常量且使用常量表达式为其初始化”这 2 个条件,由此编译器会认定 num
是一个常量表达式。
另外需要重点提出的是,当常量表达式中包含浮点数时,考虑到程序编译和运行所在的系统环境可能不同,常量表达式在编译阶段和运行阶段计算出的结果精度很可能会受到影响,因此 C++11 标准规定,浮点常量表达式在编译阶段计算的精度要至少等于(或者高于)运行阶段计算出的精度。
1.2 constexpr修饰函数
constexpr
还可以用于修饰函数的返回值,这样的函数又称为“常量表达式函数”。
注意,constexpr
并非可以修改任意函数的返回值。换句话说,一个函数要想成为常量表达式函数,必须满足如下 4 个条件:
1) 该函数必须有返回值,即函数的返回值类型不能是 void。
举个例子:
constexpr void display() {//函数体
}
像上面这样定义的返回值类型为 void 的函数,不属于常量表达式函数。原因很简单,因为通过类似的函数根本无法获得一个常量。
2) 整个函数的函数体中,除了可以包含 using
指令、typedef
语句以及 static_assert
断言外,只能包含一条 return 返回语句。
举个例子:
constexpr int display(int x) {int ret = 1 + 2 + x;return ret;
}
注意,这个函数是无法通过编译的,因为该函数的返回值用 constexpr 修饰,但函数内部包含多条语句。
如下是正确的定义 display() 常量表达式函数的写法:
constexpr int display(int x) {//可以添加 using 执行、typedef 语句以及 static_assert 断言return 1 + 2 + x;
}
可以看到,display()
函数的返回值是用 constexpr
修饰的 int
类型值,且该函数的函数体中只包含一个return
语句。
3)return 返回的表达式必须是常量表达式。
举个例子:
#include <iostream>
using namespace std;int num = 3;
constexpr int display(int x){return num + x;
}
int main()
{//调用常量表达式函数int a[display(3)] = { 1,2,3,4 };return 0;
}
该程序无法通过编译,编译器报“display(3)
的结果不是常量”的异常。
常量表达式函数的返回值必须是常量表达式的原因很简单,如果想在程序编译阶段获得某个函数返回的常量,则该函数的 return 语句中就不能包含程序运行阶段才能确定值的变量。
注意,在常量表达式函数的 return
语句中,不能包含赋值的操作(例如 return x=1
在常量表达式函数中不允许的)。另外,用 constexpr
修改函数时,函数本身也是支持递归的,感兴趣的读者可自行尝试编码测试。
4) 函数在使用之前,必须有对应的定义语句。我们知道,函数的使用分为“声明”和“定义”两部分,普通的函数调用只需要提前写好该函数的声明部分即可(函数的定义部分可以放在调用位置之后甚至其它文件中),但常量表达式函数在使用前,必须要有该函数的定义。
举个例子:
#include <iostream>
using namespace std;//普通函数的声明
int noconst_dis(int x);
//常量表达式函数的声明
constexpr int display(int x);//常量表达式函数的定义
constexpr int display(int x){return 1 + 2 + x;
}
int main()
{//调用常量表达式函数int a[display(3)] = { 1,2,3,4 };cout << a[2] << endl;//调用普通函数cout << noconst_dis(3) << endl;return 0;
}
//普通函数的定义
int noconst_dis(int x) {return 1 + 2 + x;
}
程序执行结果为:
3
6
读者可自行将 display()
常量表达式函数的定义调整到 main()
函数之后,查看编译器的报错信息。可以看到,普通函数在调用时,只需要保证调用位置之前有相应的声明即可;而常量表达式函数则不同,调用位置之前必须要有该函数的定义,否则会导致程序编译失败。
1.3 constexpr修饰类的构造函数
对于 C++ 内置类型的数据,可以直接用 constexpr
修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr
修饰是不行的。
举个例子:
#include <iostream>
using namespace std;//自定义类型的定义
constexpr struct myType {const char* name;int age;//其它结构体成员
};int main()
{constexpr struct myType mt{ "zhangsan", 10 };cout << mt.name << " " << mt.age << endl;return 0;
}
此程序是无法通过编译的,编译器会抛出“constexpr不能修饰自定义类型”的异常。
当我们想自定义一个可产生常量的类型时,正确的做法是在该类型的内部添加一个常量构造函数。例如,修改上面的错误示例如下:
#include <iostream>
using namespace std;
//自定义类型的定义
struct myType {constexpr myType(char *name,int age):name(name),age(age){};const char* name;int age;//其它结构体成员
};
int main()
{constexpr struct myType mt { "zhangsan", 10 };cout << mt.name << " " << mt.age << endl;return 0;
}
程序执行结果为:
zhangsan 10
可以看到,在 myType
结构体中自定义有一个构造函数,借助此函数,用 constexpr
修饰的 myType
类型的mt
常量即可通过编译。
注意,constexpr
修饰类的构造函数时,要求该构造函数的函数体必须为空,且采用初始化列表的方式为各个成员赋值时,必须使用常量表达式。
前面提到,constexpr
可用于修饰函数,而类中的成员方法完全可以看做是“位于类这个命名空间中的函数”,所以 constexpr 也可以修饰类中的成员函数,只不过此函数必须满足前面提到的 4 个条件。
举个例子:
#include <iostream>
using namespace std;//自定义类型的定义
class myType {
public:constexpr myType(const char *name,int age):name(name),age(age){};constexpr const char * getname(){return name;}constexpr int getage(){return age;}
private:const char* name;int age;//其它结构体成员
};int main()
{constexpr struct myType mt { "zhangsan", 10 };constexpr const char * name = mt.getname();constexpr int age = mt.getage();cout << name << " " << age << endl;return 0;
}
程序执行结果为:
zhangsan 10
注意,C++11 标准中,不支持用 constexpr 修饰带有 virtual 的成员方法。
1.4 constexpr修饰模板函数
C++11 语法中,constexpr
可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。
针对这种情况下,C++11 标准规定,如果 constexpr
修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr
会被自动忽略,即该函数就等同于一个普通函数。
举个例子:
#include <iostream>
using namespace std;//自定义类型的定义
struct myType {const char* name;int age;//其它结构体成员
};
//模板函数
template<typename T>
constexpr T dispaly(T t){return t;
}int main()
{struct myType stu{"zhangsan",10};//普通函数struct myType ret = dispaly(stu);cout << ret.name << " " << ret.age << endl;//常量表达式函数constexpr int ret1 = dispaly(10);cout << ret1 << endl;return 0;
}
程序执行结果为:
zhangsan 10
10
可以看到,示例程序中定义了一个模板函数 display()
,但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求.
2. const与constexpr区别:
一些读者在学习过程中,经常会把 const
和 constexpr
搞混,不知道什么时候用 const
,什么时候用 constexpr
。本节就带领大家对 const
和 constexpr
做系统地区分。
有关 constexpr
的功能和用法,您可以阅读《C++11 constexpr》一节。
我们知道,constexpr
是 C++ 11 标准新添加的关键字,在此之前(C++ 98/03标准)只有const
关键字,其在实际使用中经常会表现出两种不同的语义。举个例子:
#include <iostream>
#include <array>
using namespace std;void dis_1(const int x){//错误,x是只读的变量array <int,x> myarr{1,2,3,4,5};cout << myarr[1] << endl;
}void dis_2(){const int x = 5;array <int,x> myarr{1,2,3,4,5};cout << myarr[1] << endl;
}int main()
{dis_1(5);dis_2();
}
可以看到,dis_1()
和 dis_2()
函数中都包含一个 const int x
,但 dis_1()
函数中的 x
无法完成初始化 array
容器的任务,而 dis_2()
函数中的 x
却可以。
这是因为,dis_1()
函数中的“const int x”
只是想强调 x
是一个只读的变量,其本质仍为变量,无法用来初始化 array
容器;而 dis_2()
函数中的“const int x”
,表明 x
是一个只读变量的同时,x
还是一个值为 5 的常量,所以可以用来初始化array
容器。
C++ 11标准中,为了解决const
关键字的双重语义问题,保留了const
表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr
关键字。因此 C++11 标准中,建议将const
和 constexpr
的功能区分开,即凡是表达“只读”语义的场景都使用 const
,表达“常量”语义的场景都使用 constexpr
。
在上面的实例程序中,dis_2()
函数中使用 const int x
是不规范的,应使用 constexpr
关键字。
有读者可能会问,“只读”不就意味着其不能被修改吗?答案是否定的,“只读”和“不允许被修改”之间并没有必然的联系,举个例子:
#include <iostream>
using namespace std;int main()
{int a = 10;const int & con_b = a;cout << con_b << endl;a = 20;cout << con_b << endl;
}
程序执行结果为:
10
20
可以看到,程序中用 const
修饰了 con_b
变量,表示该变量“只读”,即无法通过变量自身去修改自己的值。但这并不意味着 con_b
的值不能借助其它变量间接改变,通过改变 a
的值就可以使con_b
的值发生变化。
在大部分实际场景中,const
和 constexpr
是可以混用的,例如:
const int a = 5 + 4;
constexpr int a = 5 + 4;
它们是完全等价的,都可以在程序的编译阶段计算出结果。但在某些场景中,必须明确使用 constexpr
,例如:
#include <iostream>
#include <array>
using namespace std;constexpr int sqr1(int arg){return arg*arg;
}const int sqr2(int arg){return arg*arg;
}int main()
{array<int,sqr1(10)> mylist1;//可以,因为sqr1时constexpr函数array<int,sqr2(10)> mylist1;//不可以,因为sqr2不是constexpr函数return 0;
}
其中,因为 sqr2()
函数的返回值仅有const
修饰,而没有用更明确的 constexpr
修饰,导致其无法用于初始化 array
容器(只有常量才能初始化array
容器)。
总的来说在 C++ 11 标准中,const
用于为修饰的变量添加“只读”属性;而 constexpr
关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,这样的优化极大地提高了程序的执行效率。
参考: https://blog.csdn.net/lemonxiaoxiao/article/details/128344695
相关文章:
【C++】const与constexpr详解
1. constexpr:常量表达式 所谓常量表达式,指的就是由多个(≥1)常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。 实际开发中,我们经常会…...
蓝桥杯:日期统计讲解(C++)
日期统计 本题来自于:2023年十四届省赛大学B组真题 主要考察:暴力。 代码放在下面,代码中重要的细节全都写了注释,非常清晰明了: #include <bits/stdc.h> //万能头文件 using namespace std;int main() {…...
Python re.findall()中的正则表达式包含多个括号时的返回值——包含元组的列表
当re.findall()中的正则表达式包含多个括号时,返回值是一个列表,其中每个元素都是一个元组。这个元组的长度与正则表达式中括号的数量相同,元组中的每个元素都是与相应括号中的模式匹配的文本。 import re # 定义一个包含三个括号的正则表达…...

Python——列表
一、列表的特性介绍 列表和字符串⼀样也是序列类型的数据 列表内的元素直接⽤英⽂的逗号隔开,元素是可变的,所以列表是可变的数据类型,⽽字符串不是。 列表的元素可以是 Python 中的任何类型的数据对象。如:字符串、…...

无人机图像识别技术研究及应用,无人机AI算法技术理论,无人机飞行控制识别算法详解
在现代科技领域中,无人机技术是一个备受瞩目的领域。随着人们对无人机应用的需求在不断增加,无人机技术也在不断发展和改进。在众多的无人机技术中,无人机图像识别技术是其中之一。 无人机图像识别技术是利用计算机视觉技术对无人机拍摄的图像…...

清华AutoGPT:掀起AI新浪潮,与GPT4.0一较高下
引言: 随着人工智能技术的飞速发展,自然语言处理(NLP)领域迎来了一个又一个突破。最近,清华大学研发的AutoGPT成为了业界的焦点。这款AI模型以其出色的性能,展现了中国在AI领域的强大实力。 目录 引言&…...

人工智能学习与实训笔记(二):神经网络之图像分类问题
人工智能专栏文章汇总:人工智能学习专栏文章汇总-CSDN博客 目录 二、图像分类问题 2.1 尝试使用全连接神经网络 2.2 引入卷积神经网络 2.3 分类函数Softmax 2.4 交叉熵损失函数 2.5 学习率优化算法 2.6 图像预处理算法 2.6.1 随机改变亮暗、对比度和颜色等 …...

SSM框架,spring-aop的学习
代理模式 二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来…...

【设计模式】4、策略模式
文章目录 一、问题二、解决方案2.1 真实世界的类比2.2 策略模式结构2.3 适用场景2.4 实现方式2.5 优缺点2.6 与其他模式的关系 三、示例代码3.1 go3.2 rust 策略模式是一种行为设计模式,它能定义一系列算法,把每种算法分别放入独立的类中,以是…...

【C++学习手札】多态:掌握面向对象编程的动态绑定与继承机制(深入)
🎬慕斯主页:修仙—别有洞天 ♈️今日夜电波:世界上的另一个我 1:02━━━━━━️💟──────── 3:58 🔄 ◀️ ⏸ ▶️ ☰ &am…...

【机构vip教程】Android SDK手机测试环境搭建
Android SDK 的安装和环境变量的配置 前置条件:需已安装 jdk1.8及 以上版本 1、下载Android SDK,解压后即可(全英文路径);下载地址:http://tools.android-studio.org/index.php/sdk 2、新建一个环境变量&…...

2024.2.18
使用fgets统计给定文件的行数 #include<stdio.h> #include<string.h> int main(int argc, const char *argv[]) {FILE *fpNULL;if((fpfopen("./test.txt","w"))NULL){perror("open err");return -1;}fputc(h,fp);fputc(\n,fp);fput…...
Haproxy实验
环境: servera(Haproxy):192.168.233.132 serverb(web1):192.168.233.144 serverc(web2):192.168.233.140 serverd(客户端):192.168.233.141 servera(Haproxy): yum install haproxy -y vim /etc/haproxy/haproxy.cfg(配置文件) # 设置日志&#…...

CSRNET图像修复,DNN
CSRNET图像修复 CSRNET图像修复,只需要OPENCV的DNN...

004 - Hugo, 分类
004 - Hugo, 分类content文件夹 004 - Hugo, 分类 content文件夹 ├─.obsidian ├─categories │ ├─Python │ └─Test ├─page │ ├─about │ ├─archives │ ├─links │ └─search └─post├─chinese-test├─emoji-support├─Git教程├─Hugo分类├─…...
Vue3之ElementPlus中Table选中数据的获取与清空方法
Vue3之ElementPlus中Table选中数据的获取与清空方法 文章目录 Vue3之ElementPlus中Table选中数据的获取与清空方法1. 点击按钮获取与清空选中表格的数据1. 用到ElementPlus中Table的两个方法2. 业务场景3. 操作案例 1. 点击按钮获取与清空选中表格的数据 1. 用到ElementPlus中…...
Leetcode 516.最长回文子序列
题意理解: 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。 回文理解为元素对称的字串,这里…...

cool Node后端 中实现中间件的书写
1.需求 在node后端中,想实现一个专门鉴权的文件配置,可以这样来解释 就是 有些接口需要token调用接口,有些接口不需要使用token 调用 这期来详细说明一下 什么是中间件中间件顾名思义是指在请求和响应中间,进行请求数据的拦截处理…...

Leecode之面试题消失的数字
一.题目及剖析 https://leetcode.cn/problems/missing-number-lcci/description/ 数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗? 注意:本题相对书上原题稍作改动 示例 1&…...

STM32的三种下载方式
结果jlink,串口,stlink方式都没有问题,是当时缩减代码,看真正起作用的代码段有哪些,就把GPIO初始化中 /*开启GPIO外部时钟*/RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE); 把开启外部时钟的代码注释掉了。…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...

Zustand 状态管理库:极简而强大的解决方案
Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...

Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...

【无标题】湖北理元理律师事务所:债务优化中的生活保障与法律平衡之道
文/法律实务观察组 在债务重组领域,专业机构的核心价值不仅在于减轻债务数字,更在于帮助债务人在履行义务的同时维持基本生活尊严。湖北理元理律师事务所的服务实践表明,合法债务优化需同步实现三重平衡: 法律刚性(债…...

高考志愿填报管理系统---开发介绍
高考志愿填报管理系统是一款专为教育机构、学校和教师设计的学生信息管理和志愿填报辅助平台。系统基于Django框架开发,采用现代化的Web技术,为教育工作者提供高效、安全、便捷的学生管理解决方案。 ## 📋 系统概述 ### 🎯 系统定…...
数据库——redis
一、Redis 介绍 1. 概述 Redis(Remote Dictionary Server)是一个开源的、高性能的内存键值数据库系统,具有以下核心特点: 内存存储架构:数据主要存储在内存中,提供微秒级的读写响应 多数据结构支持&…...