第一种技术的优点是它是一个通用的习语,明显且正确。它适用于任何地方、任何类型的变量。它很可能被优化编译器识别并被实际的“交换”指令(如果可用)替换。因此,除了更清晰、更正确之外,第一种技术也可能更有效。
第二种技术的优点是它避免了使用临时变量,并且它是一种令人愉快的晦涩技巧,深受那些不断收集晦涩技巧并提出涉及晦涩技巧的误导性“陷阱”面试问题的人的喜爱,并且(据我所知),他们通过用一些晦涩难懂的技巧来使自己的程序变得更难维护、更难移植、更不可靠。
第一种技术的缺点是:None.
(理论上,有人可能会说它使用临时变量是有缺点的,但实际上,这根本没有缺点,因为临时变量是免费的。我认为地球上没有人仍在为处理器编写代码,所以内存和寄存器有限,以这种方式“保存”临时变量实际上是值得担心的。)
第二种技术的缺点是它更难编写,更难让读者理解,并且可能效率较低(也许效率非常低)。它仅“适用于”算术类型,不适用于结构或其他类型。如果它被用来尝试与自身交换数据,它将无法工作(它会悄悄地损坏数据)。 (稍后会详细介绍这种可能性。)如果这些还不够糟糕,那么即使在“普通”情况下,它也可能从根本上存在错误,因为它可能会溢出,并且对于浮点类型,它可能会改变一个或两个值稍微由于舍入误差而导致,并且对于指针类型,如果被交换的指针不指向同一对象内,则它是未定义的。
您具体询问了性能问题,所以让我们多说几句。 (免责声明:我不是微优化方面的专家;我倾向于用相当抽象、随意的术语来思考指令级性能。)
第一种技术使用三个分配。第二种技术使用一次加法和两次减法。在许多机器上,算术运算与简单的赋值所花费的周期数相同,因此在许多情况下,两种技术的性能是相同的。但很难想象第二种技术如何变得更加高效,而很容易想象第一种技术如何变得更加高效。特别是,正如我已经提到的,如果目标处理器有第一种技术,编译器更容易识别并转换为更高效的 SWP 指令。
现在,有些题外话。这里介绍的第二种技术是传统的、令人惊奇的晦涩技巧的一种不太美味的变体,用于在不使用临时变量的情况下交换两个变量。在不使用临时变量的情况下交换两个变量的传统的、令人难以理解的技巧是:
a ^= b;
b ^= a;
a ^= b;
曾几何时,在某些圈子里,以一种更加美妙而晦涩的方式呈现这些技术是一种时尚:
a ^= b ^= a ^= b; /* WRONG */
a += b -= a -= b; /* WRONG */
但是这些演绎(同时,是的,绝对是精致地如果你喜欢这类东西,那就太晦涩难懂了)还有它们所代表的额外的崩溃缺点未定义的行为,因为他们尝试修改a
在同一个表达式中多次出现,且没有插入序列点。 (另请参阅关于该主题的规范问题.)
公平地说,我必须提到,在一种实际情况下,第一种技术使用临时变量可能是一个重大缺点,而第二种技术缺乏临时变量可能因此成为一个实际优势。一种情况是,如果您试图编写一个通用的“交换”宏,类似于
#define Swap(a, b) (a = a + b, b = a - b, a = a - b)
这个想法是,您可以在任何地方、任何类型的变量上使用这个宏,并且(因为它是一个宏,因此很神奇)您甚至不必使用&
关于您调用它的参数,就像它是一个函数一样。但在传统的 C 语言中,至少,如果你想写一个Swap
像这样的宏,使用技术 1 基本上是不可能做到的,因为没有办法声明必要的临时变量。
你不是在问这个子问题,但既然我提出了它,我不得不说解决方案(尽管它对那些喜欢美味默默无闻的人来说永远是令人沮丧的)就是首先不要尝试编写“通用”宏来交换两个值。你不能在 C 中做到这一点。(事实上,你可以在 C++ 中做到这一点,使用新的定义auto
,现在我猜 C 也有一些编写通用宏的新方法。)
当您尝试以这种方式编写“交换”宏时,实际上还有一个额外的崩溃问题,那就是它会not工作 — 如果调用者尝试与自身交换值,它将把一个或两个变量设置为 0,而不是交换值。你可能会说这不是问题,因为也许没有人会写Swap(x, x)
,但在不太完美的排序例程中,他们可能很容易编写Swap(a[i], a[j])
有时在哪里i
恰好等于j
, or Swap(*p, *q)
有时指针p
恰好等于q
.
另请参阅C 常见问题列表, 问题3.3b, 10.3 and 20.15c.