为什么 a = (a+b) - (b=a) 是交换两个整数的糟糕选择
Why is a = (a+b) - (b=a) a bad choice for swapping two integers?
我偶然发现了这段代码,用于交换两个整数,而无需使用临时变量或使用按位运算符。
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+b
或b=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
超出其限制怎么办?假设我们使用从0
到100
的数字。80+ 60
会超出范围。
- C++需要帮助从用户那里获得一个整数,并确保它在另外两个整数之间
- 比较C++中两个整数的最有效和最干净的方法是什么?
- 如何将字符串和整数读取到两个单独的动态数组中的程序编写?
- 为什么将两个浮点数相加会得到一个整数C++?
- 从两个 4x64 位整数数组中获取取模
- 为什么 Clang 和 GCC 中两个无符号整数之和的结果类型不同
- 比较两个整数在C++中与未知 int 类型的相等性
- 创建整数的 2D 数组,该数组将使用两个函数用随机数填充矩阵.我做错了什么?
- 如何组合两个整数向量
- 在两个整数之间交换最右边的N位
- 打印出两个整数之间的偶数
- 为什么将两个总和为 32k >整数相加时得到负数?
- C++ 两个线程,共享几个整数变量
- Python 等效代码,可像C++一样直接附加两个整数
- 是否有任何内置函数可以检查给定的两个数字在给定整数数组中的顺序是否相同?
- 移位运算符如何在查找两个整数中的不同位数?
- 如何安全地比较两个无符号整数计数器?
- 添加两个二进制数(整数数组)
- 运算符"|"如何计算两个整数?
- 如何找出在两个整数的乘积中设置了多少位(等于 1)