当前位置: 首页 > news >正文

内存序学习笔记(一)——表达式求值顺序

你知道为什么 i = i++ + 2 在 C++17 前行为未定义吗?

你知道为什么 func(a(), b()) 中,a()b() 的执行顺序为什么不是确定的吗?

这篇文章可以解答你的疑惑。

注:内容中包含作者自行理解的表述,请谨慎参考。

表达式求值

每个表达式的求值包括:

  • 值计算:计算表达式所返回的值。
  • 引发副作用:访问(读或写)volatile 泛左值所指代的对象,修改(写入)对象,调用库 I/O 函数,或调用任何做出这些操作的函数。
表达式副作用
访问 volatile 对象可能将寄存器中的该变量值更新为内存中的值
修改对象除了对象被修改,程序的状态也有略微改变
I/O对外部环境产生影响
包括上述所有表达式的函数如上

例如,表达式 a++值计算指这个表达式的返回值是 a,引发副作用a 的值加了 1。但在这个例子中,副作用是已定义的,是用户期望的。

顺序

按顺序早于(sequenced before)

  • A 按顺序早于 B(等价地,B 按顺序晚于 A),则 A 的求值会在 B 的求值开始前完成。
  • A 不按顺序早于 B 而 B 不按顺序早于 A:
    • 无顺序(unsequenced),则它们以任何顺序进行,并且同线程内,编译器可以将两者 CPU 指令交错。
    • 顺序不确定(indeterminately sequenced),它们以任何顺序进行,但不可重叠。下次求值顺序可以相反。

注意无顺序顺序不确定的区别。由此推出,顺序不确定早于都属于已定义的顺序。只有无顺序是未定义的。

求值顺序规则

将在另一篇文章中说明以下相关知识点:完整表达式、弃值表达式、成分表达式(子表达式)、指名函数表达式、成员指针表达式、复合赋值表达式。

编号表达式类型操作(早于)对象
1完整表达式值计算、副作用下一个完整表达式
2运算符的操作数值计算运算符结果
3函数实参值计算、副作用函数体内任何语句
4(内建)后自增、后自减值计算副作用
5(内建)前自增、前自减副作用值计算
6&& || ,(逗号)的左操作数值计算、副作用右操作数
7与条件运算符(?:)第一表达式值计算、副作用第二、三表达式
8(内建)赋值运算符、复合赋值运算符的左右操作数值计算副作用(修改左操作数)
8.1(内建)赋值运算符、复合赋值运算符副作用(修改左操作数)值计算(返回引用之前)
9列表初始化的子句值计算、副作用其后的子句
10函数调用表达式中,指名函数表达式值计算、副作用 C++17起实参、默认形参
11下标表达式 E1[E2] 中,E1值计算、副作用 C++17起E2
12成员指针表达式 E1.*E2E1->*E2 中,E1值计算、副作用 C++17起E2
13移位运算符表达式 E1 << E2E1 >> E2 中,E1值计算、副作用 C++17起E2
14简单赋值表达式 E1 = E2 或复合赋值表达式 E1 @= E2 中,E2值计算、副作用 C++17起E1
  1. 如果一个函数调用和一个表达式(可以是另一个函数调用)没有明确顺序,二者的求值顺序不确定。(程序必须表现为如同一次函数调用的 CPU 指令,不会与其它表达式求值指令交错。C++17起std::execution::par_unseq 例外)
  2. new() 的调用相对于 new 表达式中各实参的求值,是顺序不确定的(C++17前);顺序早于它(C++17起)。
  3. 函数返回值的复制按顺序早于 return 语句 末尾处对所有临时量的销毁。后者又早于该块中所有局部变量的销毁。
    int add(int a, int b) {return a + b;
    }int main() {int c = add(1, 2);
    }`add()` 的结果复制到 `c` 早于 `a+b` 结果临时量的销毁
    后者又早于局部变量 `a,b` 的销毁
  4. C++17起 函数调用中,每个形参的值计算和副作用(二者作为一个整体)与其它任何形参相比是顺序不确定的。
  5. C++17起 重载的运算符遵循它所重载的内建运算符的定序规则。
  6. C++17起 列表初始化逗号分隔的每个表达式,如同函数调用一般求值(顺序不确定)。

未定义行为

注:切记未定义顺序顺序不确定的区别。

  • 如果某个内存位置上的一项副作用相对于同一个内存位置上的另一副作用是无顺序的,那么它的行为未定义。

例 1:

i = ++i + 2;  // OK

++i 副作用早于求值(第 5 条),等号右边的值计算早于左边副作用(赋值)(第 8 条)。执行顺序为:

++i 副作用 > ++i 求值 > ++i + 2 求值 > i = ++i + 2 副作用 > i = ++i + 2 求值。


例 2:

i = i++ + 2; // C++17 前行为未定义

C++17前,i++ 求值早于副作用(第 6 条),两边的值计算早于 i = i++ + 2 的副作用(第 8 条),而没有定义两个副作用的顺序,故而 i 值不确定,行为未定义。
C++17起,i++ 的副作用早于 i = i++ +2 的副作用(第 14 条)。因此可能(“可能”是因为i++2 的求值顺序是不确定的)的执行顺序为:

i++ 求值 > i++ + 2 求值 > i++ 副作用 > i = i++ + 2 副作用 > i = i++ + 2 求值


例 3:

f(i = -2, i = -2); // C++17 前行为未定义

