C语言数组交换数值的多种实现与避坑指南

📅 发布时间:2026/9/9 10:32:22
C语言数组交换数值的多种实现与避坑指南
在C语言学习里“交换数组数值”算是一道很经典但又坑很多的题目。我给不少人看过代码最常见的现象是数组名赋值编译直接报错或者写了个函数想传数组进去结果跑完原数组纹丝不动。这些问题的根源往往不是“交换”这个动作本身而是对数组、指针、函数传参这三块概念还不够通透。这篇文章就围绕“交换数组数值”这件事从单个变量的交换原理讲到数组整体交换的多种实现再延伸到形参传数组、二维数组行交换和排序里的交换应用最后把新手最容易踩的坑一条条过一遍。适合刚学完指针和数组、准备做练习题或者复习找错的人看也能帮一些工作后偶尔写C的读者捡起细节。1. 交换的起点先搞清楚“交换”到底在交换什么1.1 单变量交换是数组交换的最小单元要交换两个数组最直接的做法就是循环交换两个数组中位置相同的元素。所以数组交换本质上是多次执行“两个变量交换”。别看单变量交换很简单它恰恰是理解数组交换的前提。最常见的写法是借助临时变量int a 3, b 5; int temp; temp a; a b; b temp;三个赋值语句完成两个变量的值交换。为什么需要 temp用一个生活化的类比你手上有两杯饮料一杯可乐一杯雪碧想交换杯子的内容就必须先拿一个空杯把可乐倒进去再把雪碧倒进原可乐杯最后把空杯里的可乐倒进原雪碧杯。这个“空杯”就是 temp。还有另一种“看起来很酷”的写法——异或交换法a a ^ b; b a ^ b; a a ^ b;原理是异或运算的自反性x ^ y ^ y x。第一步让 a 等于 a^b第二步 b a^b (a^b)^b a第三步 a a^b (a^b)^a b。数学上确实能完成交换也不用临时变量看起来省了内存。但我在实际项目中很少推荐这个写法。原因有两个一是可读性差一段代码维护者要先做异或运算推导才知道发生了什么二是它有个致命的前提限制——如果两个操作数指向同一块内存比如a和b是同一个变量或者通过指针交换同一地址的内容第一步*p *p ^ *p就会让该地址的值变成 0后面就全错了。日常编程里“数组相邻两个元素交换”特别容易触发这种别名问题比如冒泡排序里如果传错了下标arr[i]和arr[i]是同一个元素异或交换就会把元素清零。所以老老实实用临时变量是最稳妥的方案。1.2 数组与变量的本质差异为什么不能直接赋值对单变量来说两个 int 变量之间可以直接写a b;但数组就不行。写下arrB arrA;编译器会直接报错原因是数组名在表达式中有特殊的语义它表示的是数组首元素的地址而不是一个可修改的左值。数组名更像是“常量地址”你不能给它赋值就像不能给门牌号本身改成另一个门牌号一样。那“交换数组数值”到底交换什么有两种理解第一种是交换两个数组中对应下标元素的值比如a[0]与b[0]互换、a[1]与b[1]互换直到数组末尾这是“把数组里的每一个值都交换”第二种是交换两个指向数组的指针让指针pa原来指向 a现在指向 b这种只是改变了指针的指向并不会改变数组内部内容。很多人搞混这两个概念写代码时想交换数组内容却只交换了指针导致原数组没变这是最常见的逻辑错误。所以在动手写代码之前先问自己一个问题你要的是“数组里的数值互换”还是“指针换一个目标”本文的主题是前者所以后面讨论的都是逐元素交换和基于这个思路的变体。2. 整个数组交换的两种路线按值换和改指向2.1 逐元素交换最直观也最安全如果两个数组长度相同、类型相同最傻瓜也最可靠的方法是循环每一个下标用临时变量交换对应位置的两个元素#include stdio.h void print_array(int arr[], int len) { for (int i 0; i len; i) { printf(%d , arr[i]); } putchar(\n); } int main() { int a[] {1, 2, 3, 4, 5}; int b[] {10, 20, 30, 40, 50}; int len 5; // 交换前 print_array(a, len); print_array(b, len); // 逐元素交换 for (int i 0; i len; i) { int temp a[i]; a[i] b[i]; b[i] temp; } // 交换后 print_array(a, len); print_array(b, len); return 0; }这段代码的输出是1 2 3 4 5 10 20 30 40 50 10 20 30 40 50 1 2 3 4 5这个做法的时间复杂度是 O(n)空间复杂度是 O(1)不需要额外的数组只需要一个临时变量。它适用于固定长度的数组、动态数组malloc 出来的、结构体数组等几乎所有场景唯一的硬性要求是“两个数组的长度必须相同”。如果长度不同交换无法完全进行你只能决定是交换较短长度的部分还是先报错误终止。2.2 用指针遍历代替下标访问很多人在初学指针后会陷入一个误区以为用指针写就是高级的实际上只是同一件事的两种表达。逐元素交换也可以完全用指针完成int *pa a; int *pb b; for (int i 0; i len; i) { int temp *(pa i); *(pa i) *(pb i); *(pb i) temp; }甚至可以用两个指针直接递增int *pa a; int *pb b; for (int i 0; i len; i) { int temp *pa; *pa *pb; *pb temp; pa; pb; }这两种写法和下标访问a[i]在本质上没有区别因为数组下标运算符[]本身就是通过指针运算实现的a[i]等价于*(a i)。你选择哪种风格取决于团队规范和个人习惯。对我来说可读性优先简单的a[i]写法最常见指针版本通常出现在“需要指针作为函数参数”或者“接口本身就给的是指针”的场景中。2.3 交换指针指向只在真正需要“换目标”时使用如果你并不想改变数组里的数据而是想在某些逻辑里让数组 A 充当原来的 B数组 B 充当原来的 A那就可以交换指针int a[] {1, 2, 3, 4, 5}; int b[] {10, 20, 30, 40, 50}; int *pa a; int *pb b; int *ptemp; ptemp pa; pa pb; pb ptemp;执行完后pa指向的是b数组的首元素pb指向的是a数组的首元素。但两个数组内存里的值一点没变。如果你通过pa[2]访问看到的是原来b[2]的值。这在算法里有应用场景比如两个很大的数组频繁“交换角色”而不想逐个拷贝元素时这种 O(1) 的方式能省不少时间。但是要注意这种做法只对“可修改的指针变量”有效如果函数参数定义成了int arr[]数组名退化成指针函数内部交换形参指针并不能影响实参——这一点下一节会详细说。3. 函数里交换数组数组参数退化的坑与解法3.1 为什么函数内 swap 不起作用如果你把交换逻辑封装成一个函数最容易踩坑的版本是这样的void swap_array(int a[], int b[]) { int temp; for (int i 0; i 5; i) { temp a[i]; a[i] b[i]; b[i] temp; } }调用时int a[5] {1, 2, 3, 4, 5}; int b[5] {10, 20, 30, 40, 50}; swap_array(a, b);这个代码在多数编译器下其实是能正常工作的因为a[]虽然在参数列表里看起来像“传数组”但编译器真正接收到的是一个指针int *a。你通过a[i]修改的是实参数组的地址上的内容也就是直接改了原来的内存所以交换生效。真正的麻烦在另一个地方如果你在函数内写sizeof(a)得到的结果是 864位平台下指针大小不是 205 个 int。这意味着函数内无法通过 sizeof 推算出数组长度。还有一种情况是函数内交换指针本身比如void swap_ptr(int *pa, int *pb) { int *temp pa; pa pb; pb temp; }调用后pa和pb在函数内部确实交换了但它们是形参的副本函数结束后实参指针不会发生任何变化。C 语言的参数传递默认是“值传递”指针变量本身也是一个值传进去的是指针的值不是指针变量的地址。想改变实参指针的指向必须传“指针的指针”void swap_ptr(int **pa, int **pb) { int *temp *pa; *pa *pb; *pb temp; }3.2 正确的函数封装同时传入数组和长度封装数组交换函数时最规范的做法是同时传入两个数组和它们的长度并在函数内做长度检查#include stdio.h void swap_arrays(int a[], int b[], int len) { if (len 0) return; for (int i 0; i len; i) { int temp a[i]; a[i] b[i]; b[i] temp; } } int main() { int a[] {1, 2, 3, 4, 5}; int b[] {6, 7, 8, 9, 10}; int len_a sizeof(a) / sizeof(a[0]); // 5 int len_b sizeof(b) / sizeof(b[0]); // 5 if (len_a ! len_b) { printf(数组长度不一致无法交换\n); return 1; } swap_arrays(a, b, len_a); for (int i 0; i len_a; i) { printf(%d , a[i]); } putchar(\n); for (int i 0; i len_b; i) { printf(%d , b[i]); } putchar(\n); return 0; }注意sizeof(a) / sizeof(a[0])只能在“数组定义所在的代码块”里用不能在函数内部对形参使用原因就是上面说的退化问题。这个公式是 C 语言里计算数组长度的常用技巧建议直接记下来。3.3 用 memcpy 一次交换整块内存除了逐个元素交换C 标准库的memcpy可以整块复制内存这让数组交换多了一种实现先拷贝到临时缓冲区再拷贝再拷回。#include stdio.h #include string.h int main() { int a[] {1, 2, 3, 4, 5}; int b[] {10, 20, 30, 40, 50}; int len sizeof(a) / sizeof(a[0]); int temp[len]; memcpy(temp, a, sizeof(a)); memcpy(a, b, sizeof(b)); memcpy(b, temp, sizeof(temp)); for (int i 0; i len; i) { printf(%d , a[i]); } putchar(\n); for (int i 0; i len; i) { printf(%d , b[i]); } putchar(\n); return 0; }这里需要注意几个点temp的大小必须至少和 a、b 中任意一个一样大否则会越界memcpy的第三个参数是字节数不是元素个数所以传sizeof(a)而不是len如果两个数组的地址区间有重叠应该用memmove而不是memcpy因为memcpy不保证处理重叠情况。对于同类型等长数组交换memcpy写法简洁性能也很好因为库函数通常做了优化。用表格对比三种整数组交换实现实现方式代码复杂度空间开销典型场景for 循环逐元素交换低O(1)基本场景、需要逐元素处理逻辑memcpy 整块交换低需要一个临时数组 O(n)等长大类型数组、纯数据搬运交换指针指向低O(1)只需要改变“逻辑角色”不改变原数据4. 数组内部的交换逆序、相邻交换与排序应用4.1 字符串逆序是数组交换的经典练习数组交换不只发生在两个数组之间也经常发生在同一个数组内部。比如字符串逆序就是典型的“首尾交换向中间推进”#include stdio.h #include string.h void reverse_string(char str[]) { int left 0; int right strlen(str) - 1; while (left right) { char temp str[left]; str[left] str[right]; str[right] temp; left; right--; } } int main() { char s[] hello; reverse_string(s); printf(%s\n, s); // 输出 olleh return 0; }这个过程每一步都是“交换数组里的两个元素”left 指向当前要处理的左侧位置right 指向右侧位置每轮交换后 left 右移、right 左移直到 left right表示所有位置都处理完了。这个题目很多练习平台都有类似 “字符串逆序 c语言 pta” 里的常见变体本质就是一个双指针交换。如果你记得“数组尾巴上的元素是n-1-i”那逆序也可以硬算下标int n strlen(s); for (int i 0; i n / 2; i) { char temp s[i]; s[i] s[n - 1 - i]; s[n - 1 - i] temp; }两种写法都行双指针版本在逻辑上更直观一点尤其适合之后学习快慢指针等算法。4.2 冒泡排序里的元素交换说到数组交换绕不开冒泡排序。冒泡排序的核心操作就是“相邻两个元素比较逆序则交换”。一个最简单的冒泡排序实现#include stdio.h void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } } int main() { int a[] {5, 2, 9, 1, 7}; int n sizeof(a) / sizeof(a[0]); bubble_sort(a, n); for (int i 0; i n; i) { printf(%d , a[i]); } putchar(\n); return 0; }这里swap的对象是arr[j]和arr[j 1]也就是同一个数组里相邻的两个元素。如果把它也封装成一个函数会更清晰void swap_int(int *x, int *y) { int temp *x; *x *y; *y temp; }调用时写swap_int(arr[j], arr[j 1]);。注意这里必须传地址因为swap_int要修改的是实参指针指向的整型变量而不是指针本身。初学者最容易写成swap_int(arr[j], arr[j 1]);编译会报错或警告因为函数期望接收int *却传了int。4.3 二维数组的行交换逐元素和指针两种思路二维数组交换行也是数组交换的延伸。比如有一个int matrix[3][3]想交换第 0 行和第 1 行。第一种做法是逐元素交换for (int j 0; j 3; j) { int temp matrix[0][j]; matrix[0][j] matrix[1][j]; matrix[1][j] temp; }第二种做法是定义两个指向一维数组的指针然后交换指针的指向。指向数组的指针声明为int (*p0)[3] matrix[0]; int (*p1)[3] matrix[1]; int (*ptemp)[3]; ptemp p0; p0 p1; p1 ptemp;这种指向int[3]类型的指针方便在进行需要整行处理的逻辑时直接使用。不过要注意交换指针只是让p0和p1换了目标matrix本身的内存布局没有改变。如果外部代码通过matrix[i][j]访问看到的还是原来的数据。5. 交换字符串与字符数组小心数组越界和指针陷阱5.1 字符数组 vs. 字符串字面量当数组元素是 char 时交换逻辑和 int 数组没有本质区别但有一些字符数组特有的坑。看这段char str1[] abc; char str2[] xyz; for (int i 0; i 3; i) { char temp str1[i]; str1[i] str2[i]; str2[i] temp; }交换三次后str1变成xyzstr2变成abc。如果你直接用char *s1 abc;这种指针形式然后试图通过s1[i]写入就会触发未定义行为因为字符串字面量在很多编译环境下存储在只读数据区想要修改必须用字符数组。所以做字符串内容交换时一定要先确认目标是char[]而不是char *。5.2 交换字符串数组里的字符串指针有一种情况是交换“字符串数组”里的某两个字符串指针比如char *fruits[] {apple, banana, pear, orange}; // 想交换 fruits[0] 和 fruits[1] char *temp fruits[0]; fruits[0] fruits[1]; fruits[1] temp;这交换的是指针也就是字符串的起始地址而不是逐个字符去交换。因为fruits数组中的每个元素本身就是char *类型的变量所以临时变量temp也是char *。在按字典序排序字符串数组时会用到这种操作比较时用strcmp交换时直接交换指针效率比复制整个字符串高很多。这也是“指针数组”在实际应用中的典型场景——你不希望为了排序把大段字符串搬来搬去只移动它们的入口地址就行。6. 避坑总结交换数组时最容易犯的六个错6.1 用a b直接赋值数组数组名不能作为左值被赋值编译会报错error: assignment to expression with array type。解决办法是逐元素循环或memcpy。如果有人拿int *p a; p b;来“证明”数组能赋值那只是指针换目标数组内容没变。6.2 交换完发现原数组没变排查顺序先确认你看的是不是两个指针而不是两个数组本身再确认传参时是否传了地址最后确认函数内是否用swap_int(arr[i], arr[j])这种传值写法而不是swap_int(arr[i], arr[j])。6.3 两个数组长度不一致交换前检查长度。长度不一致时可以选择只交换较短长度也可以直接拒绝执行。不检查长度可能导致两个问题短的数组越界访问了相邻内存或者一部分元素没有被交换。我曾经调试过一个项目两个数组逻辑上都是 10 个元素其中一个被反复重用后长度变成 8结果交换时把另一个数组的边界外内存给覆盖了最后定位非常费劲。6.4 异或交换同一元素导致清零如果交换的两个“变量”实际上是同一个内存位置异或交换会让该位置变成 0。即使不用异或使用临时变量交换同一个元素也不会出错这是临时变量法的另一个安全优势。如果你的代码里同时存在“按值交换”和“按指针交换”尤其要注意是否出现别名重叠。6.5 函数内sizeof(arr)算不出长度数组参数在函数内退化为指针sizeof(arr)得到 8 或 4而不是整个数组的字节数。在函数内想要长度信息必须在调用时把长度作为参数传进去。数组定义处的sizeof(arr) / sizeof(arr[0])只能用在定义它的代码块里。还有一个变体是使用变长数组VLA参数void f(int n, int arr[n])这里arr仍然是指针但sizeof行为因编译器而异为了可移植性最好还是显式传 n。6.6memcpy重叠区域未处理当交换的两个内存区域有重叠时memcpy的结果是未定义的需要用memmove。数组之间如果保证是两段完全不重叠的内存则用哪个都可以memcpy通常更快。如果你在写“把数组某一段顺移到另一段”这种交换时直接使用memmove更省心。7. 调试数组交换问题时的实用技巧7.1 小数组 打印是最快的验证方式在平时的练习题里数组长度通常不会很大直接在交换前后各打印一次数组内容是最快的验证方式。打印时注意写清楚是哪个数组、哪个阶段避免输出混在一起。更好的方式是把打印也封装成函数void print_int_array(const int arr[], int n) { for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); }需要检查“是不是交换了某个中间值”时可以在循环里加打印for (int i 0; i n; i) { printf(交换第 %d 位: a[%d]%d, b[%d]%d\n, i 1, i, a[i], i, b[i]); }7.2 用最小复现法排查越界当你发现交换后某个数组出现异常大数或随机值时怀疑的重点应该是越界。把问题简化成“只交换 2 个元素的数组”再逐步加长能快速判断问题是否和下标计算有关。另一个办法是故意在数组前后放“哨兵变量”如果交换后哨兵值变了说明越界写入了。7.3 用断言保护交换的前提条件在开发阶段可以在交换函数开头加上断言或防御性检查void swap_arrays(int a[], int b[], int len) { if (len 0) return; // 如果 a 或 b 是空指针直接返回或报错 if (a NULL || b NULL) { fprintf(stderr, 输入数组指针为空\n); return; } for (int i 0; i len; i) { int temp a[i]; a[i] b[i]; b[i] temp; } }虽然加了不少判断会让代码看起来没那么“精简”但在一份被反复调用的工具代码里这些防护能省下很多排查时间。7.4 区分“数组地址”和“元素地址”的打印方式调试时如果用%p打印printf(%p, a)和printf(%p, a[0])打印出的数值通常一样但语义不同前者是数组首元素的地址a是整个数组的地址虽然数值相同但类型不同指针运算加 1 的跨度不同。不要把三者的打印值相同当成“它们完全等价”。理解了这层区别再去看二维数组、函数指针等概念时会顺畅很多。数组交换这个问题看起来是几行代码的事但牵扯的东西比想象中多值传递和地址传递的概念、数组名的指针语义、函数形参退化的机制、memcpy 内存操作的安全边界以及指针数组和字符串字面量的区别。把这些基础点打通之后不只是“交换数组数值”这一道题很多数组相关的问题都会变得顺理成章。如果后面再遇到“树状数组上二分”“二维数组怎么传参”这类进阶问题你会发现它们的底层也在反复使用这些基础概念。