从Prasoon's answer到关于“未定义的行为和序列点”的问题,我不明白以下是什么意思
..只能访问先前值以确定要存储的值。
作为示例,以下引用在C ++中具有未定义的行为:
a[i] = i++;
int x = i + i++;
尽管有解释,我不明白这一部分(我想我正确理解了答案的其余部分)。
我不明白上面的代码示例有什么问题。我认为这些编译器的定义良好的步骤如下所示。
a[i] = i++;
a[i] = i;
i = i + 1;
int x = i + i++ ;
x = i + i;
i = i + 1;
我错过了什么?什么'先前值只能被访问以确定要存储的值是什么意思?
答案 0 :(得分:4)
另请参阅this question和my answer to it。我不会投票把它作为副本关闭,因为你问的是C ++而不是C,但我相信这两种语言的问题都是一样的。
只能访问先前值以确定要存储的值。
这似乎是一个奇怪的要求;为什么标准关注为什么访问一个值?当您意识到如果读取先前值以确定要存储在同一对象中的值时,这是有意义的,这会隐式地对这两个操作施加排序,因此必须在写入之前进行读取。由于这种排序,对同一对象的两次访问(一次读取和一次写入)是安全的。编译器不能以导致它们相互干扰的方式重新排列(优化)代码。
另一方面,在像
这样的表达式中a[i] = i++
对i
有三次访问:左侧读取以确定要修改a
的哪个元素,右侧读取以确定要递增的值,以及将递增的值存储回i
的写入。 RHS上的读写是可以的(i++
本身是安全的),但是在LHS上的读取和RHS上的写入之间没有定义的顺序。所以编译器可以自由地重新排列代码,改变那些读写操作之间的关系,并且标准比喻地抛出它的手并使行为不确定,对可能的后果一无所知。
C11和C ++ 11都改变了这个领域的措辞,明确了一些排序要求。 “先前价值”措辞不再存在。引用C ++ 11标准草案,1.9p15:
除非另有说明,否则评估各个运营商的操作数 并且个别表达的子表达式没有被排序。 [...] 对运算符的操作数的值计算进行排序 在运算符结果的值计算之前。如果一方 对标量对象的影响相对于任何一个都没有排序 对同一标量对象或值计算的其他影响 使用相同标量对象的值,行为未定义。
答案 1 :(得分:3)
a[i] = i++;
i
已修改。还会读取i
以确定要使用的a
索引,这不会影响i
的商店。这是不允许的。
int x = i + i++;
i
已修改。 i
还用于计算要存储到x
的值,这不会影响到i
的商店。这是不允许的。
答案 2 :(得分:1)
由于标准规定“只能访问先前值以确定要存储的值”,因此编译器不需要遵循您概述的“明确定义”步骤。
他们经常没有。
标准的措辞对您的特定示例意味着什么是允许编译器订购这样的步骤:
a[i] = i++;
i = i + 1;
a[i] = i;
int x = i + i++ ;
i = i + 1;
x = i + i;
其结果与您想象的明确定义的顺序完全不同。编译器也被允许做任何其他可能的事情,即使它对你的意义不如我刚刚输入的那样。这就是未定义行为的含义。
答案 3 :(得分:0)
虽然像x=y+z;
这样的语句在语义上等同于temp=y; temp+=z; x=temp;
,但通常没有要求(除非x
为volatile
)以便编译器实现它办法。它可能在某些平台上更有效地执行x=y; x+=z;
。除非变量是volatile
,否则编译器为赋值生成的代码可以向其写入任何值序列,前提是:
任何有权阅读" old"变量的值作用于赋值之前的值。
任何有权阅读" new"的代码变量的值作用于给定的最终值。
鉴于i=511; foo[i] = i++;
,编译器有权将值5
写入foo[511]
或foo[512]
,但同样有权将其存储到{{1} }}或foo[256]
,或foo[767]
,或其他任何内容。由于编译器有权将值存储在foo[24601]
的任何可能位移,并且由于编译器有权使用向指针添加过大位移的代码执行任何喜欢的操作,因此这些权限一起有效地表示编译器可以使用foo
执行任何操作。
请注意,理论上,如果foo[i]=i++;
是16位i
但unsigned int
是65536元素或更大的数组(完全可以在经典的Macintosh上使用),上述权利允许给定foo
的编译器写入任意值foo[i]=i++;
,但不做任何其他操作。在实践中,标准避免了这种细微的区别。在给出像foo
这样的表达式时,标准对编译器所做的事情没有要求,而不是说编译器的行为在某些狭窄的角落情况下受到约束而在其他情况下不受约束要容易得多。