我有以下代码
int m[4]={1,2,3,4}, *y;
y=m;
*y = f(y++); // Expression A
我的朋友告诉我,Expression A
他的行为定义明确,但我不确定他是否正确。
根据他的说法,函数在两者之间f()
引入了一个sequence point
,因此行为是明确定义的。
有人请澄清。
PS:我知道我们不应该出于实际目的编写这样的代码。只是为了学习。:)
我有以下代码
int m[4]={1,2,3,4}, *y;
y=m;
*y = f(y++); // Expression A
我的朋友告诉我,Expression A
他的行为定义明确,但我不确定他是否正确。
根据他的说法,函数在两者之间f()
引入了一个sequence point
,因此行为是明确定义的。
有人请澄清。
PS:我知道我们不应该出于实际目的编写这样的代码。只是为了学习。:)
充其量,有问题的代码具有未指定的行为。对于赋值运算符,“操作数的求值顺序未指定”(C99 §6.5.16/4)。
如果首先计算左操作数,则结果f(y++)
将存储在m[0]
. 如果首先计算右操作数,则结果将存储在m[1]
.
至于行为是否未定义,相关段落为:
在前一个序列点和下一个序列点之间,对象的存储值最多只能通过表达式的评估修改一次。此外,应仅读取先验值以确定要存储的值(C99 §6.5/2)。
如果首先评估左侧,那么我们与第二句发生冲突,因为顺序是:
y
在左侧读取以取消引用它y
在右侧读取以增加它y++
是完整的并被y
写入)在步骤 1 中,读取 的“先验值”,y
但目的不是“确定要存储的值”。因此,该行为确实是未定义的,因为一个有效的评估顺序会产生未定义的行为。
关于引入序列点的函数调用,您是绝对正确的。但是,该序列点并不能挽救您的情况。
首先考虑这个简单的例子
i = some_function(i++);
它有效吗?是的。为什么?它是有效的,因为函数(您正在谈论的那个)引入的序列点将两个修改i
彼此分开,从而使代码有效。这个表达式没有计算顺序,这会导致在i
没有中间序列点的情况下被修改两次。
但是,让我们回到您的变体
*y = f(y++);
在这种情况下,该序列点也存在。然而,语言不保证=
运算符的求值顺序(意思是:语言不保证赋值运算符的哪个操作数首先被求值:左或右)。编译器可以先计算左侧( ),然后*y
计算函数参数(y++
),然后调用函数,然后执行实际赋值。在这种潜在情况下,前两个步骤 - 读取y
和修改y
- 没有被序列点分隔。因此,行为是未定义的。
表达式定义不明确:
该表达式的有效解释是:
(1) int* t0 = y++;
(2) int t1 = f(t0);
(3) int& t2 = *y;
-----------------
t2 = t1;
该表达式的一个同样有效的解释是:
(1) int& t2 = *y;
(2) int* t0 = y++;
(3) int t1 = f(t0);
-----------------
t2 = t1;
这两者都是有效的,并产生不同的结果。所以表达式有未定义的结果。
编辑:这是不正确的,但是我将其留在这里,因为评论中的讨论有些启发性,我希望有价值。
它基于 C(或 C++)中运算符的评估顺序进行了明确定义。
赋值强制首先评估表达式的右侧。函数应用程序首先强制评估其参数,因此效果看起来相当清晰(虽然我还没有尝试运行它,所以请随时纠正我!)。我们可以使用临时变量(我将它们称为 t0 和 t1)重写它,我相信这可能会更清楚一点:
t0 = y++;
t1 = f(t0);
*y = t1;
术语“序列点”有点像红鲱鱼。序列点并没有真正创建,而只是为语言定义了严格的评估顺序的结果。
编辑:虽然这个答案似乎在智力上令人满意,但 James McNellis 的答案引用了 C99 规范的相关部分,该部分指出分配的评估顺序没有明确定义。完全归功于他实际检查了他的事实。我将把我的答案从“定义明确”修改为“对于特定编译器来说可能定义明确”,因为我认为大多数编译器不太可能定期更改它们发出此类代码的顺序(我说“可能”是为了解释任何非常激进的优化)。