5

我读过,谁能解释这些未定义的行为(i = i++ + ++i,i = i++ 等),并在浪费了 2 多个小时后尝试理解 “comp.lang.c FAQ”上的序列点时间试图通过 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

问题:

  1. 我想知道上图中显示的所有表达式(4组)是否有未定义的行为?如果只有其中一些具有未定义的行为,哪些有哪些没有?

  2. 对于定义的行为表达式,您能否展示(不解释) 编译器如何评估它们。只是为了确保,如果我正确地得到了这个前置增量和后置增量。

背景:

今天,我参加了一次校园面试,在面试中我被要求解释i++ + ++i给定值i. 在 gcc 中编译该表达式后,我意识到我在采访中给出的答案是错误的。我决定以后不再犯这样的错误,因此,尝试编译所有可能的前置和后置增量运算符组合并在 gcc 中编译它们,然后尝试解释结果。我挣扎了2个多小时。我找不到对这些表达式求值的单一行为。所以,我放弃了,转向stackoverflow。经过一点点阅读档案,发现有一些类似sequence point和未定义的行为。

4

7 回答 7

9

除第一组外,其他三组中的所有表达式都有未定义的行为。

如何评估定义的行为(第 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 组具有未定义的行为,因为=运算符没有引入序列点。

于 2012-11-28T22:11:55.027 回答
5

我想知道上图中显示的所有表达式(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++

不要假设算术表达式从左到右求值。不要假设运算数在评估++--立即更新。

因此,like 表达式的结果i++ + ++i会根据编译器、编译器设置甚至周围的代码而有所不同。该行为未定义,因此编译器不需要“做正确的事情”,无论它可能是什么。你会得到一个结果,但不一定是你期望的结果,而且它不会在所有平台上保持一致。

看着

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

或者它可以做其他事情。如果它导致更有效的操作顺序(我的示例几乎可以肯定不是),编译器可以自由更改单个表达式的评估顺序。

于 2012-11-28T22:46:26.997 回答
4

这些语句没有序列点。它们之间有序列点。

如果您在连续序列点之间修改同一对象两次(在这种情况下,通过=或通过 prefix 或 postfix ++),则行为未定义。所以第一组 4 条语句的行为是明确定义的;其他人的行为是不确定的。

如果定义了行为,则i++产生 的先前i,并作为副作用i通过添加1来修改。 通过添加来++i修改,然后产生修改后的值。i1

于 2012-11-28T22:17:23.410 回答
2

第一组均已定义。它们都会在下一个序列点之前的某个时间增加ij作为副作用的值,因此i保留为 2 和j3。此外,i++计算为 1,++i计算为 2,j++计算为 2,++j计算为 3。这意味着第一个分配1 + 2k,第二个分配1 + 3k,第三个分配2 + 3k,第四个分配2 + 2k

其余的都是未定义的行为。在第二组和第三组中,i在一个序列点之前被修改了两次;第四组i在一个序列点之前修改了3次。

于 2012-11-28T22:18:40.873 回答
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到一些可能未指定的值而没有任何其他副作用),即使这样的保证有时可能是有用的(如果指针将在对象的值很重要的情况下识别不同的对象,但可能不会这样做)它是时尚的对于编译器编写者来说,当被授予全权委托以触发任意破坏性副作用时,他们可以实现的任何有用优化的微小可能性将比程序员从受约束行为的保证中获得的价值更有价值。

于 2017-05-18T19:23:49.753 回答
0

逗号有点棘手。它们成对时确实从左到右(对于 for 循环中的 vars 真的)。如果放置在一对以上的语句中,则不能保证以逗号分隔的语句按给定顺序进行评估。另请注意,如果函数参数和声明用逗号分隔,则不保证执行顺序。

所以

int a=0;
function_call(++a, ++a, ++a);

可能会产生不可预知的结果。

于 2019-06-04T13:49:40.773 回答
-1

在大多数情况下,gcc 首先实现预增量并在操作中使用这些值,然后评估后增量。

例如。在块 2 Pre 递增 none 所以使用i1

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

希望能解释一下。但这完全取决于编译器。

于 2017-05-18T15:38:48.933 回答