表达式的 Associativity、Precedence 与求值顺序如何确定?
两种顺序都满足依赖关系,题目无法唯一确定执行顺序
题目给出的 Precedence(优先级)和 Associativity(结合性)能确定表达式如何分组,但不足以确定所有运算的实际执行先后。
假定 & 是需要左右两个操作数的普通二元运算符,且没有额外的求值顺序规则,两种标注都满足计算依赖:
- 第一种先算完左侧,再计算右侧。
- 第二种先计算
a - b,再算完右侧,最后完成左侧除法。
如果题目另外规定“每个二元运算符都必须先完整求出左操作数,再求右操作数”,就只有第一种符合要求。“Associativity: Left to right” 本身没有规定操作数的求值顺序。
Precedence、Associativity 分别决定什么?
| 概念 | 决定的内容 | 示例 |
|---|---|---|
| Precedence | 不同优先级的运算符如何分组 | a + b * c 分组为 a + (b * c) |
| Associativity | 同优先级运算符连续出现时如何分组 | 左结合使 a / b * c 分组为 (a / b) * c |
| Order of evaluation | 操作数、子表达式按什么顺序求值 | f() + g() 中先调用哪个函数 |
“从左到右结合”描述的是语法分组。例如:
a - b - c
左结合得到:
(a - b) - c
这排除了 a - (b - c) 的分组方式,却不能推出所有子表达式都必须从左到右执行。f() + g() 只有一个 +,无需考虑结合方向,仍然需要确定先调用哪个函数。
先确定原表达式的结构
原表达式为:
(a - b) / c & (d * e / a - 3)
按题目给出的规则:
- 显式括号将
a - b分为一组,作为左侧/的操作数。 - 右侧括号内,
*和/的优先级高于-。 *与/同级且左结合,所以d * e / a分组为(d * e) / a。- 外层
/的优先级高于&,所以左侧分组为(a - b) / c。
补全括号后,表达式为:
(((a - b) / c) & (((d * e) / a) - 3))
& 是表达式树的根节点,连接左右两个子表达式。它位于最外层,但优先级低于 /;结构位置和运算符优先级需要分开看。
两种标注为什么都满足依赖关系?
将六个运算结果分别记为:
L1 = a - b
L2 = L1 / c
R1 = d * e
R2 = R1 / a
R3 = R2 - 3
F = L2 & R3
这里的排列不规定执行顺序,结果之间的依赖关系是:
L1 → L2 ─────────┐
├→ F
R1 → R2 → R3 ───┘
箭头表示后一个运算需要前一个运算的结果:
L2需要L1。R2需要R1,R3需要R2。F需要L2和R3。
题目没有规定左右两条分支之间的执行先后。
第一种标注对应:
L1 → L2 → R1 → R2 → R3 → F
配平括号后写作:
(((a - b)¹ / c)² & (((d * e)³ / a)⁴ - 3)⁵)⁶
第二种标注对应:
L1 → R1 → R2 → R3 → L2 → F
写作:
(((a - b)¹ / c)⁵ & (((d * e)² / a)³ - 3)⁴)⁶
两种顺序都在取得所需中间结果后才执行后续运算,因此都满足依赖关系。仅凭优先级和结合性,无法从中选定一种。
括号对编译器起什么作用?
括号明确表达式的分组,编译器据此建立表达式结构,例如抽象语法树(AST)。
本题中的 (a - b) 将整个减法结果作为 / c 的左操作数。按普通运算的语义,左侧除法需要先取得 a - b 的结果。
括号并不要求编译器按以下全局流程执行:
先算完所有括号内的表达式
再算所有括号外的表达式
第一种顺序就在算完右侧括号之前执行了左侧括号外的 / c,依然符合表达式结构。
求值时,运算符先取得所需的操作数值,再产生结果。先处理哪条分支,要看具体语言允许的求值顺序。编译器也不必为六个编号各生成一条机器指令;优化后的实现需要保持语言要求的可观察行为。
求值顺序在这里是否影响结果?
如果 a、b、c、d、e 都是求值期间保持不变的普通数值,运算没有副作用且全部正常完成,两种顺序就会得到相同结果,因为它们保留了相同的分组和运算依赖。
如果操作数中有修改共享状态的函数调用,求值顺序就可能影响结果。例如下面的伪代码:
x = 0
function next():
x = x + 1
return x
result = next() - next()
假定两个调用依次执行:
- 先求左操作数,结果为
1 - 2 = -1。 - 先求右操作数,结果为
2 - 1 = 1。
这个表达式无需考虑结合方向,结果的差异来自操作数的求值顺序。具体语言是否允许这两种执行方式,需要查该语言的规则。
本题还有几个前提需要注意:
- 题目没有说明
&的具体含义,不能直接套用某种语言中的按位与、字符串连接或其他定义。 - 除零、异常等情况可能中断某条分支的计算,使求值顺序影响可观察行为。
- 调整独立分支的执行先后与改变分组是两回事。例如,将
(d * e) / a改为d * (e / a)就改变了分组;在整数除法、浮点运算等情况下,结果可能不同。
回答这道题时,可以先补全括号,再列出依赖关系。要为所有运算排出确定的执行编号,还需要题目给出足以确定顺序的操作数求值规则。
备注:内容仅供参考。