C++17新增了一项规则(第 18 条),它规定了形参求值和副作用是顺序不确定的,此前未定义。此前两个 i = -2 不仅顺序不一定,甚至 CPU 指令可能交错。此后尽管顺序不确定,但 CPU 指令不会交错。因此执行顺序总是:

i = -2 副作用 > i = -2 求值 > i = -2 副作用 > i = -2 求值 > f(i = -2, i = -2) 副作用 > f(i = -2, i = -2) 求值


例 4:

f(++i, ++i);       // C++17 前行为未定义,C++17 起未指明

同上,C++17前,++i++i 的副作用顺序是未定义的。C++17后(第 18 条),不管编译器优先执行哪个 ++i 都是符合规则的。但由于两个 ++i 顺序不确定,所以该表达式的值计算和副作用未明确。


例 5:

i = ++i + i++;     // 行为未定义

同例 2,i++i = ++i + i ++ 的副作用顺序未定义。

序列点规则(C++11前)

C++11 前没有 C++11起一般完备的规则,表达式求值的顺序规定依靠序列点定义。

C++11前的定义

序列点 (sequence point)是执行序列中的点,在该点所有来自序列中先前求值的副作用都已经完成,而后继求值的副作用都尚未开始。

C++11前的规则

  1. 每个完整表达式结尾(典型地在分号处)有一个序列点。

  2. 调用函数时(无论该函数是否内联,无论是否使用函数调用语法),所有函数实参的求值(若存在)之后有一个序列点,它发生于函数体内的任何表达式或语句的执行之前。

  3. 在从函数返回时,在从函数调用结果的复制初始化之后,和 return 语句的 表达式 末尾的临时对象析构(若存在)前,有一个序列点。

  4. 对函数的返回值进行复制之后,并在函数外任何表达式的执行之前有一个序列点。

  5. 一旦函数执行开始,则在被调用函数的执行完成前,不求值调用方函数的任何表达式(函数不能交错执行)。

  6. 每个使用内建(非重载)运算符的下列四种表达式的求值中,表达式 a 的求值后有一个序列点。

a && b
a || b
a ? b : c
a , b

C++11前的未定义行为

  1. 前后序列点间,至多可以修改在同一个内存位置中的任何对象的存储值一次,否则行为未定义。
i = ++i + i++;     // 未定义行为
i = i++ + 1;       // 未定义行为
i = ++i + 1;       // 未定义行为
++ ++i;            // 未定义行为
f(++i, ++i);       // 未定义行为
f(i = -1, i = -1); // 未定义行为
  1. 前后序列点间,访问表达式求值所修改的在同一个内存位置中的任何对象的先前值,必须只为确定要存储的值。如果以其他任何方式访问,那么行为未定义。
cout << i << i++; // 未定义行为
a[i] = i++;       // 未定义行为

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 深入提示工程:解锁ChatGPT的无限潜能,掌握AI时代的智能对话技巧
  • iOS——线程安全、线程同步与线程通信
  • 模型训练套路(一)
  • [数据集][目标检测]街道乱堆垃圾检测数据集VOC+YOLO格式94张1类别
  • 【数学建模】2024数学建模国赛B题(word论文+matlab):生产过程中的决策问题
  • C++ STL-List容器概念及应用方法详解
  • 2024年高教社杯数学建模国赛C题超详细解题思路分析
  • Linux 一个简单的中断信号实现
  • 力扣100题——子串
  • 经验笔记:SSL证书
  • Stream插件相关的用法
  • 操作系统概述及特征
  • 回溯——7.子集II
  • 【蓝桥杯嵌入式(一)程序框架和调度器】
  • 《机器学习》 基于SVD的矩阵分解 推导、案例实现
  • hexo+github搭建个人博客
  • 【391天】每日项目总结系列128(2018.03.03)
  • Android优雅地处理按钮重复点击
  • express + mock 让前后台并行开发
  • Git 使用集
  • iBatis和MyBatis在使用ResultMap对应关系时的区别
  • JavaScript设计模式之工厂模式
  • JS实现简单的MVC模式开发小游戏
  • LeetCode18.四数之和 JavaScript
  • MySQL QA
  • OSS Web直传 (文件图片)
  • Promise面试题2实现异步串行执行
  • Python学习笔记 字符串拼接
  • select2 取值 遍历 设置默认值
  • vue.js框架原理浅析
  • 大整数乘法-表格法
  • 基于Mobx的多页面小程序的全局共享状态管理实践
  • 十年未变!安全,谁之责?(下)
  • 在 Chrome DevTools 中调试 JavaScript 入门
  • UI设计初学者应该如何入门?
  • ​VRRP 虚拟路由冗余协议(华为)
  • ​字​节​一​面​
  • (10)STL算法之搜索(二) 二分查找
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (floyd+补集) poj 3275
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (二)【Jmeter】专栏实战项目靶场drupal部署
  • (附源码)计算机毕业设计SSM保险客户管理系统
  • (每日一问)设计模式:设计模式的原则与分类——如何提升代码质量?
  • (三)Honghu Cloud云架构一定时调度平台
  • (十五)Flask覆写wsgi_app函数实现自定义中间件
  • .describe() python_Python-Win32com-Excel
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .NET MAUI Sqlite程序应用-数据库配置(一)
  • .net的socket示例
  • .net生成的类,跨工程调用显示注释
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复
  • @KafkaListener注解详解(一)| 常用参数详解
  • [1159]adb判断手机屏幕状态并点亮屏幕
  • [2010-8-30]