1

我有这样的代码

#include  <stdio.h>

int main()
{
    int A[4] = {3, 519, 27, 49};
    (A[1]) ^=  (A[3]);
    (A[3]) ^=  (A[1]);
    (A[1]) ^=  (A[3]);

    printf("%d, %d, %d, %d\n", A[0], A[1], A[2], A[3]);

    A[1] ^= A[3] ^= A[1] ^= A[3];

    printf("%d, %d, %d, %d\n", A[0], A[1], A[2], A[3]);
}

我想交换 A[1] 和 A[3] 的值。对于第一个 printf,我得到的答案是 3、49、27、519,这是对的。但是对于第二个,我得到 3、0、27、49。我认为语句“A[1] ^= A[3] ^= A[1] ^= A[3];” 已翻译为:

A[1] = A[1] ^ A[3];
A[3] = A[3] ^ A[1] ^ A[3];
A[1] = A[1] ^ A[3] ^ A[1] ^ A[3];

在计算这些表达式时,A[1] 始终为 519,而 A[3] 等于 49。当我使用 gdb 进行调试时,我发现在此语句中 A[1] 先更改 49 - > 566 ,然后 A[3] 从 519 更改为 49,然后 A[1] 从 566 更改为 0。

我也尝试像这样更改声明: volatile int A[4] = {3, 519, 27, 49}; 但输出仍然是一样的。并像这样更改语句: A[1] ^= (A[3] ^= (A[1] ^= A[3])); 答案仍然是错误的。

但是如果我用 g++ 而不是 gcc 编译代码,我可以得到正确的答案: 3, 49, 27, 519 3, 519, 27, 49 如果语句是

int a = 49;
int b = 519;
a ^= b ^= a ^= b;

它可以交换价值。我不知道为什么数组的元素是错误的。

4

1 回答 1

6

第二条语句调用未定义的行为,因为您在序列点之间修改了两次相同的值。

这是未定义行为的原因是,如果我们希望编译器能够生成高效的代码,那么像这样的松散规则是必要的。在您的示例中并不明显,但是如果您有这样的功能:

void
foo(int *a, int *b, int *c, int *d)
{
    *a ^= *b ^= *c ^= *d;
}

这将在机器代码中翻译成如下内容:

load r1 (register 1) with value at d
load r2 with value at c
load r3 with value at b
load r4 with value at a
r2 = r1 xor r2
r3 = r2 xor r3
r4 = r3 xor r4
store r2 at c
store r3 at b
store r4 at a

但是编译器不知道指针是否指向相同的内存。因此,如果我们想强制执行此函数的严格排序,我们必须执行以下操作:

load r1 with value at d
load r2 with value at c
r2 = r1 xor r2
store r2 at c
load r1 with value at b
r2 = r1 xor r2
store r2 at b
load r1 with value at a
r2 = r1 xor r2
store r2 at a

现在,这似乎是相同数量的工作,不是吗?相同数量的指令,只是顺序不同,作为奖励,我们使用的寄存器更少。那为什么不呢?原因是内存很慢。第一个指令序列将执行得更快,因为 cpu 在执行下一条指令之前实际上并没有等待前一条指令完成。您可以拥有数十条已经开始但尚未完成的指令。当我们想要第一个示例中的 r1 的值时,我们已经从内存开始了另外三个加载。这听起来并不多,但这样的事情加起来。

因此,C 标准已决定允许编译器在序列点之间做他们想做的事情(我在这个答案的第一句话中链接了一个 wiki 页面,解释了序列点是什么),如果他们需要这样做来生成高效的代码。这意味着如果您想确保计算的正确结果,您必须遵循某些规则(并且编译器通常不会警告您)。您不能在两个序列点之间两次修改相同的值。如果您读取和修改两个序列点之间的值,您只能读取它来计算修改。等等。有很多关于什么是未定义的规则,几乎所有这些规则都是未定义的,因为我们希望我们的编译器能够在各种不同的 CPU 架构上生成快速代码。

于 2013-10-16T07:19:33.617 回答