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

Why is a = (a+b) - (b=a) a bad choice for swapping two integers?

本文关键字:整数 两个 选择 a+b 为什么 交换      更新时间:2023-10-16

我偶然发现了这段代码,用于交换两个整数,而无需使用临时变量或使用按位运算符。

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;
}

但我认为这段代码在 swap 语句中具有未定义的行为a = (a+b) - (b=a);因为它不包含任何确定计算顺序的序列点

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

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

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

不确定b是先修改还是其值在表达式中使用(a+b),因为缺少序列点.
查看标准 syas:

C11: 6.5 表达式:

如果标量对象的副作用相对于同一标量

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

阅读 C-FAQ- 3.8 和此答案,了解有关序列点和未定义行为的更详细说明。

<小时 />

1.重点是我的。

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

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

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

原因如下:

1)正如你所注意到的,在C中不能保证它确实这样做了。它可以做任何事情。

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

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

我偶然发现了这段代码,用于交换两个整数,而无需使用 Temporary 变量或使用按位运算符。

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

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

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

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

至少有两个问题。

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

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

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

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

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

a.c:3:26: 警告:"b"上的操作可能未定义 [-W序列点]

从性能角度来看,是否使用这种结构也是有争议的。当你看

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

语句:

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

调用未定义的行为。违反引文第二项规定:

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

2010 年发布了一个问题,其中包含完全相同的示例。

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

史蒂夫·杰索普(Steve Jessop)对此发出警告:

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

以下是 2012 年发布的一个问题的解释。请注意,由于缺少括号,样本并不完全相同,但答案仍然相关。

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

a = x + y;

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

事实上,在某些体系结构上,可以发出以下代码: 同时计算 x 和 y -- 例如,VLIW 架构。

现在,对于 N11 的 C1570 标准报价:

附件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;

问题是根据C++标准

除非另有说明,否则对单个运算符的操作数的评估 和单个表达式的子表达式是未排序的。

所以这个表达

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

具有未定义的行为。

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

但是,由于您有一个c++标签,我更喜欢一个简单的std::swap(a, b),以使其更易于阅读。

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

如果a+b超出其限制怎么办?假设我们使用从0100的数字。80+ 60会超出范围。