资讯动态

C语言变量交换的四种写法:原理、坑点与工程选型

发布时间:2026/9/23 16:41:10 来源:尧图企业网站定制
两个变量交换数值放在C语言题目里几乎算得上“入门第一课”。但我发现网上搜索热度一直没降过有人问“翁恺c语言练习题里为什么要反复做这道题”也有人在讨论“冒泡排序c语言交换元素时到底怎么写更稳”还有人跑到嵌入式群里问“单片机c语言没有堆栈吗为什么”。别看这道题表面简单背后牵扯到变量存储、指针传参、位运算、宏定义副作用、甚至Keil调试时怎么看变量值内容远比三行代码丰富得多。这篇我打算把实际开发中常见的四种交换写法全部拆开讲一遍分别是临时变量法、算术运算法、异或运算法、指针参数与宏封装法。每种都附完整代码、运行验证、坑点说明最后再讲一讲在不同场景下应该怎么选型。适合刚开始学C语言的初学者也适合准备面试、笔试的在校生以及刚转嵌入式开发、正在跟开发板打交道的朋友。看完这篇文章你不仅能写出四种版本还能在面试官追问“哪种写法更好”的时候有理有据地答出来。1. 从一道基础题说起为什么这么多人在研究变量交换1.1 看起来很简单的题背后是三种语言思维先明确一下需求有两个变量a和b希望经过一系列操作之后a里存放原来b的值b里存放原来a的值。举例来说a 3、b 5交换后是a 5、b 3。很多新手第一反应是直接写a b; b a;写完一编译运行结果不对。原因不复杂执行完a b之后变量a的旧值已经被覆盖此时再写b a拿到的其实就是b自己原来a的值早就丢了。这个看起来可笑的错误恰恰是理解“变量是内存空间”的关键。变量在内存中是一块有名字、有地址、能读写的数据区域赋值操作的本质是往这块区域里写数据写入的同时会覆盖旧数据。所以交换的本质是“在覆盖之前先把一个值保存起来”至于这个“保存”放在哪里就衍生出了不同的方案。另外变量交换并不只是教学题目。冒泡排序、选择排序、数组元素移动、状态切换、PID参数调试这些都是高频使用场景。你写嵌入式代码时调整两个模块引用计数你写算法题时交换数组里的两个元素你写状态机时交换当前状态和下一状态。每次都会遇到同一个问题如何在不让数据丢失的前提下完成互换。1.2 四种方法整体选型与分类思路我先给出一张总览表格后面每个方法单独展开。这个表格你如果赶时间可以直接抄到笔记里。方法核心思路是否额外占用变量主要风险典型场景临时变量法借助第三个变量暂存是几乎无项目工程、嵌入式、通用代码算术运算法用加减运算实现原位交换否整数溢出、可读性较差面试题、教学演示异或运算法利用按位异或的性质否相同地址/相同值会清零面试题、底层优化代码指针参数与宏封装通过地址修改实参或宏展开替换是指针本身指针悬空、宏副作用函数封装、嵌入式寄存器操作从这里可以看出四种方法的维度并不完全一样。临时变量法属于“老老实实多用一个变量”算术法和异或法属于“秀操作省一个变量”指针和宏属于“从参数传递角度解决问题”。站在面试角度能说出这四类写法并且能分析出各自适用边界基本就算过关了。站在工程角度我的建议是优先用临时变量法除非有极强的性能理由或者寄存器资源极度紧张再考虑后面几种。2. 四种实现方法的原理拆解与代码分析2.1 临时变量法最直观也最容易被低估的写法先说最常规的写法用第三个变量做中转。#include stdio.h void swap_with_temp(int *a, int *b) { int temp *a; *a *b; *b temp; } int main(void) { int a 3; int b 5; printf(交换前: a %d, b %d\n, a, b); swap_with_temp(a, b); printf(交换后: a %d, b %d\n, a, b); return 0; }原理很简单先把a的值拷贝一份放进temp此时a的值虽然被覆盖但旧数据已经有了安全备份。然后让a接收b的值最后让b从temp那里取回原来的a值。三步操作顺序不能乱一旦颠倒就会出现覆盖丢失。为什么我说这种方法“最容易被低估”因为总有人觉得多一个变量不够“高级”。但在真实工程中多一个局部变量的开销微乎其微而可读性和安全性带来的收益非常大。你写出来的代码不光是自己看还要让同事和以后的自己看懂。temp这个变量名一看就知道是临时中转维护成本极低。更重要的是临时变量法没有任何溢出风险不会因为数值范围问题出错也不要求变量必须是整数类型。浮点数、字符串指针、结构体变量理论上都可以用同一个思路完成交换。2.2 算术运算法省变量的代价是溢出风险第二种写法不需要额外变量纯粹用加减法完成交换。#include stdio.h void swap_with_add_sub(int *a, int *b) { *a *a *b; *b *a - *b; *a *a - *b; } int main(void) { int a 2000000000; int b 2000000001; printf(交换前: a %d, b %d\n, a, b); swap_with_add_sub(a, b); printf(交换后: a %d, b %d\n, a, b); return 0; }一步步推演设原值为a0、b0。执行*a *a *b后a里存的是a0 b0执行*b *a - *b后b里变成(a0 b0) - b0 a0此时b已经拿到原来a的值执行*a *a - *b后a里变成(a0 b0) - a0 b0交换完成。这个方法看起来很聪明但有一个致命问题整数溢出。C语言标准中有符号整数溢出属于未定义行为。拿上面代码里的int a 2000000000; int b 2000000001;举例a b的结果是 4000000001超出了 32 位int能表示的最大值约 21 亿此时结果会被截断。虽然用截断后的结果再逆推在某些编译器上依然能“歪打正着”得到正确交换结果但这是完全依赖实现的行为不保证所有平台都一致。不同架构、不同优化级别下可能出现完全不同的结果。所以我个人的看法是算术运算法适合面试时展示思路或者笔试时间紧张时顺手写出来但真正用于线上代码时要非常谨慎。如果交换的变量是unsigned int且确定和值不会溢出自洽性会好一些但也要考虑可读性和维护成本。为了省一个临时变量去承担未定义行为这笔账往往不划算。2.3 异或运算法位运算实现零临时变量第三种写法是用按位异或运算符^完成交换。异或运算的规则很简单两个二进制位相同结果为 0不同结果为 1。它还有一个重要性质任意数与自己异或等于 0任意数与 0 异或等于它本身。#include stdio.h void swap_with_xor(int *a, int *b) { *a *a ^ *b; *b *a ^ *b; *a *a ^ *b; } int main(void) { int a 10; int b 20; printf(交换前: a %d, b %d\n, a, b); swap_with_xor(a, b); printf(交换后: a %d, b %d\n, a, b); return 0; }推演一下。设原值为a0、b0。第一步*a *a ^ *b后a里存的是a0 ^ b0。第二步*b *a ^ *b右边是(a0 ^ b0) ^ b0 a0所以b得到原来的a0。第三步*a *a ^ *b右边是(a0 ^ b0) ^ a0 b0于是a得到原来的b0。三次异或完成交换。异或法的最大优点是不需要额外变量也不会产生加减法那种溢出问题所有位操作都在既定位宽内完成。但它有一个特别容易被忽视的大坑当a和b指向同一个内存地址时第一步就会把那个变量清零。比如你在数组里做交换传入arr[2]和arr[2]结果不是交换而是直接把arr[2]变成 0。这个坑在面试中也是高频考点面试官会故意写出一种看起来没问题、实际会翻车的版本让你找错。还有一点值得提异或运算虽然看起来“省了一个变量”但在现代 CPU 上并不一定比临时变量法快。这涉及到指令流水线和寄存器分配的问题编译器往往会把临时变量直接优化到寄存器里而三次异或反而可能打断某些优化路径。我在后面性能对比章节会具体展开。2.4 指针参数与宏封装函数封装时的关键差异前三种写法要么写在main里要么用指针作为函数参数传入。这一节专门说说函数封装这件事。新手最容易犯的错误是写成这样#include stdio.h void swap_bad(int a, int b) // 错误示范 { int temp a; a b; b temp; } int main(void) { int a 3; int b 5; swap_bad(a, b); printf(a %d, b %d\n, a, b); // 依旧是 3 和 5 return 0; }为什么没有交换成功因为C语言默认函数的参数传递是“值传递”。调用swap_bad(a, b)时程序把a和b的值拷贝了一份传给函数内部的形参函数内部确实完成了交换但交换的是两份拷贝外层实参a和b一点没变。要想修改外层的变量必须把变量的地址传进去也就是用指针参数。#include stdio.h void swap_with_ptr(int *a, int *b) { int temp *a; *a *b; *b temp; } int main(void) { int a 3; int b 5; printf(交换前: a %d, b %d\n, a, b); swap_with_ptr(a, b); printf(交换后: a %d, b %d\n, a, b); return 0; }这里int *a表示形参a是一个指针变量存的是实参的地址。函数体内*a表示“访问该地址上存放的变量”所以对*a赋值就是在修改外层变量。这个原理在嵌入式开发中尤其重要你要写一个函数修改 MCU 某个寄存器的值或者修改某个全局状态变量都必须用指针传递地址才能做到。除了函数还有一种常见做法是宏封装。很多嵌入式代码写成这样#define SWAP_WITH_TEMP(type, a, b) \ do { \ type temp (a); \ (a) (b); \ (b) temp; \ } while (0)宏的好处是可以保持调用处的类型不需要专门写int*或float*版本缺点是宏展开后可能导致参数被重复求值。比如你写SWAP_WITH_TEMP(int, i, j)宏展开后(a)出现了两次i被执行两次结果完全不可控。所以在宏定义里所有参数都要加括号并且尽量用临时变量缓冲。如果只是做普通变量交换我建议优先写普通函数或者通用宏但在表达式中出现自增、函数调用这类“有副作用表达式”时不要直接用宏交换。2.5 从原理到写法四种方法的核心代码速查这里我整理一段可以直接跑的完整测试程序把四种方法放在同一个工程里方便对照测试。有人喜欢把这种代码收进“c语言必背100代码”之类的笔记里确实值得背一背但更重要的是理解背后的原理。#include stdio.h void swap_temp(int *a, int *b) { int t *a; *a *b; *b t; } void swap_arithmetic(int *a, int *b) { if (a ! b) { *a *a *b; *b *a - *b; *a *a - *b; } } void swap_xor(int *a, int *b) { if (a ! b) { *a *a ^ *b; *b *a ^ *b; *a *a ^ *b; } } #define SWAP_PARAM(type, a, b) \ do { type t (a); (a) (b); (b) t; } while (0) int main(void) { int x 100; int y 999; printf(初始值: x %d, y %d\n, x, y); swap_temp(x, y); printf(临时变量法: x %d, y %d\n, x, y); swap_arithmetic(x, y); printf(算术运算法: x %d, y %d\n, x, y); swap_xor(x, y); printf(异或运算法: x %d, y %d\n, x, y); SWAP_PARAM(int, x, y); printf(宏封装法: x %d, y %d\n, x, y); return 0; }输出结果依次是初始值: x 100, y 999 临时变量法: x 999, y 100 算术运算法: x 100, y 999 异或运算法: x 999, y 100 宏封装法: x 100, y 999这里有两点值得注意。第一swap_arithmetic和swap_xor里我加了if (a ! b)判断就是为了防止传入同一地址时把数据清零。第二宏封装法里的do { ... } while(0)不是为了循环而是为了让宏在if语句中展开后不会出现悬挂问题。如果你不加这个写完if (cond) SWAP_PARAM(int, a, b); else ...时展开后的分号可能会出现语法错误。3. 实操过程与运行效果验证含环境准备3.1 开发环境搭建与调试工具选择写C语言开发环境不用搞得太复杂。Windows 上我建议用 VSCode 配置 C 语言环境编译器用 MinGW-w64 里的 gcc这样单文件编译、调试都很方便。具体来说下载安装 MinGW-w64然后编辑环境变量 PATH加入...\MinGW\bin路径这样在终端敲gcc --version就能看到编译器信息。VSCode 里装好 C/C 插件写代码时能自动提示、跳转定义对于查看变量和函数转跳非常有帮助。如果你用的是嵌入式开发板环境通常是 Keil MDK 或者 IAR。Keil 里新建工程后把源文件加进去编译按钮旁边就是调试按钮。我见过不少新手在 Keil 调试助手里不知道怎么显示结构体变量。实际上你只要在 Debug 模式下进入 Watch 窗口手动输入结构体变量名如果它带-或者.子成员展开箭头就能看到每个字段的实时值。如果你用的是数组直接输入数组名再展开索引也能逐项查看。这些调试手段对分析变量交换是否生效非常有用。3.2 完整测试工程四种方法在一个项目中验证为了直观看到四种方法的效果我建议你按下面的流程做一遍。第一步新建一个swap_test.c文件把 2.5 小节里的完整代码粘贴进去。第二步打开终端输入编译命令gcc -o swap_test swap_test.c -stdc99-stdc99是为了启用 C99 标准避免某些旧编译器对变量声明位置的限制。如果你用的是 VSCode也可以直接按 F5 运行调试观察变量变化。第三步运行程序./swap_test正常情况下你看到的结果就是我上面列出的输出。这里我提醒一个很容易踩的坑如果你用printf输出int类型的变量格式控制符必须写成%d不要写成%f或%s。有位学弟曾经用%d输入一个字符类型的变量后发现输出值变成了一长串数字原因就是格式控制符和变量类型不匹配导致从栈上读取了错误位数的数据。C语言不会帮你检查这类错误只能自己多留个心眼。3.3 变量地址与内存变化的观察技巧交换操作说到底是内存数据的变化。想要深入理解它建议在调试器里观察变量地址和内存内容。用 VSCode 调试时可以在main函数第一行打断点然后打开变量监视窗口添加a、b、a、b。你会发现a和b的地址是不同的通常相差 4 个字节如果是int。进入函数后形参ptr1、ptr2两个指针变量本身也有自己的地址它们存储的内容才是外层变量的地址。在 Keil 的 Debug 模式下你选择 View - Watch Window然后添加a和b再单步执行每一行交换代码可以看到变量值实时变化。这也是练习看变量的最好方式。很多学嵌入式的人会问“单片机c语言没有堆栈吗为什么”其实MCU 的 C 语言同样有栈只不过栈空间由启动文件和链接脚本分配通常放在片内 RAM 的高地址区域。局部变量、函数调用的返回地址、中断现场都会用到栈。临时变量法里的temp变量一般就分配在栈上或者优化后分配在寄存器里。我实测下来在调试模式下观察异或法时有一个非常有意思的现象你把 Watch 窗口里的a和b同时盯住单步执行*a *a ^ *b一行后a的值变成了一个“看不出规律”的数等到第三步执行完才恢复成对方原来的值。这有助于你理解中间状态的含义——并不是每一步的结果都必须满足常规直觉异或法的中间值本身就是加密式的混乱状态。3.4 边界值与性能损耗对比测试面试官常问“你这段代码在所有情况下都正确吗”所以我们需要自己做一轮边界测试。建议你额外写一个测试函数分别测负数、0、最大值、溢出场景以及a和b指向同一个地址的情况。测试用例临时变量法算术运算法异或运算法宏封装法a 3, b 5正常正常正常正常a -3, b 5正常可能受溢出影响正常正常a 0, b 0正常正常正常正常a 2147483647, b 1正常溢出风险高正常正常a 与 b 指向同一地址正常异常异常清零正常从这个表格能看出来临时变量法和宏封装法宏内部其实就是临时变量法是最稳的算术法和异或法都需要增加地址判断。所以如果你在写一个通用工具函数最好在函数开头就判断if (a b) return;避免后续不必要的操作。关于性能我实际用 gcc 在-O2优化级别下看了生成汇编码。临时变量法和异或法的核心交换部分往往会被优化成几乎相同的寄存器交换指令或者直接用 XCHG 指令。也就是说在绝大多数现代平台上“临时变量慢”是一个过时的刻板印象。真正需要关心性能的地方是嵌入式低资源环境比如寄存器数量极少、内存带宽极小的 8 位单片机。在那种环境下异或法省掉的临时变量可能意味着少占用一个寄存器但代码可读性会下降且要冒地址相同的风险。我的建议是如果你不是在做一个对性能极度敏感的算法或者你正在维护一段别人都要读的代码优先选临时变量法。4. 常见问题与排查技巧实录4.1 为什么我的交换函数根本没生效这是后台私信里被问得最多的问题问的人多半写出了类似这样的代码void swap(int a, int b) { int t a; a b; b t; }然后在main里调用swap(x, y);输出发现x和y原封不动。原因前面已经讲过C语言函数传参是值传递。这里再补充一个理解角度你可以把形参a、b看作函数内部的另两个变量它们的内存地址和外层实参完全不同。函数内部交换的是“传入值的副本”外层实参无法感知。解决方法是改为传地址或者在调用处直接利用宏替换完成交换。4.2 异或交换在哪些情况下会翻车异或法翻车主要分两种情况。第一种是a和b指向同一个地址第一步就清零第二种是a和b原本值相同比如a b 5三次异或后依然是 5这本身不算错误但会导致你看到结果时误以为交换没发生。还有一种隐蔽问题如果你把异或交换的代码写成一个宏例如#define SWAP_XOR(a, b) \ (a) ^ (b); \ (b) ^ (a); \ (a) ^ (b);在if语句里使用时会出问题if (cond) SWAP_XOR(x, y); // 宏展开后只有第一行属于 if后面两行无条件执行这种语义混乱很难排查。所以宏定义务必使用do { ... } while(0)包裹这也是我前面强调过得写法。4.3 单片机与嵌入式开发中的几个注意点在嵌入式场景下变量交换不是简简单单几行代码就完了。做中断低功耗或者状态机时经常会有在中断服务函数里修改主循环变量的需求。如果被交换的变量被中断和主循环共享要先用关中断或者临界区保护再执行交换。否则可能在交换到一半的时候被中断打断导致变量变成“中间值”。另外嵌入式开发中经常访问寄存器映射地址比如#define REG_A (*(volatile unsigned int *)0x40000000)。如果你要交换两个寄存器或者内存映射变量的值不能随便套用临时变量法吗当然可以套但要注意类型。volatile关键字要求编译器每次访问变量都从内存读取不能优化到缓存寄存器里。所以交换 volatile 变量时临时变量最好也声明成 volatile否则可能产生“编译器以为没变”的优化问题。还有人在遇到全局变量交换时喜欢写一个通用函数却忘了全局变量在多文件工程中的声明问题。如果你在一个.c文件里定义全局变量int g_flag_a;另一个文件里用extern int g_flag_a;声明后再交换一定要保证两个文件的类型一致。我建议这类共享数据尽量通过函数接口封装不要让外部随意读写。4.4 常见错误速查表错误类型典型代码结果正确做法不借助任何变量直接赋值a b; b a;丢失原值引入临时变量或其他方法形参值传递void swap(int a, int b)外层不变改为指针参数宏参数无括号#define SWAP(t, a, b) { t x a; a b; b x; }有副作用的表达式被多次求值参数加括号并用 do-while 包裹同地址异或swap_xor(a, a)变量被清零增加地址判断算术法溢出a a b超过 int 范围未定义行为避免使用算术法处理大数调试时看错格式printf(%d, 3.14)输出错误使用%f等匹配格式控制符这张表建议收藏实测下来很多面试题就是围绕这些错误变着花样出的。5. 不同场景下我建议怎么选5.1 学习、面试、工程、嵌入式四个维度如果是在学习阶段我建议四种方法都亲手写一遍。尤其是异或法能帮你建立位运算的直觉。写的时候别只看视频要自己在 VSCode 或 Keil 里跑一遍观察每一步变量的值。如果是面试或笔试阶段优先写临时变量法这是最稳妥、最容易自圆其说的方案。面试官如果追问“能不能不用临时变量”你再用算术法或者异或法补充并主动指出来需要注意哪些坑。主动踩坑点的展示比一口气背四种写法更能体现你的工程意识。如果是工程项目尤其是团队协作、长期维护的代码无脑选临时变量法。无论是普通 C 工程还是嵌入式固件工程可读性都是第一位的。你没听说过哪个项目因为不用异或法交换变量而崩溃但是会因为可读性太差的代码改出无数 bug。如果是嵌入式底层开发、寄存器级操作为主的场景可以参考上下文选择。比如你在一个寄存器严重不够用的 MCU 中写排序算法为了减少临时变量带来的栈和寄存器压力异或法可能是合理的优化方向。但这个使用必须配合详尽的注释并且做好同地址判断防止排序到中间碰上自身交换而清零。5.2 性能与可读性之间的取舍性能这个问题我用一句话总结不要在没有性能瓶颈的地方做无谓优化。现代编译器的优化能力远超大多数人的直觉。写一个临时变量交换函数开启优化后编译器经常直接把交换算法合并成一条寄存器交换指令而手写的异或交换反而可能打断编译器的优化节奏。也就是说你辛辛苦苦写的“高级写法”很可能在性能上并不占优甚至更慢。看一个例子在 x86-64 平台上用 gcc -O2 编译临时变量交换函数核心代码经常是movl (%rdi), %eax movl (%rsi), %edx movl %edx, (%rdi) movl %eax, (%rsi)这套MOV指令清晰、高效。异或交换生成的指令可能是一串XOR并没有本质优势。在做嵌入式开发时如果使用了 8 位低端 MCU又没有开启优化临时变量法可能会把变量存到栈上额外造成一次内存访问而异或法只用寄存器和 ALU 就能完成。这时候异或法确实有实际物理意义但这类平台已经越来越少而且通常用在极简的小工程里。5.3 我个人的实操心得写到这里我多说一点个人偏好。做过多年代码维护我慢慢意识到代码里最值钱的部分不是“省了一个变量”而是“别人一看就懂”。临时变量法的可读性远高于其他方法。我如果是在公司代码里做 code review看到有人写异或交换第一反应是问他有没有处理同地址情况有没有注释说明为什么要这样优化。如果只是单纯想炫技我会建议他改回来。不过这不代表你应该忽略其他几种写法。作为C语言学习者熟练掌握指针、位运算、宏定义、值传递和地址传递的区别才是核心目标。变量交换只是载体。如果你能把四种写法和背后的原理都讲清楚再遇到类似的题目比如“交换两个数组元素”、“交换链表相邻节点”、“交换结构体中的两个字段”你都能举一反三。我个人在实际操作中的体会是所有交换问题的本质一句话就能概括——先保存一个值再覆盖再恢复。无论是临时变量、加减、异或还是通过指针间接操作都是在回答“被覆盖前的值存到哪里”这个问题。把这句话想透C语言变量交换这道题就彻底过关了。最后再分享一个小技巧写代码之前先在纸上画出变量和地址的关系图再动手写。这个习惯能让你少踩一半的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价