☰
C++交换变量值:从临时变量到异或法,避开溢出与传参陷阱
2026/11/2 17:25:16 网站建设 项目流程

交换两个变量的值,大概是每个写C++的人最早接触的"算法"之一。看起来简单到不值一提——不就是找个临时变量倒一下吗?但我带过不少刚入门的朋友,也翻过很多初学者的代码,发现这个最基础的操作里藏着不少值得说道的东西:有人用加减法交换结果溢出翻车,有人用异或交换把自己绕晕,有人在函数里传值调用改了半天发现原变量纹丝不动。这些坑不踩一遍,很难真正理解C++里"值"和"引用"的区别,也很难建立起对数据类型边界的敏感度。

这篇内容面向刚学C++、正在啃基础语法的朋友,也适合那些想回头把基本功夯实的人。我会从最朴素的临时变量法讲起,把加减法、异或法这些"花式"交换的原理和风险掰开揉碎,再延伸到函数封装时的传参方式选择,最后聊聊这个小题里能折射出的编程习惯问题。每个方案我都会给出可运行的代码、说明为什么这么写、什么场景下不该这么写,尽量让你看完就能上手,而不是停留在"知道有这么回事"。

1. 临时变量法:最笨但最该先掌握的路子

刚接触交换逻辑的时候,很多人会觉得引入第三个变量"不够聪明",总想着两个变量直接搞定。我的建议恰恰相反:先把最朴素的临时变量法写熟、写透,再去研究那些花活。原因很简单,临时变量法是所有交换方案里语义最清晰、最不容易出错、也最贴近实际工程写法的一种。

1.1 三行代码背后的内存模型

先看最标准的写法:

#include <iostream> int main() { int a = 3; int b = 8; int temp = a; // 把 a 的值复制一份存起来 a = b; // 用 b 的值覆盖 a b = temp; // 把暂存的旧 a 值写回 b std::cout << "a = " << a << ", b = " << b << std::endl; return 0; }

输出结果是a = 8, b = 3。这三行代码之所以成立,关键在于temp保存的是a的值拷贝,而不是a这个变量本身。你可以把变量想象成贴了标签的盒子,a盒子里装着 3,b盒子里装着 8。第一步是把 3 抄一份放进temp这个新盒子里;第二步把 8 倒进a盒子,此时a盒子原来的 3 已经被覆盖掉了;第三步再把temp里的 3 倒进b盒子。整个过程没有任何一步会"丢失"数据,因为旧值在覆盖之前已经被备份了。

这里有个初学者常犯的误区:以为int temp = a;之后temp和a就"绑定"在一起了,改a会连带改temp。实际上对于int这种基本类型,赋值就是纯粹的值复制,两个变量此后各过各的,互不影响。这个认知非常重要,后面讲函数传参时还会反复用到。

1.2 为什么工程代码几乎只用这一种

你去看任何成熟项目的源码,需要交换两个值时,基本都是临时变量法(或者直接用std::swap,而标准库内部实现本质上也是这一套)。原因有这么几条:

  • 可读性最高。任何人扫一眼就知道你在干什么,不需要停下来推理。
  • 没有溢出风险。加减法交换在极端值下会溢出,临时变量法完全不涉及算术运算。
  • 对类型没有额外要求。加减法和异或法都要求类型支持相应运算符,而临时变量法只要求类型可拷贝。像自定义结构体、字符串、甚至复杂对象,只要支持拷贝构造和赋值,这套逻辑就能用。
  • 编译器优化友好。现代编译器对临时变量的寄存器分配做得很成熟,所谓"多一个变量多一份开销"在绝大多数场景下是不存在的。

我见过有朋友为了"省一个变量"硬用异或法,结果代码被同事 review 时打回来重写。工程上追求的从来不是省那一个栈上的字节,而是让人一眼看懂、让维护成本最低。

1.3 用 std::swap 更省心

如果只是单纯想交换,C++ 标准库已经给你备好了工具:

#include <iostream> #include <utility> int main() { int a = 3, b = 8; std::swap(a, b); std::cout << "a = " << a << ", b = " << b << std::endl; return 0; }

std::swap定义在<utility>头文件里(C++11 之后),它对基本类型就是内部做了一次临时变量交换,对容器和自定义类型则会走移动语义,效率更高。实际写业务代码时,能用std::swap就别自己手写,这是最省事也最不容易出岔子的选择。手写临时变量法的价值主要在于理解原理和应对面试、教学场景。

提示:自己实现交换逻辑时,变量命名别图省事叫t、tmp、x之类。叫temp或者更具体的名字(比如交换坐标时叫tempX),能让代码意图更清楚。这是很小的习惯,但积累起来就是代码质量的差距。

