ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C语言swap函数:从值传递到指针,彻底搞懂交换的本质

2026/10/1 14:14:30 拓冰建站 浏览量
C语言swap函数:从值传递到指针,彻底搞懂交换的本质 1. swap函数到底是解决什么问题为什么C语言里绕不开它做了这么多年C语言相关的开发我越来越觉得一个程序员对swap函数的态度基本能反映他对指针和内存模型的理解深度。swap函数在C语言里就是交换两个变量值的工具排序、状态切换、缓冲区处理到处都是它的身影。可就是这么个三行代码的小东西能同时踩中传值调用、形参实参、指针解引用、栈帧生命周期好几个坎。很多初学者把代码敲完printf一看没换过来当场就懵了。这篇文章不打算绕弯子从最基础的写法讲到工程里常见的坑再给几个可以直接抄的调试方法。适合刚学完C语言函数部分、准备期末上机的人也适合复习指针和刷算法题时顺手查缺补漏的老朋友。先说最朴素的场景你手头有两个变量x和y想把它们的值互换。这个动作在逻辑上简单到不能再简单——把x的值给y把y的值给x。但真要在C语言里写出来却藏着关键的分叉你到底是在“操作两个变量本身”还是在“操作两个变量的副本”。这个分叉一出现传值和传地址的区别、指针和数组名的联系、函数栈帧的生命周期就全串起来了。所以我一直觉得拿swap当C语言指针入门的第一课比背一百句“指针就是地址”都管用。1.1 第一次写swap就翻车为什么函数里改不动外面的变量新手最容易写出的版本是这样的void swap(int a, int b) { int temp a; a b; b temp; }然后在main里调用int x 3; int y 5; swap(x, y); printf(x%d, y%d\n, x, y);编译能通过运行后输出还是x3, y5。这时候很多人会怀疑是不是自己编译器坏了或者printf写错了。其实问题不在编译器而在于C语言函数参数的默认行为——值传递。调用swap(x, y)时程序把x和y的值复制一份交给函数里的形参a和b。函数内部改的是自己栈上的两个新变量也就是a和b而不是main函数里的x和y。等swap返回栈上的a和b连同它们的修改一起被回收x和y自然纹丝不动。这里可以借一个生活场景来理解你把自己家钥匙的复印件交给朋友请朋友帮忙把门锁换个新的。朋友拿着复印件忙活半天能打开的始终是你家的复印件房子真房子一点不受影响。C语言的值传递就是这个逻辑——形参是实参的复印件函数操作的是复印件。想操作真房子必须把真房子本身的信息传过去也就是传内存地址。1.2 传指针不是“把地址当值传”而是要借地址找到原变量既然传值没用那就要传地址。写法变成这样void swap(int *a, int *b) { int temp *a; *a *b; *b temp; }main函数里的调用也要改swap(x, y);这里的x是x在内存中的地址y是y的地址。调用swap时函数形参a和b拿到的是两个地址值换句话说a和b本身依然是值——地址值。函数内写a意思是“根据a里存的地址找到那个地址上放着的变量去读写它”。所以a *b这行并不是在改a这个指针变量自己而是在改a指向的那个int变量也就是main里的x。三个语句走完main里的x和y就被真正交换了。这个细节如果没沉下心捋清楚后面一写链表、一写二叉树很容易出现“函数里看着改了回到main发现还是空的”这种问题。swap刚好是解决这类问题的最小实验场变量只有两个逻辑只有交换你可以放心地打印地址、打印解引用结果把指针和地址之间的关系彻底看清楚。2. 三种主流swap写法从教科书到工程实战既然理解了传地址下面就可以把几种常见写法放在一起比一比。不同写法对应不同场景有的适合教学有的适合嵌入式有的纯粹是面试题里的“奇技淫巧”。2.1 临时变量版稳定、清晰、编译器最喜欢最标准、也最推荐在实际工程里用的是临时变量版本void swap(int *a, int *b) { int temp *a; *a *b; *b temp; }它做的事情可以拆成三步先把a指向的值存到temp再把b指向的值放进a指向的位置最后把temp里保管的原a值放进b指向的位置。为什么需要temp因为如果不提前存一份直接执行*a b原来a的值就被覆盖了再执行b *a得到的只是b自己的旧值两个变量最终都会变成同一个值。生活里也一样你不借助一个临时桌面就没法把两堆文件完全对调位置。这个版本的优势有三个。第一是可读性强只要认得英文单词temp三行代码一目了然。第二是没有副作用对任何int都安全哪怕两个指针指向同一个地址结果也符合预期。第三是现代编译器很喜欢它优化后temp大概率直接放进寄存器并不需要额外访问内存。我见过很多同学担心这个temp会拖慢速度实际上在-O2编译选项下这段代码和手写的异或版本几乎没有性能差别甚至更快。工程代码里还可以加一点防御比如判断指针是否为空。但要注意内部计算密集的排序算法里每次都判断空指针会引入分支反而影响性能。所以我的建议是库函数或公共接口可以检查空指针但作为内部工具函数保持简洁就好。2.2 异或运算版不借助临时变量的经典技巧面试的时候经常被问“能不能不用临时变量交换两个整数”标准答案是用异或void swap(int *a, int *b) { if (a ! b) { *a ^ *b; *b ^ *a; *a ^ *b; } }这个技巧依赖异或的自反性质。简单推导一下假设初始时a Xb Y。第一步执行a ^b后a变成X ^ Y第二步执行b ^a此时b等于Y ^ (X ^ Y)由于异或满足交换律和结合律结果等于X所以b成功拿到原来的X第三步执行a ^ *ba等于(X ^ Y) ^ X结果等于Y于是a拿到原来的Y。三步之后两个变量的值互换完成。这个写法确实省了一个临时变量。在寄存器特别紧张的老式嵌入式环境里这曾经是实打实的优化手段。但现在再用它更多是作为理解位运算的练习题。工程上我不推荐原因有三一是可读性差不写注释过一段时间自己都容易看懵二是它只能作用于整数类型float、double、结构体都没法直接异或三是忽略了a和b指向同一个地址的情况——如果a和b相等第一步就把变量清零了后面两步再也无法恢复所以函数里必须加if (a ! b)保护。其实从性能角度看现代编译器下这种写法未必比临时变量快因为三条位运算之间存在数据依赖反而可能拖慢流水线。2.3 交换指针本身二级指针和真正的“换指针”swap还有一种容易混淆的变体交换两个指针变量的值而不是交换指针指向的内容。比如你有char *p1和char *p2想让他们互换指向的字符串。注意整个p1和p2是两个指针变量字符串本身所在的地址值存在这两个变量里。要交换p1和p2的值函数的形参就不能是int *而必须是int **void swap_ptr(int **a, int **b) { int *temp *a; *a *b; *b temp; }调用方式是swap_ptr(p1, p2)。传入的是p1和p2这两个指针变量自己的地址函数通过a和b去改p1和p2的值。这个写法初学者很容易踩坑以为swap(int *a, int *b)就能交换两个外部指针实际结果往往是外面p1和p2完全没变化。原因和传值调用一样函数内部只是交换了两个“指向外部指针的指针”所指向的内容不对准确地说如果参数只有int *你在函数里交换的是int *型形参a和b自己的值而外部p1、p2不受影响。想修改主调函数里的指针变量就必须传它的地址也就是指针的指针。这种二级指针写法在链表操作里特别常见比如反转链表、删除节点、交换相邻节点时经常要处理“头指针本身的更新”。能把swap_ptr写对说明你真正理解了“指针也是变量变量也有地址”这句话。3. 手把手实测编译、运行、调试一次swap调用光看代码还是不够我建议你亲手编译、运行、调试一遍。这个过程可以帮你建立内存层面的直观感受比复读十遍理论都有用。3.1 一个能直接跑的完整例子先把这个能直接编译的完整程序存成swap.c#include stdio.h void swap(int *a, int *b) { int temp *a; *a *b; *b temp; } int main(void) { int x 3; int y 5; printf(before: x%d, y%d\n, x, y); printf(address: x%p, y%p\n, (void *)x, (void *)y); swap(x, y); printf(after : x%d, y%d\n, x, y); return 0; }编译命令很简单gcc -g -O0 -o swap swap.c ./swap在我机器上的输出类似这样before: x3, y5 address: x0x7ffcb2a8a1dc, y0x7ffcb2a8a1d8 after : x5, y3注意看两个细节。第一交换前后x和y的地址没有变化变化的只是地址上存放的值说明我们修改的是同一块内存。第二swap函数内部拿到的是地址而不是值所以函数返回后修改依然有效。如果把这个例子里的swap改成错误版main里写swap(x, y)编译器可能会提示“passing argument 1 of swap makes pointer from integer without a cast”之类的警告运行结果则永远保持3和5不变。这种警告不是无关紧要的小噪音它是在提醒你你的实参和形参类型对不上代码健壮性已经出问题了。3.2 在gdb里单步观察值到底在哪个瞬间变了编译时加上-g选项就可以用gdb调试。下面是我常用的观察流程gdb ./swap (gdb) break swap (gdb) run (gdb) print a (gdb) print b (gdb) print *a (gdb) print *b程序会在进入swap函数第一行之前停下来。这时你会看到形参a的值等于main函数里x的地址b的值等于y的地址*a等于3*b等于5。这就是整个swap的核心——实参x、y的值并没有复制进来复制进来的只是它们的地址。继续单步(gdb) next (gdb) print temp (gdb) next (gdb) print *a (gdb) next (gdb) print *b每执行一条赋值语句你都能亲眼看到某个内存位置的值发生跳变。我记得第一次对着gdb做这个实验时心里那道“函数到底能不能改外部变量”的墙瞬间就塌了。如果你不想用gdb也可以在swap函数里临时加printf打印进入函数时的指针值和解引用值。重点不是工具而是观察“地址没变值在变”这个过程。4. 高频踩坑现场为什么你的swap就是没生效这一部分我想集中讲讲实操中最高频的翻车点。有些错误我已经看了十多年几乎每个初学者都会轮到一遍甚至工作几年的人不小心也会再犯。4.1 传值调用最常见的“没换过来”先看这个经典错误版void swap(int a, int b) { int temp a; a b; b temp; }调用swap(x, y)后函数内部的a和b确实交换了但x和y一点没变。从汇编层面理解调用函数时参数被压栈或者放进寄存器函数体内所有操作都基于这些副本而不是基于main函数的栈帧。等函数返回栈空间被释放副本不复存在。所以“函数内改了外面没变”不是玄学而是C语言内存模型里的必然结果。我把这个问题放在第一个是因为它代表一种思维惯性很多人写代码时把函数形参当作原变量的“别名”以为形参变实参就跟着变。C语言里没有这种魔法。Java、C选手可能习惯了引用传递但纯C里必须自己显式传地址。如果哪次swap没生效先别急着怀疑库函数第一件事就是检查你的函数声明是不是变成了swap(int a, int b)。4.2 用宏定义SWAP看起来很爽副作用藏得深有人嫌函数调用有开销喜欢用宏#define SWAP(a, b) do { int temp (a); (a) (b); (b) temp; } while (0)这种写法确实能做到“类似泛型”的效果只要传入的变量类型能用int表示都能换。但它有两个非常坑的特性。第一个是参数求值多次的问题比如SWAP(i, j)展开后i可能被执行多次结果完全不可预测。第二个是类型不安全如果a和b是float或者结构体这段宏在编译期就可能产生非法左值。我在实际项目里建议除非你对宏每次展开后的代码做了严格审查否则还是老实写函数。现代编译器基本都会把短小函数内联掉性能上并不吃亏。如果你确实想在C语言里写一个能交换任意类型数据的通用函数比较安全的方式是逐字节交换void swap_mem(void *a, void *b, size_t size) { unsigned char *pa a; unsigned char *pb b; unsigned char temp; for (size_t i 0; i size; i) { temp pa[i]; pa[i] pb[i]; pb[i] temp; } }调用时需要传入指针和字节数比如swap_mem(x, y, sizeof(x))。它能处理结构体、数组以及任意内存块。但要注意它仍然不能交换两个数组名本身因为数组名不是可修改的左值。4.3 数组名、字符串常量、错误指针类型的特殊场景有三个场景特别容易让swap相关代码翻车我单独列一下。第一个是数组名。数组名在表达式里会退化为指向首元素的指针但数组名本身并不是一个能重新赋值的变量。所以你不能写SWAP(arr1, arr2)或者swap(arr1, arr2)来交换两个数组的“整体”。想交换数组内容得用循环逐元素交换或者用上面的swap_mem按字节交换。第二个是字符串常量。char *s hello里的s指向一个字符串字面量这个字面量通常存放在只读区。如果你想把两个字符串指针互换应该交换指针变量本身也就是用二级指针版本swap_ptr而不是去修改字符串内容。反过来如果你在main里声明的是char str[] hello那str是数组内容存放在可写区你可以通过下标或指针循环交换里面的字符做字符串逆序。第三个是指针类型不匹配。swap(int *a, int *b)不能直接用来交换两个double的值。double的二进制位模式比int宽硬转类型会引发未定义行为。正确的做法是使用专门的double版本函数或者用通用swap_mem。编译器给出的“incompatible pointer type”警告就是在提醒你这个问题别忽略。4.4 常见问题速查表我用一张表把上面这些现象收敛起来方便你排查时对照现象可能原因解决方法调用后x和y没变形参是值传递实际传了变量副本改为传地址swap(x, y)交换后其中一个变量变0异或版swap被传入同一地址加if (a ! b)保护或改用临时变量版编译器警告pointer from integer实参传的是int而不是int*调用时补上取地址符交换两个外部指针没变化函数参数只用了int*改的是指针副本改用int**版本调用时传p1, p2数组内容似乎没交换数组名不可重新赋值用循环或swap_mem逐字节交换宏SWAP(i, j)结果混乱宏参数被多次求值用函数替代宏或非常小心地传简单变量这张表在我带新人时贴过无数遍每次都能省下大把排查时间。说到底大部分问题都出自一句话的疑问我到底是在改这个变量还是在改指向它的指针。5. swap在排序、字符串逆序和面试题里的应用聊完了函数本身再看看swap是怎么在真实代码里跑起来的。它最经典的舞台就是排序其次是字符串处理最后还有一些面试题里的变体。5.1 冒泡排序swap复用与提前跳出优化排序算法里swap几乎是万能积木。以冒泡排序为例void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { swap(arr[j], arr[j 1]); swapped 1; } } if (!swapped) { break; } } }这段代码里调用swap时传给它的是arr[j]和arr[j 1]。arr是数组名在函数参数里本质上是int *指针所以arr[j]拿到的是第j个元素的地址完全符合swap的接口要求。加了swapped标记后如果能在一轮扫描里发现整个数组已经有序就提前退出不用再傻傻跑完全部循环。这个优化特别适合对近乎有序的数据排序比如用户列表里只有一两处顺序颠倒的情况。选择排序也类似每一轮找最小元素的下标然后swap(arr[i], arr[min_index])。你会发现只要把swap抽出来排序算法的主体就可以专心处理比较和选择逻辑不用为“怎么交换”分心。5.2 字符串逆序从数组下标到指针的写法字符串逆序是swap思想最常见的一个手写练习题。先看用数组下标的版本#include stdio.h #include string.h void reverse(char *s) { int left 0; int right strlen(s) - 1; while (left right) { char temp s[left]; s[left] s[right]; s[right] temp; left; right--; } }这个交换逻辑和swap完全一致只是被内联在循环里。注意调用处最好用char str[] hello而不是char *str hello因为字符串字面量在只读区直接修改是未定义行为。我在初学阶段就在这上面栽过程序编译通过、运行却直接崩溃后来加了gdb才定位到是改只读内存的问题。还可以写一版纯指针风格void reverse(char *s) { char *left s; char *right s strlen(s) - 1; while (left right) { char temp *left; *left *right; *right-- temp; } }两种写法本质一样只是把下标换成指针移动。理解这两种写法的互换对后续阅读Linux内核或开源代码会有很大帮助因为工程代码里两种风格都大量存在。5.3 不许用临时变量交换两个数面试题里的两个陷阱面试官问“不用临时变量交换两个数”最常见的答案有两个一个是异或法一个是加减法。异或法前面已经提过这里只强调注意事项传入的两个指针不能指向同一块内存否则会清零。加减法是这样void swap(int *a, int *b) { if (a b) { return; } *a *a *b; *b *a - *b; *a *a - *b; }推导起来很直观但风险比异或法还大。当a和b的值都很大的时候*a *b可能超出int能表示的范围有符号整数溢出在C语言里属于未定义行为可能会得到理论上的“交换”结果也可能因为优化器假定永远不会溢出产生完全错误的代码。所以这个写法我只能放到“面试题里的奇技淫巧”这一档工程代码里绝对不碰。面试官如果再追问“能不能不用临时变量交换两个任意类型的数据”答案就简单了不能。因为任何交换本质上都需要一个临时状态来保存旧值异或和加减不过是把这个临时状态隐藏在运算过程里。能把这一点说明白面试官往往反而会加分因为你展示的不是背答案而是理解原理。5.4 链表里的swap指针的指针练习题最后提一个进阶变体在单向链表里交换两个相邻节点。直觉上你只需要改几个节点的next指针根本不用交换节点里的值。但实现起来会发现要修改prev节点的next指向可能还得处理头指针本身。这时候swap_ptr的二级指针思路就派上用场了当你需要“让调用者手里的那个指针也发生变化”时就必须传指针的地址。这个题目我会推荐给已经把基础swap搞熟的人它能验证你到底是真懂了指针还是只是背下了语法。6. 我的一点实际体会swap写得好C语言地基才算稳如果要把前面这些内容浓缩成一句经验我会说swap是C语言里最便宜、最有效的调试指针理解的工具。为什么这么说因为它足够小——变量只有两个函数只有一个调试成本几乎为零。但又足够关键——它将传值调用、指针解引用、栈帧生命周期、类型安全这些问题全部压缩在了一起。你完全可以在几秒钟内写一个错误的swap再用几秒钟把它改对然后花上半小时观察它背后的内存变化这个过程比啃一整章的指针理论都来得扎实。我个人在实际项目里几乎只用临时变量版swap最多加上static inline让编译器把它内联不写异或版也不碰那种看起来很聪明的宏。原因很简单代码是给人读的机器只关心它快不快而人更关心它好不好懂。一个坑可能在三年后被自己踩第二次那时候唯一能救你的就是一眼能看明白的代码而不是一个需要推导五分钟的位运算把戏。所以我在带新人的时候经常让他们做一个练习把交换逻辑封装成函数然后分别尝试传值、传地址、传指针的地址观察三种调用方式下的打印结果。这个练习做完大部分关于“函数能不能修改外部变量”的疑问也就自动消失了。最后再分享一个小技巧如果你在调试的过程中发现某个变量“莫名其妙”没变先别急着加断点试着在调用前后各加一行printf打印变量的值和地址。如果值没变但地址也没变那说明调用时传的可能是副本如果值变了但地址也没变那说明你成功操作了原变量。把“值变不变、地址变不变”这个二维表刻在脑子里你会发现自己排查C语言基础bug的速度能快上一大截。