未定义行为与求值顺序

Posted SuPhoebe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了未定义行为与求值顺序相关的知识,希望对你有一定的参考价值。

未定义行为

若违反某些规则,则令整个程序失去意义。

定义

在计算机程序设计中,未定义行为(undefined behavior)是指执行某种计算机代码所产生的结果,这种代码在当前程序状态下的行为在其所使用的语言标准中没有规定。常见于编译器对源代码存在某些假设,而执行时这些假设不成立的情况。

同时语言规范也不要求编译器诊断未定义行为(尽管许多简单情形确实会得到诊断),而且不要求所编译的程序做任何有意义的事(尽管编译器都会按照自己的想法做一些事情)。

也就是说就算在这种情况下,编译器编写出了一个rm -rf /的东西也是符合标准的。那么,黑客攻击也经常会用到类似的手段,比如通过数组越界非法访问一部分内存,那部分内存是程序栈空间的代码段,然后就能够修改程序需要执行的代码部分,实现hack。

未定义行为在C/C++里面非常常见,如,数组边界外的内存访问,有符号整数溢出,空指针的解引用,在无序列点的表达式中多于一次修改同一标量,通过不同类型的指针访问对象,等等。

求值顺序

在早期的C++中f(i++, i++)或者a = a++这样的语句就是未定义行为。

而在C++11和C++17中,C++委员会慢慢地将这些行为进行规范。

例如,在C++17中规定,赋值晚于自增,那么a = a++得到的a将不会改变,因为a自增之后,再将自增之前的值赋值给了a,那么a就不会改变。

但是任然有些是未定义的,假设i = 1,那么f(i++, i++)可能是两种结果f(1, 2)或者f(2, 1)(在C++17之前,编译器可以随便实现,甚至允许出现f(2341, 141)的结果,尽管大多数编译器并不会出现这样的结果)。

下面是一些规则:

  1. 下列全表达式:
    其值计算和副作用,包括应用到表达式结果的隐式转换,对临时量的析构函数调用,(初始化聚合体时)默认成员初始化器,和涉及函数调用的所有其他的语言构造,都按顺序早于下一个全表达式的每个值计算和副作用。

    • 不求值操作数
    • 常量表达式
    • 立即调用(C++20 起)
    • 整个初始化器,包含任何逗号分隔的成分表达式
    • 在非临时对象生存期末尾生成的析构函数调用
    • 不是其他全表达式一部分的表达式(例如整个表达式语句,for/while 循环的控制表达式,if/switch 的条件表达式,return 语句中的表达式,等等),
  2. 任何运算符的各操作数的值计算(但非副作用)均按顺序早于该运算符结果的值计算(但非副作用)。

  3. 调用函数时(无论函数是否内联,且无论是否使用显式函数调用语法),与任何实参表达式或与指代被调用函数的后缀表达式关联的每个值计算和副作用,都按顺序早于被调用函数体内的每个表达式或语句的执行。

  4. 内建后自增与后自减运算符的值计算按顺序早于其副作用。

  5. 内建前自增与前自减运算符的副作用按顺序早于其值计算(作为由复合赋值的定义所致的隐含规则)。

  6. 内建逻辑与 (AND) 运算符 && 和内建逻辑或 (OR) 运算符 || 的第一(左)操作数的每个值计算和副作用,按顺序早于第二(右)操作数的每个值计算和副作用。

  7. 与条件运算符 ?: 中的第一个表达式关联的每个值计算和副作用,都按顺序早于与第二或第三表达式关联的每个值计算和副作用。

  8. 内建赋值运算符和所有内建复合赋值运算符的副作用(修改左参数),均按顺序晚于左右参数的值计算(但非副作用),且按顺序早于赋值表达式的值计算(即早于返回指代被修改对象的引用之时)。

  9. 内建逗号运算符 , 的第一个(左)参数的每个值计算和副作用均按顺序早于第二个(右)参数的每个值计算和副作用。

  10. 列表初始化中,给定的初始化器子句的每个值计算和副作用按顺序早于,与在该初始化器的花括号包围的逗号分隔列表中,跟在它之后的任何初始化器子句相关联的值计算和副作用。

  11. 若某个函数调用既不按顺序早于又不按顺序晚于另一函数调用,则它们是顺序不确定的(程序必须表现为如同组成不同函数调用的 CPU 指令决不会交错,即使函数被内联也是如此)。
    规则 11 有一个例外:在 std::execution::par_unseq 执行策略下执行的标准库算法所作的函数调用是无顺序的,并且可以任意交错。(C++17 起)

  12. 对分配函数(operator new)的调用相对于 new 表达式中构造函数参数的求值来说,是顺序不确定的 (C++17 前)按顺序早于它 (C++17 起)。

  13. 从函数返回时,作为求值函数调用结果的临时量的复制初始化按顺序早于在 return 语句的操作数末尾处对所有临时量的销毁,而这些销毁进一步按顺序早于对环绕 return 语句的块的所有局部变量的销毁。
    (C++14 起)

  14. 函数调用表达式中,指名函数的表达式按顺序早于每个参数表达式和每个默认实参。

  15. 函数调用表达式中,每个形参的初始化的值计算和副作用相对于任何其他形参的初始化的值计算和副作用是顺序不确定的。

  16. 用运算符写法进行调用时,每个重载的运算符均遵循其所重载的内建运算符的定序规则。

  17. 下标表达式 E1[E2] 中,E1 的每个值计算和副作用均按顺序早于 E2 的每个值计算和副作用。

  18. 成员指针表达式 E1.*E2 或 E1->*E2 中,E1 的每个值计算和副早于都按顺序早于 E2 的每个值计算和副作用(除非 E1 的动态类型不含 E2 所指的成员)。

  19. 移位运算符表达式 E1<<E2 和 E1>>E2 中,E1 的每个值计算和副早于都按顺序早于 E2 的每个值计算和副作用。

  20. 每个简单赋值表达式 E1=E2 和每个复合赋值表达式 E1@=E2 中,E2 的每个值计算和副作用均按顺序早于 E1 的每个值计算和副作用。

  21. 带括号的初始化器中的逗号分隔的表达式列表中的每个表达式,如同函数调用一般求值(顺序不确定)。

以上是关于未定义行为与求值顺序的主要内容,如果未能解决你的问题,请参考以下文章

CSDN之C技能树学习:15 - 运算符优先级与求值顺序

CSDN之C技能树学习:15 - 运算符优先级与求值顺序

三角函数式的化简与求值20201128

二元运算符求值顺序问题

1.19.9.函数概览函数引用精确函数引用模糊函数引用函数解析顺序精确函数引用模糊函数引用自定义函数准备工作概述开发指南函数类求值方法标量函数表值函数聚合函数

知识点 47 三角函数式的化简与求值