2. 加减法交换:省了变量,多了溢出隐患

临时变量法讲完,接下来聊第一个"花式"方案——用加减运算完成交换。这个思路在很多老教材里出现过,原理不难,但它的适用边界比想象中窄,新手如果只记住写法不理解限制,很容易在实际数据上翻车。

2.1 运算过程拆解

加减法交换的核心思路是:先把两个数的和存到其中一个变量里,再通过减法把对方的值"还原"出来。代码长这样:

#include <iostream> int main() { int a = 3, b = 8; a = a + b; // a 变成 11(两数之和) b = a - b; // b 变成 11 - 8 = 3(原来的 a) a = a - b; // a 变成 11 - 3 = 8(原来的 b) std::cout << "a = " << a << ", b = " << b << std::endl; return 0; }

我们一步步跟踪:初始a=3, b=8。第一步a = a + b之后a=11,此时b还是 8,两数之和被保存在a里。第二步b = a - b,用和减去原来的b(8),得到 3,正好是原来的a,于是b变成了 3。第三步a = a - b,用和(11)减去刚算出的b(3),得到 8,正好是原来的b。三步走完,两个值就换过来了。

这个推导过程值得你自己在纸上走一遍,因为它体现了一个很重要的思维:利用"和"作为中间载体,通过减法反推出原始值。理解了这层,你再看后面的异或法就不会觉得是玄学。

2.2 溢出的临界点在哪

加减法交换最大的问题就是整数溢出。C++ 里int通常是 32 位有符号整数,取值范围大约在 -21 亿到 21 亿之间。如果a和b都接近这个上限,a + b就会超出int能表示的范围,产生未定义行为(undefined behavior)。

举个具体的例子:假设a = 2000000000,b = 2000000000,两者相加是 40 亿,远超int上限。这时候a = a + b这一步就已经出问题了,后面的减法自然全错。更麻烦的是,有符号整数溢出在 C++ 标准里属于未定义行为,不同编译器、不同优化等级下结果可能不一样,这种 bug 极难排查。

#include <iostream> #include <climits> int main() { int a = INT_MAX; // 2147483647 int b = INT_MAX - 1; // 2147483646 a = a + b; // 这里已经溢出,行为未定义 b = a - b; a = a - b; std::cout << "a = " << a << ", b = " << b << std::endl; return 0; }

这段代码在某些环境下可能"看起来"还能跑出正确结果(因为补码运算的环绕特性),但这纯属巧合,不能依赖。我在实际项目里见过有人用这套逻辑处理金额、计数之类的数据,平时测试数据小没问题,一上线遇到大数值就出诡异结果,排查半天才定位到交换逻辑上。

2.3 什么情况下可以考虑用

那加减法交换是不是完全不能用?也不是。如果你的数据范围明确可控,比如交换的是 0 到 100 之间的分数、棋盘坐标这类小整数,溢出风险基本为零,用它也无妨。但即便如此,我依然不推荐在正式代码里这么写,因为收益(省一个变量)微乎其微,而可读性和安全性都打了折扣。

它真正的价值在于教学和思维训练:帮你理解变量之间如何通过运算互相推导,为后面学习更复杂的算法技巧打基础。面试里偶尔也会被问到"不用临时变量怎么交换",这时候你能说清楚原理和溢出风险,比单纯背出代码要加分得多。

注意:如果确实要在受限场景下用加减法交换,务必确认两个操作数的类型范围足够容纳它们的和。对于unsigned类型,溢出是定义良好的环绕行为,不会产生未定义行为,但结果依然不是你想要的交换结果,所以同样要避免。

3. 异或交换:位运算的巧劲与它的脾气

如果说加减法交换是"用算术绕路",那异或交换就是"用位运算绕路"。它是面试题里的常客,也是很多人第一次感受到位运算魅力的地方。但这套方案有几个隐蔽的坑,不理解清楚就乱用,迟早要吃亏。

3.1 异或的三条性质是根基

异或交换能成立,全靠异或运算(符号^)的三条性质:

  1. 自反性:x ^ x = 0,任何数和自己异或都等于 0。
  2. 恒等性:x ^ 0 = x,任何数和 0 异或都等于它自己。
  3. 交换律与结合律:a ^ b ^ a = b,异或的顺序不影响结果。

第三条是核心。把它翻译成人话:一个数被另一个数异或两次,就会"还原"回自己。这就像给数据上了一把"异或锁",锁两次就解开了。基于这个性质,交换代码可以写成:

#include <iostream> int main() { int a = 3, b = 8; a = a ^ b; // a 变成 a^b b = a ^ b; // b 变成 (a^b)^b = a a = a ^ b; // a 变成 (a^b)^a = b std::cout << "a = " << a << ", b = " << b << std::endl; return 0; }

