我已经阅读了Could anyone explain these undefined behaviors (i = i++ + ++i , i = i++, etc...)和尝试理解 Sequence points on "comp.lang.c FAQ"之后浪费了2个多小时的时间来尝试通过gcc编译器解释以下结果。
expression(i=1;j=2) i j k
k = i++ + j++; 2 3 3
k = i++ + ++j; 2 3 4
k = ++i + j++; 2 3 4
k = ++i + ++j; 2 3 5
k = i++ + i++; 3 2
k = i++ + ++i; 3 4
k = ++i + i++; 3 4
k = ++i + ++i; 3 6
i = i++ + j++; 4 3
i = i++ + ++j; 5 3
i = ++i + j++; 4 3
i = ++i + ++j; 5 3
i = i++ + i++; 4
i = i++ + ++i; 5
i = ++i + i++; 5
i = ++i + ++i; 6
问题:
我想知道上图中显示的所有表达式(在4组中)是否有未定义的行为?如果只有一些具有未定义的行为,那些行为和哪些不行?
对于已定义的行为表达式,请您显示(不解释)编译器如何评估它们。只是为了确保,如果我得到这个预增量&邮寄增量正确。
背景
今天,我参加了一次校园面试,在这次面试中,我要求i++ + ++i
解释i
给定值sequence point
的结果。在gcc中编译该表达式后,我意识到我在采访中给出的答案是错误的。我决定将来不会犯这样的错误,因此,尝试编译前后增量运算符的所有可能组合并在gcc中编译它们然后尝试解释结果。我挣扎了2个多小时。我找不到评估这些表达式的单一行为。所以,我放弃了,转向stackoverflow。在阅读了一些档案之后,发现存在类似{{1}}和未定义行为的内容。
答案 0 :(得分:8)
除第一组外,其他三组中的所有表达式都有未定义的行为。
如何评估定义的行为(第1组):
i=1, j=2;
k=i++ + j++; // 1 + 2 = 3
k=i++ + ++j; // 1 + 3 = 4
k=++i + ++j; // 2 + 3 = 5
k=++i + j++; // 2 + 2 = 4
这是相当直接的。后增量与预增量之比。
在第2组和第4组中,很容易看到未定义的行为。
组2具有未定义的行为,因为=
运算符未引入序列点。
答案 1 :(得分:4)
中的任何一个语句都没有序列点。 之间有序列点。
如果在连续序列点之间修改同一对象两次(在这种情况下,通过=
或通过前缀或后缀++
),则行为未定义。所以第一组4个语句的行为是明确定义的;其他人的行为未定义。
如果定义了行为,则i++
会生成之前的值i
,并且副作用会通过添加i
来修改1
对它。 ++i
通过向i
添加1
来修改{{1}},然后生成修改后的值。
答案 2 :(得分:4)
我想知道上图中显示的所有表达式(在4组中)是否都有未定义的行为?
第2至5行:
k = i++ + j++;
k = i++ + ++j;
k = ++i + ++j;
k = ++i + j++;
都是明确定义的。所有其他表达式都是未定义的,因为它们都试图通过在序列点之间多次计算表达式来修改对象的值(对于这些示例,序列点出现在';'终止每个语句)。例如,i = i++;
未定义,因为我们试图通过赋值和后缀i
来修改++
的值而没有插入序列点。 FYI =
运算符不引入序列点。 ||
&&
?:
和,comma
运算符引入了序列点
对于已定义的行为表达式,请显示(不解释)编译器如何评估它们。
让我们从
开始k = i++ + j++;
表达式a++
计算为a
的当前值,并且在下一个序列点之前的某个时刻,a
增加1.所以,逻辑上,评估类似于
k = 1 + 2; // i++ evaluates to 1, j++ evaluates to 2
i = i + 1; // i is incremented and becomes 2
j = j + 1; // j is incremented and becomes 3
...然而
评估表达式i++
和j++
的确切顺序以及应用副作用的顺序是未指定。以下是完全合理的操作顺序(使用伪汇编代码):
mov j, r0 ; read the value of j into register r0
mov i, r1 ; read the value of i into register r1
add r0, r1, r2 ; add the contents of r0 to r1, store result to r2
mov r2, k ; write result to k
inc r1 ; increment value of i
inc r0 ; increment value of j
mov r0, j ; store result of j++
mov r1, i ; store result of i++
不要对算术表达式进行左右评估。不要认为++
和--
的操作在评估后立即更新。
因此,i++ + ++i
等表达式的结果将根据编译器,编译器设置甚至周围的代码而有所不同。行为是 undefined ,因此编译器不需要“做正确的事情”,无论可能是什么。您将获得 结果,但它不一定是您期望的结果,并且在所有平台上都不一致。
看着
k = i++ + ++j;
逻辑评估是
k = 1 + 3 // i++ evaluates to i (1), ++j evaluates to j + 1 (2 + 1 = 3)
i = i + 1
j = j + 1
同样,这里有一个可能的操作顺序:
mov j, r0
inc r0
mov i, r1
add r0, r1, r2
mov r2, k
mov r0, j
inc r1
mov r1, i
或者它可以做其他事情。编译器可以自由地改变单个表达式的计算顺序,如果它导致更有效的操作顺序(我的例子几乎肯定不是)。
答案 3 :(得分:2)
第一组都是定义的。在下一个序列点之前的某个时间,它们都会将i
和j
的值都增加为副作用,因此i
保留为2,j
保留为3。此外,i++
评估为1,++i
评估为2,j++
评估为2,++j
评估为3.这意味着第一个评估为1 + 2
k
,第二个将1 + 3
分配给k
,第三个分配2 + 3
到k
,第四个分配2 + 2
到k
其余部分都是未定义的行为。在第二组和第三组中,i
在序列点之前被修改两次;在第四组i
中,在序列点之前修改了三次。
答案 4 :(得分:0)
如果编译器可以告诉两个左值表达式识别同一个对象,那么让它以某种合理的方式表现就没有任何有意义的成本。更有趣的场景是其中一个或多个操作数是解除引用的指针。
鉴于代码:
void test(unsigned *a, unsigned *b, unsigned *c)
{
(*a) = (*b)++ + (*c)++;
}
有许多合理的方法可以让编译器处理它。它 可以加载b和c,添加它们,将结果存储到a,然后递增 b和c,或者它可以加载a和b,计算a + b,a + 1和b + 1,然后 以任意顺序写出它们,或执行无数其他任何一个 操作序列。在某些处理器上,可能会有一些安排 比其他人更有效率,编译器应该没有理由期待 程序员会认为任何安排都比任何安排更合适 其他
请注意,即使在大多数硬件平台上也会受到限制
通过相同可能导致的合理行为的数量
指向a,b和c的指针,标准的作者不做任何努力
区分似是而非的难以置信的结果。尽管很多
实现可以很容易地以基本上零成本提供某些行为保证(例如,保证像上面这样的代码总是将*a
,*b
和*c
设置为某些可能的 - 没有任何其他副作用的未指定值),即使这样的保证有时可能有用(如果指针在对象值很重要的情况下识别不同的对象,但可能不会这样做),它很时髦编译器编写者认为,当授予全权委托以触发任意破坏性副作用时,他们可以实现的有用优化的可能性略高于程序员从约束行为的保证中获得的价值。
答案 5 :(得分:0)
逗号有点棘手。 当成对使用时,它们确实会从左到右移动(实际上是for循环中的var)。 用逗号分隔的语句不能保证按给定顺序求值 如果放在多个语句中。 还要注意,在函数参数和声明用逗号分隔的情况下,不能保证执行顺序。
所以
int a=0;
function_call(++a, ++a, ++a);
可能会有不可预测的结果。
答案 6 :(得分:-1)
在大多数情况下,gcc首先实现预增量并在操作中使用这些值,然后评估后增量。
例如。在方框2中
前递增无,因此使用i
1
k = i++ + i++ // hence k = 1+1=2
i中有两个后增量,所以i = 3
一个预增量将i更改为2
k = i++ + ++i // hence k= 2+2= 4
i
中的一个帖子增量i= 3
k= ++i + i++
i
中的两个预增量使其为3
k=++i + ++i // hence k=3+3= 6
i = 3
希望解释一下。 但它纯粹取决于编译器。