第一句话:
在前一个和下一个序列点之间,通过表达式的求值修改对象的存储值最多只能一次。
这句话已经很清楚了。语言不会强制对子表达式进行排序,除非它们之间有一个序列点,并且它不要求任何未指定的评估顺序,而是说两次修改一个对象会产生未定义的行为。这允许进行激进的优化,同时仍然可以编写遵循规则的代码。
接下来的句子:
此外,先前的值只能被读取以确定要存储的值
乍一看确实有点令人费解;为什么读取值的目的会影响表达式是否具有定义行为?
但是,它反映出如果子表达式B依赖于子表达式A的结果,则必须首先评估A,然后才能评估B。C90和C99标准没有明确说明这一点。
该句的更明显的违例,在脚注中给出了一个示例:
a[i++] = i;
假设
a
是一个已声明的数组对象,
i
是一个已声明的整数对象(没有指针或宏技巧),每个对象不会被修改超过一次,因此不违反第一句话。但是,在左侧的
i++
的评估确定要修改哪个对象,在右侧
i
的评估确定要存储在该对象中的值 - 读取操作RHS和写入操作LHS的相对顺序未定义。同样,语言可以要求子表达式按某种未指定的顺序进行评估,但实际上它将整个行为留作未定义,以允许更积极的优化。
在你的例子中:
int i = 0, *a = &i;
a[i] = i;
读取i
的上一个值既是为了确定要存储的值,也是为了确定要将其存储到哪个对象中。由于a[i]
引用了i
(但仅因为i == 0
),修改i
的值会改变左值表达式a[i]
所引用的对象。在这种情况下,存储在i
中的值恰好与先前存储在那里的值相同(为0
),但标准对偶然存储相同值的写入操作也没有例外。我认为这种行为是未定义的。(当然,标准中的示例并不意味着覆盖此案例;它隐含地假设a
是一个声明的数组对象,与i
无关。)
至于标准允许的示例:
int a[10], i = 0;
a[i] = i;
有人可能会解释这个标准是未定义的。但我认为第二个句子只适用于由表达式修改的对象的值。 i
从未被表达式修改,因此没有冲突。 i
的值用于确定要修改的对象和要在那里存储的值,但这没问题,因为 i
的值本身永远不会更改。 i
的值不是“先前的值”,它只是值。
C11 标准有一个新的模型来评估这种表达式 - 或者说,它用不同的话来表达相同的模型。 它不是关于“序列点”,而是讨论副作用在彼此之前或之后排序,或与彼此无序。 它明确了这样一个想法:如果子表达式 B 取决于子表达式 A 的结果,则必须在评估 B 之前评估 A。
在 N1570 草案 的第 6.5 节中:
1 表达式是一系列运算符和操作数,指定计算值或指定对象或函数,生成副作用或执行组合。 运算符的操作数的值计算在运算符结果的值计算之前进行。
2 如果标量对象上的副作用与同一标量对象上的不同副作用或使用相同标量对象的值计算无序,则行为未定义。 如果表达式的子表达式有多个允许的排序方式,则如果出现了这样的未排序副作用,则行为未定义。
3 操作符和操作数的分组由语法指示。 除非另有规定,否则子表达式的副作用和值计算是无序的。