刚学C语言指针的时候,几乎所有人都会遇到这么一道题:编写void swap(float *px, float *py),交换两个实型变量的值,然后在主函数里验证。这个题目看着基础,信息量却一点都不小——函数名、参数列表、返回值类型、调用方式,每个细节都对应一个考点。从我带人的经验来看,很多人卡住的地方不在“交换”本身,而在“为什么形参是两个指针”“主函数怎么传参才叫验证成功”这些问题上。这篇文章逐个拆开讲,再附上可以直接跑的完整代码和调试心得。适合刚学完指针的初学者,也适合想把这个经典点彻底搞明白的开发者。
1. 先把题目拆开读:float *px 到底在声明什么
拿到题目第一步,不是急着写代码,而是把函数声明void swap(float *px, float *py)逐词拆开。这一行字符串里包含的信息,足够你回答面试官关于指针和函数传参的一串追问。
1.1 形参 float *px 的读法与真正含义
float *px的正确读法是:px 是一个指针变量,它存储的是一个内存地址,这个地址里存放着一个 float 类型的数据。注意,px 本身存的是地址,而不是浮点数本身。要拿到这个地址指向的数据,必须使用间接访问运算符*,也就是*px。
我经常用一个类比来解释这个概念:px 是门牌号,*px才是房间里住的人。你想修改房间里的人,必须拿着门牌号走进去改,而不是把房间里的人拉出来复印一份,在复印件上改。实型变量在内存中占 4 个字节,存放的是 IEEE 754 单精度浮点格式的数据(符号位、指数位、尾数位),和整数的补码表示完全不同。很多人觉得“拷贝一份值然后再赋回去”也能实现交换,但这里的关键不是数据长什么样,而是函数里修改的是副本还是原件。
1.2 void 返回值:一个容易被忽略的考点
题目要求返回类型是void,这个设计是有讲究的。swap 这个函数的目的是在函数内部直接修改调用者定义的变量,这种效果叫“副作用”,它通过指针完成,因此不需要再返回任何值。如果想把 swap 设计成有返回值的函数,除非你用某种技巧把两个数打包返回,否则只可能返回一个数——而“同时返回两个交换后的数”这件事本身就不是 C 语言普通函数能直接做到的。
所以,void在这里不是“随便写写”,而是提醒你:这个函数的工作方式是原地修改,不是返回结果。这也顺带解释了为什么函数内部可以用return;提前结束,但不能return 某个值;——类型是空的,就只能空着回。
2. 指针传参背后的机制:为什么值传递救不了 swap
理解了声明之后,下一个必须想清楚的问题是:为什么参数非得是float *px和float *py,直接用float x, float y不行吗?这个问题的答案,就是整个题目的核心考点。
2.1 函数参数传递时到底发生了什么
C 语言的函数参数传递方式只有一个:值传递。调用swap(a, b)时,系统会把 a 和 b 的数值复制一份,放进这个函数栈帧里的形参 x 和 y 中。从此,x 和 y 就是两个全新的存储单元,它们和 main 函数里的 a、b 没有任何血缘关系。你在 swap 里把 x 和 y 交换得再漂亮,回到 main 函数之后,a 和 b 依然纹丝不动。
做个演示就特别直观。假如写出下面这个错误的版本:
void swap_wrong(float x, float y) { float temp = x; x = y; y = temp; }然后在主函数里调用:
float a = 3.14f, b = 2.71f; swap_wrong(a, b); printf("a = %f, b = %f\n", a, b);输出结果依然会是a = 3.14, b = 2.71。如果在 swap_wrong 内部加一行 printf,你会发现函数里 x 和 y 确实悄悄交换了,但这仅仅是“复印件”之间的交换。这就是值传递的本质——你对形参做的所有操作,都不会反馈到实参上。
2.2 地址传递究竟传递了什么
当函数参数换成float *px后,调用时写成swap(&a, &b)。这里的&a是取变量 a 的地址,形参 px 拿到的是 a 的门牌号。此时,px 变量本身的存储单元依然是复制的,但 px 里保存的“门牌号”却和 a 是同一个地址。通过*px,函数可以直接找到那个地址,修改里面的数据。这就是间接寻址,也是指针传参能“改到外面”的原因。
这里有一个特别容易误解的细节:指针变量本身在函数间传递时也是值传递!也就是说,形参 px 和实参 &a 不是同一个变量,你如果写px = py,只是让 px 这个指针变量指向 py 指向的位置,并没有改变任何 float 变量本身。只有带着*的写操作,比如*px = *py,才能真正落到实参变量的内存上。把下面的对比记住,基本就出不了大问题:
| 传递方式 | 函数内修改形参 | 是否影响实参 | 本质原因 |
|---|---|---|---|
| 传值 float x | x = 5.0f | 不影响 | 形参是实参的副本 |
| 传指针 float *px | *px = 5.0f | 影响 | *px 访问的是实参的内存地址 |
| 传指针 float *px | px = 其他指针 | 不影响 | 修改的是指针形参自己,不是它指向的数据 |
3. 完整代码与逐行解读:直接能跑的验证程序
理解归理解,代码还是要一行一行落到实处。下面就是我建议初学者完整敲一遍的参考实现。
3.1 完整参考代码
#include <stdio.h> void swap(float *px, float *py); int main(void) { float a = 3.14f, b = 2.71f; printf("交换前:a = %f, b = %f\n", a, b); swap(&a, &b); printf("交换后:a = %f, b = %f\n", a, b); return 0; } void swap(float *px, float *py) { float temp; temp = *px; *px = *py; *py = temp; }把代码放进任意一个 C 语言编译环境(GCC、Clang、VS 或者在线编译器都可以),编译运行,你会看到:
交换前:a = 3.140000, b = 2.710000 交换后:a = 2.710000, b = 3.140000到这一步,题目要求的主函数验证就算完成了。
3.2 函数体三行代码的逐句拆解
float temp;这行声明了一个临时变量,它的类型必须是 float,不是指针。为什么?因为我们要拿来暂存*px的值——也就是一个实型数据本身。
temp = *px; // 第一步:把 px 指向的地址里的 float 值,复制到 temp 中 *px = *py; // 第二步:把 py 指向的地址里的 float 值,写入 px 指向的地址 *py = temp; // 第三步:把 temp 中保存的原 *px 值,写入 py 指向的地址这三步就是经典的“三杯水交换法”:两杯水要互换,必须有一个空杯做中转。这里 temp 就是空杯。如果你省掉 temp,直接写:
*px = *py; *py = *px;那么第一步执行完,px 指向的地址已经被覆盖成原来的*py值,原来的*px数据已经丢了。第二步执行时,*py再拿到的还是原来的*py,结果就是两个变量都变成了原来 py 的值,交换失败。这种“覆盖后丢失”的错误,是初学指针时最经典的翻车案例。
3.3 主函数验证到底在验证什么
很多人觉得验证就是把程序跑一遍,看到“交换前后”两行输出就成了。其实验证的核心是“比较”:交换后的 a 是否等于交换前的 b,交换后的 b 是否等于交换前的 a。如果只是肉眼扫一眼输出,万一你选的数不够明显,比如 1.03 和 1.02,用%.2f打印出来几乎看不出变化,验证就失去了意义。
更稳妥的做法是把验证写进代码里,让程序自己判断:
float old_a = a; float old_b = b; swap(&a, &b); if (a == old_b && b == old_a) { printf("验证成功:a = %f, b = %f\n", a, b); } else { printf("验证失败\n"); }这里直接用==做 float 比较是安全的,因为 a 的值是从 b 原样拷贝过来的,不是经过复杂运算产生的新浮点结果,不涉及精度误差问题。这个验证方式比单纯打印更严谨,能让你一眼看出 swap 到底有没有生效。
4. 现场排错实录:新手最常见的五种翻车方式
代码本身不长,但新手写起来,翻车姿势多到超出想象。下面这些错误,我见过太多次,每一个都是真实调试现场的血泪经验。
4.1 错误一:调用时忘记写 &,直接 swap(a, b)
这是出现频率最高的问题。函数要求的是指针,你传了一个 float 变量进去。编译器会报警告或者错误,有些编译器会提示makes pointer from integer without a cast。如果你强行编译运行,程序大概率崩溃——因为函数拿到 a 的数值后,会把 3.14 的浮点编码当成一个内存地址去访问,这个地址十有八九是非法地址,直接段错误。
修正方式只有一个:swap(&a, &b)。取地址运算符&是这一刻的主角。
4.2 错误二:把临时变量声明成了指针
有人知道要定义临时变量,但写成了:
void swap(float *px, float *py) { float *temp; *temp = *px; // ... }temp 被声明成指针,却没有被初始化。它根本不知道指向哪里,*temp = *px就是在向一个随机地址写入数据。这类野指针问题在 C 语言里极其危险,轻则数据被莫名篡改,重则程序直接崩溃。正确写法是float temp;——你需要的只是一个装 float 数据的盒子,不是一个门牌号。
4.3 错误三:把 *px 和 px 混为一谈
在函数体内写px = py,而不是*px = *py,这是概念性错误。px = py的意思是让 px 这个指针变量存储 py 保存的地址,结果只是两个指针指向了同一个位置,float 变量的内存内容完全没动。运行之后,main 函数里的 a、b 还是原来的值,你还会百思不得其解:明明函数执行了,为什么没效果?
排查技巧:在 swap 里临时打印px、py、*px、*py这四个值。打印*px能看到数据本身,打印px能看到地址。如果你发现函数执行前后,*px的值没变,而 px 的地址变了,那基本就是写了px = py这类代码。
4.4 错误四:只用一个 printf 看结果,且数据选得不直观
有人主函数写成:
printf("交换后:a = %f, b = %f\n", a, b);但没有在主函数里写“交换前”的输出,或者两个数选得特别接近,比如 1.01 和 1.02。这样即使交换成功,你肉眼也未必能分辨。建议选差异明显的数值做验证,而且一定要在调用前后都打印。前面提到的 if 判断验证法,也建议直接加进去。
4.5 错误五:想用位运算或加减法交换 float
整数变量可以用异或交换,这个技巧不少同学从书上见过。于是有人想当然地用异或去交换 float:
*px = *px ^ *py; *py = *px ^ *py; *px = *px ^ *py;这在 float 上是行不通的。异或是按二进制位运算,float 的内存布局是 IEEE 754 标准,符号位、指数位、尾数位都不在参与异或运算后保证能变回合法浮点数。同样,加减法交换也存在浮点精度风险,比如大数加小数后可能丢失精度。所以在 swap 的标准实现里,老老实实用 float temp 中转是最可靠、最不易出错的方案。
5. 扩展思路:从交换两个数到操作数组元素
这道题的边界只是“两个实型变量”,但真正读懂它的人,会立刻想到它在真实程序里的泛化场景:数组元素交换、排序算法、多值返回等。
5.1 把 swap 用到数组中,排序的基础就有了
如果有一个 float 数组float arr[5] = {5.0f, 3.0f, 4.0f, 1.0f, 2.0f};,想交换下标 i 和下标 j 的两个元素,只需要这样调用:
swap(&arr[i], &arr[j]);因为arr[i]是一个 float 类型的左值,对它取地址得到的类型正好是float *,和 swap 的参数完全匹配。冒泡排序、选择排序里最核心的“交换元素”动作,底层就是这样一个 swap 函数。很多排序算法的完整代码中,单独写一个 swap 辅助函数,就是因为这个操作太通用、太常用了。
// 以冒泡排序为例的简单片段 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4 - i; j++) { if (arr[j] > arr[j + 1]) { swap(&arr[j], &arr[j + 1]); } } }5.2 指针移动时的隐蔽陷阱:别把地址和数据搞混
在数组中用指针遍历并交换连续元素时,有个特别容易犯的错。假设你写循环的时候图省事,让一个指针p从数组头开始走,每次交换swap(p, p + 1)后执行p += 2,这里的p + 1不是地址加 1 字节,而是加一个float类型的宽度,也就是 4 字节。如果不理解这一点,交换对象会错位,数据越跳越乱。
另外继续强调一次:写成swap(arr[i], arr[i + 1])是错的,arr[i]是数据本身,不是地址。写成swap(&arr[i], &arr[i + 1])才是正解。调错的时候,错误可能只表现为“数组数据没变化”,但排查起来会非常耗时间。
5.3 给 swap 加一道防线:空指针检查
工程实践中,函数接收外部传入的指针时,第一件事通常是检查指针是否为空。swap 也可以加:
void swap(float *px, float *py) { if (px == NULL || py == NULL) { return; } float temp = *px; *px = *py; *py = temp; }这样即使调用方误传了空指针,函数也只是静默返回,不会造成段错误。不过需要说明,教学题目中往往不要求也不建议额外加这种检查,因为题目隐含的假设是“传入的指针一定是有效实参的地址”。但从工程代码的安全性角度看,这是一个值得养成的习惯。你也可以思考一个问题:加了检查之后,如果同时有人传了px == py,swap 会怎样?答案是函数正常运行,temp = *px后*px = *py和*py = temp都是写同一个地址,数据不变,这也是一个边界情况的讨论。
最后再分享一点我的个人体会:这类基础题目,光看懂不算会,一定要亲手编译运行一遍,再故意把代码改成错误版本,观察错误现象。有一次我给 A 同学演示,故意把&a, &b改成a, b,程序直接崩溃,那一刻他对“指针参数需要取地址”的理解,比看十遍书都深刻。多出几次错,指针这点事就真正长在身上了。