逐步验证:初始a=3, b=8。第一步后a = 3^8。第二步b = a ^ b = (3^8)^8 = 3,b拿到了原来的a。第三步a = a ^ b = (3^8)^3 = 8,a拿到了原来的b。交换完成。

3.2 同一个变量异或自己会归零

异或交换最著名的坑,就是当两个待交换的变量指向同一块内存时,结果会变成 0。看这段代码:

#include <iostream> void xorSwap(int& x, int& y) { x = x ^ y; y = x ^ y; x = x ^ y; } int main() { int a = 5; xorSwap(a, a); // 传的是同一个变量 std::cout << "a = " << a << std::endl; // 输出 0,不是 5 return 0; }

为什么会这样?因为x和y是同一个变量的两个引用,第一步x = x ^ y相当于a = a ^ a = 0,此时a已经变成 0 了,后面两步再怎么算都救不回来。这个坑在数组操作里特别容易踩:比如你想交换数组里下标i和j的元素,如果某次i恰好等于j,用异或交换就会把那个元素清零。

加减法交换也有同样的问题——a = a + a之后a变成两倍,再减就乱了。所以任何"不用临时变量"的交换方案,都必须先判断两个操作数是不是同一块内存。而临时变量法天然免疫这个问题,这也是它更稳妥的原因之一。

3.3 可读性与实际收益的权衡

除了自交换归零,异或交换还有几个现实问题:

  • 可读性差。不熟悉位运算的人看到三行^会懵,需要停下来推导。
  • 只适用于整数。浮点数、字符串、自定义类型都不能用异或交换,通用性远不如临时变量法。
  • 现代编译器下没有性能优势。有人用异或交换是图"快",但在开启优化的情况下,临时变量法生成的汇编和异或法几乎没差别,甚至更快,因为异或法有数据依赖链,指令流水线不好排。

我个人的态度很明确:生产代码里永远用临时变量或std::swap,异或交换只作为理解位运算的教学案例。知道它怎么来的、坑在哪,比会背这三行代码重要得多。

提示:如果面试被问到异或交换,除了写出代码,主动提一句"当两个变量是同一块内存时会出错,需要额外判断",往往能让面试官眼前一亮。这说明你不是死记硬背,而是真的理解边界条件。

4. 封装成函数:传值、传引用与传指针的选择

把交换逻辑写进main里只是练习,真正写程序时你肯定想把它封装成一个函数反复调用。这时候问题就来了:为什么我写了个swap函数,调用完原变量却没变?这个困惑几乎每个初学者都经历过,根源在于C++的参数传递机制。

4.1 传值调用为什么改不动原变量

先看一个"错误"的写法:

#include <iostream> void swapByValue(int x, int y) { int temp = x; x = y; y = temp; } int main() { int a = 3, b = 8; swapByValue(a, b); std::cout << "a = " << a << ", b = " << b << std::endl; // 还是 3 和 8 return 0; }

运行结果是a = 3, b = 8,完全没变。原因在于传值调用会把实参的值复制一份给形参,函数内部操作的x、y是a、b的副本,函数里怎么倒腾都不影响外面的原变量。函数返回时,副本被销毁,一切照旧。

这个机制其实和前面讲的int temp = a是同一个道理:赋值就是复制值,副本和原件是两回事。理解了这一点,你就明白为什么"在函数里改参数"经常不生效——不是函数写错了,是参数传递方式选错了。

4.2 引用传递才是正解

要让函数真正修改调用方的变量,得用引用传递。引用相当于给原变量起了个别名,函数内操作别名就是操作原变量本身:

#include <iostream> void swapByReference(int& x, int& y) { int temp = x; x = y; y = temp; } int main() { int a = 3, b = 8; swapByReference(a, b); std::cout << "a = " << a << ", b = " << b << std::endl; // a = 8, b = 3 return 0; }

参数列表里的int& x表示x是int的引用。调用swapByReference(a, b)时,x绑定到a,y绑定到b,函数里对x、y的修改直接作用在a、b上。这就是标准库std::swap的基本形态。

引用传递有几个要点值得记住:

  • 引用必须在定义时初始化,不能先声明后绑定。
  • 引用一旦绑定就不能改绑到别的变量,所以x = y是"把 y 的值赋给 x 指向的变量",不是"让 x 改指向 y"。
  • 引用传递没有拷贝开销,对大对象来说效率更高。

4.3 指针方案与它的适用场景

除了引用,用指针也能实现交换:

#include <iostream> void swapByPointer(int* x, int* y) { int temp = *x; *x = *y; *y = temp; } int main() { int a = 3, b = 8; swapByPointer(&a, &b); std::cout << "a = " << a << ", b = " << b << std::endl; // a = 8, b = 3 return 0; }

