57

我偶然发现了这段代码,用于在不使用临时变量或使用位运算符的情况下交换两个整数。

int main(){

    int a=2,b=3;
    printf("a=%d,b=%d",a,b);
    a=(a+b)-(b=a);
    printf("\na=%d,b=%d",a,b);
    return 0;
}

但我认为此代码在交换语句中具有未定义的行为,a = (a+b) - (b=a);因为它不包含任何序列点来确定评估顺序。

我的问题是:这是交换两个整数的可接受解决方案吗?

4

10 回答 10

82

不,这是不可接受的。此代码调用未定义的行为。这是因为b未定义操作。在表达式中

a=(a+b)-(b=a);  

由于缺少序列点,不确定是b先修改还是将其值用于表达式( ) 。 看看什么标准的syas:a+b

C11:6.5 表达式:

如果标量对象的副作用相对于同一标量对象的不同副作用或使用相同标量对象的值的值计算是未排序的,则行为未定义。如果一个表达式的子表达式有多个允许的排序,则如果在任何排序中出现这种未排序的副作用,则行为是未定义的。84) 1

阅读C-faq- 3.8和这个答案以获得对序列点和未定义行为的更详细解释。


1. 重点是我的。

于 2013-12-27T12:29:58.810 回答
48

我的问题是 - 这是交换两个整数的可接受解决方案吗?

谁可以接受?如果您问我是否可以接受,那不会超过我参加的任何代码审查,相信我。

为什么 a=(a+b)-(b=a) 是交换两个整数的错误选择?

出于以下原因:

1)正如您所指出的,C 中不能保证它确实做到了。它可以做任何事情。

2) 假设它确实交换了两个整数,就像在 C# 中那样。(C# 保证副作用从左到右发生。)代码仍然是不可接受的,因为它的含义完全不明显!代码不应该是一堆巧妙的技巧。为必须阅读和理解它的人编写代码。

3)再一次,假设它有效。该代码仍然是不可接受的,因为这完全是错误的:

我偶然发现了这段代码,用于在不使用临时变量或使用位运算符的情况下交换两个整数。

那简直是假的。这个技巧使用一个临时变量来存储a+b. 该变量由编译器代表您生成,没有给出名称,但它就在那里。如果目标是消除临时工,这只会使情况变得更糟,而不是更好!你为什么要首先消除临时工?它们很便宜!

4) 这仅适用于整数。除了整数之外,还有很多东西需要交换。

简而言之,花时间专注于编写明显正确的代码,而不是试图想出实际上使事情变得更糟的聪明技巧。

于 2013-12-27T17:59:06.100 回答
32

至少有两个问题a=(a+b)-(b=a)

您提到自己的一个:缺少序列点意味着行为未定义。因此,任何事情都可能发生。例如,不能保证先评估哪个:a+bb=a。编译器可以选择首先为赋值生成代码,或者做一些完全不同的事情。

另一个问题是有符号算术的溢出是未定义的行为。如果a+b溢出,则无法保证结果;甚至可能会抛出异常。

于 2013-12-27T12:35:48.190 回答
29

除了关于未定义的行为和样式的其他答案之外,如果您编写仅使用临时变量的简单代码,编译器可能会跟踪这些值而不是在生成的代码中实际交换它们,并且稍后仅在某些情况下使用交换的值案例。它不能用你的代码做到这一点。在微优化方面,编译器通常比你好。

因此,您的代码很可能更慢、更难理解,并且也可能是不可靠的未定义行为。

于 2013-12-27T12:32:30.417 回答
28

如果您使用 gcc 并且-Wall编译器已经警告您

ac:3:26:警告:对“b”的操作可能未定义 [-Wsequence-point]

从性能的角度来看,是否使用这样的结构也是值得商榷的。当你看

void swap1(int *a, int *b)
{
    *a = (*a + *b) - (*b = *a);
}