调用时传的是地址&a、&b,函数内用*x、*y解引用后操作原变量。指针方案在C语言里是标配(因为C没有引用),在C++里则更多出现在需要表达"参数可能为空"的场景——比如某个指针可以为nullptr,函数内部要先判空再操作。如果参数一定有效,优先用引用,语法更干净,也不用担心空指针。

三种传参方式的对比可以整理成一张表:

传参方式能否修改原变量是否有拷贝开销能否为空典型场景
传值int x否有(基本类型可忽略)否只读使用参数值
传引用int& x是无否需要修改原变量
传指针int* x是(需解引用)无(只传地址)是参数可选、可能为空

注意:如果函数只是读取参数而不修改,对大对象应该用const引用(const int& x或const BigObject& obj),既避免拷贝又防止误改。这是C++里非常常见的写法,养成习惯受益很大。

5. 从交换小题看几个容易忽略的基本功

交换数值这个题目本身很小,但它像一面镜子,能照出不少初学者在基本功上的薄弱环节。我在帮人看代码时,经常从这类小练习里发现一些共性问题,这里挑几个最有代表性的聊聊。

5.1 数据类型的选择与边界意识

很多人写交换时默认用int,但实际数据未必适合。比如交换两个金额,用int存分还行,用float就可能遇到精度问题;交换两个超大计数,int可能不够用,得考虑long long。选类型时要先问自己:这个数据的取值范围大概多大?会不会有负数?需不需要小数?

#include <iostream> int main() { long long a = 9000000000LL; long long b = 8000000000LL; long long temp = a; a = b; b = temp; std::cout << "a = " << a << ", b = " << b << std::endl; return 0; }

这段代码里如果a、b声明成int,字面量9000000000就已经超出int范围了,编译期就会报警告甚至错误。养成"先想范围再定类型"的习惯,能避免很多后期才暴露的 bug。另外,字面量后面加LL后缀是明确告诉编译器"这是long long",避免类型推断出岔子。

5.2 变量命名与代码意图的表达

交换逻辑里临时变量叫什么,看似小事,其实反映了一个人的代码习惯。我见过太多int t = a;、int tmp = a;这种命名,在短小的交换函数里问题不大,但一旦逻辑变复杂,t、tmp、x1这类名字就会让代码变得难读。

更好的做法是让名字携带信息:交换坐标时叫tempX、tempY,交换账户余额时叫tempBalance,交换配置项时叫tempConfig。名字长一点没关系,现代编辑器有自动补全,敲起来不费劲,但读代码的人会感谢你。这是那种"当下多花两秒、以后省半小时"的投入。

5.3 用断言和测试守护边界情况

写完交换逻辑,别急着认为万事大吉。至少测这么几种情况:普通值、相等值、边界值(比如INT_MAX)、负数、零。如果用了异或或加减法方案,还要专门测"两个变量是同一个"的场景。

#include <iostream> #include <cassert> void swapByReference(int& x, int& y) { int temp = x; x = y; y = temp; } int main() { int a = 3, b = 8; swapByReference(a, b); assert(a == 8 && b == 3); int c = 5, d = 5; swapByReference(c, d); assert(c == 5 && d == 5); int e = -1, f = 0; swapByReference(e, f); assert(e == 0 && f == -1); std::cout << "all tests passed" << std::endl; return 0; }

assert在调试构建下生效,发布构建时会被优化掉,几乎零成本。对于这种基础函数,写几行断言把边界情况钉死,以后重构时心里有底。这个习惯从小练习开始培养,比等到写大项目时再补要容易得多。

5.4 别急着炫技,先求正确

最后说一个心态问题。初学者学完异或交换后,往往特别想在实际代码里用上,觉得这样显得"高级"。我完全理解这种心情,但工程实践里,正确性和可维护性永远排在技巧前面。一个用临时变量、三秒就能看懂的交换,比一个需要推导半天的异或交换更有价值。

真正的高手不是把代码写得别人看不懂,而是把复杂问题拆解得清清楚楚。交换数值这种题,练的是你对变量、内存、类型、传参这些基础概念的理解,而不是让你记住多少种花式写法。把这些底层认知打牢,后面学指针、学引用、学移动语义时,你会发现很多概念是一脉相承的。

我在实际带人的过程中发现,那些愿意老老实实把临时变量法写熟、把传值和传引用的区别搞透的人,后面学C++的曲线明显更平缓。反而是急着背各种"奇技淫巧"的,往往在遇到稍微复杂一点的场景时就卡壳,因为他们缺的是对机制的理解,而不是对写法的记忆。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询