void swap2(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

并检查汇编代码

swap1:
.LFB0:
    .cfi_startproc
    movl    (%rdi), %edx
    movl    (%rsi), %eax
    movl    %edx, (%rsi)
    movl    %eax, (%rdi)
    ret
    .cfi_endproc

swap2:
.LFB1:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

您看不到混淆代码的任何好处。


查看 C++ (g++) 代码,它的功能基本相同,但考虑move

#include <algorithm>

void swap3(int *a, int *b)
{
    std::swap(*a, *b);
}

给出相同的汇编输出

_Z5swap3PiS_:
.LFB417:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

考虑到 gcc 的警告并且没有看到任何技术收益,我会说,坚持使用标准技术。如果这成为瓶颈,您仍然可以调查如何改进或避免这一小段代码。

于 2013-12-27T16:57:55.850 回答
8

该声明:

a=(a+b)-(b=a);

调用未定义的行为。引用段落中的第二个应被违反:

(C99,6.5p2)“在前一个序列点和下一个序列点之间,对象的存储值最多只能通过表达式的评估修改一次。此外,先前的值应只读以确定要存储的值。

于 2013-12-27T12:33:45.570 回答
8

一个问题在2010年发布了一个完全相同的例子。

a = (a+b) - (b=a);

史蒂夫杰索普警告说:

顺便说一句,该代码的行为是未定义的。a 和 b 都在没有中间序列点的情况下进行读写。对于初学者,编译器完全有权在评估 a+b 之前评估 b=a。

这是2012 年发布的问题的解释。请注意,由于缺少括号,示例并不完全相同,但答案仍然是相关的。

在 C++ 中,算术表达式中的子表达式没有时间顺序。

a = x + y;

是先评估 x 还是先评估 y?编译器可以选择其中一个,也可以选择完全不同的东西。求值顺序与运算符优先级不同:运算符优先级是严格定义的,而求值顺序仅定义为您的程序具有序列点的粒度。

事实上,在某些架构上,可以发出同时评估 x 和 y 的代码——例如,VLIW 架构。

现在来自 N1570 的 C11 标准报价:

附件 J.1/1

在以下情况下,这是未指定的行为:

— 评估子表达式的顺序和发生副作用的顺序,函数调用()&&||? :和逗号运算符 (6.5) 中指定的除外。

— 赋值运算符的操作数的求值顺序 (6.5.16)。

附件 J.2/1

在以下情况下,这是未定义的行为:

— 相对于同一标量对象上的不同副作用或使用同一标量对象的值的值计算(6.5),标量对象上的副作用是无序的。

6.5/1

表达式是一系列运算符和操作数,它们指定值的计算,或指定对象或函数,或产生副作用,或执行它们的组合。运算符的操作数的值计算在运算符结果的值计算之前排序。

6.5/2

如果标量对象的副作用相对于同一标量对象的不同副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义。如果一个表达式的子表达式有多个允许的排序,则如果在任何排序中出现这种未排序的副作用,则行为未定义。84)

6.5/3

运算符和操作数的分组由语法指示。85) 除非稍后指定,否则子表达式的副作用和值计算是无序的。86)

你不应该依赖未定义的行为。

一些替代方案:在 C++ 中,您可以使用

  std::swap(a, b);

异或交换:

  a = a^b;
  b = a^b;
  a = a^b;
于 2013-12-27T18:24:11.860 回答
5

您可以使用XOR 交换算法来防止任何溢出问题,并且仍然有一个单行。

但是,既然你有一个c++标签,我宁愿只是一个简单std::swap(a, b)的,让它更容易阅读。

于 2013-12-27T12:26:11.553 回答
5

问题是根据 C++ 标准

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是无序的。

所以这个表达式

a=(a+b)-(b=a);

有未定义的行为。

于 2013-12-27T12:31:44.900 回答
4

除了其他用户调用的问题之外,这种类型的交换还有另一个问题:

如果a+b超出限制怎么办?假设我们使用数字 from 0to 10080+ 60会超出范围。

于 2013-12-27T20:38:12.193 回答