适用对象:学过函数、指针基础,理解地址概念的同学
本章目标:彻底搞懂「传值调用」为什么换不了两个变量,「传址调用」为什么可以
引入:换房间的故事
先来玩一个思维实验。
场景:你现在在 23 楼 A 房间,你朋友在 25 楼 B 房间。你想让你们俩换房间住。
传值调用(错误方式):你给酒店前台打电话,说「把我房间里的东西和 25 楼 B 换一下」。前台派了两个新服务员去你的房间和朋友的房间,把两边的家具互换了一下,然后走了。但你呢?你还是待在原来的 23 楼 A 房间,你人没动,动的只是房间里临时出现又消失的两套家具。
传址调用(正确方式):你给前台打电话,说「把我的房间和朋友的房间对调,前台把我们的房卡换一下」。前台收了你的房卡和朋友的手续,把两张房卡上的楼层号对调——你现在真的住进了 25 楼。
这个故事,就是传值调用和传址调用的本质区别。
第一部分:传值调用——换了个寂寞
1.1 交换两个变量的值(失败版)
#include<stdio.h>// 传值调用版本:x 和 y 是 a、b 的副本voidswap_wrong(intx,inty){inttmp=x;x=y;y=tmp;}intmain(){inta=10;intb=20;printf("交换前:a = %d, b = %d\n",a,b);swap_wrong(a,b);// 调用后,a 和 b 变了吗?printf("交换后:a = %d, b = %d\n",a,b);return0;}输出:
交换前:a = 10, b = 20 交换后:a = 10, b = 20 // 没有任何变化!1.2 为什么会失败?
在swap_wrong(a, b)被调用的一瞬间,发生了以下事情:
- 创建两个新房间
x和y,分别从a和b那里拷贝了值——x = 10,y = 20 swap_wrong函数在x和y这两个新房间里倒腾,把值换了过来- 函数结束,
x和y这两个临时房间被系统回收 - 原来的
a和b房间纹丝不动——因为它们根本没有参与交换
📸内存快照(调用瞬间):
传值调用前: a 的房间: [10] b 的房间: [20] 调用瞬间(创建副本): x 的房间: [10] ← a 的副本 y 的房间: [20] ← b 的副本 a 的房间: [10] ← 原版,丝毫未动 b 的房间: [20] ← 原版,丝毫未动 函数返回,x、y 被回收: a: [10] ← 还是原来那个 a b: [20] ← 还是原来那个 b
⚠️提醒:传值调用的核心问题是副本隔离——形参是实参的复制品,对副本做任何操作都不影响原版。在 C 语言里,所有基本类型(int、char、float 等)默认都是传值调用。
第二部分:传址调用——让函数直接操作原房间
2.1 交换两个变量的值(成功版)
#include<stdio.h>// 传址调用版本:px 和 py 存的是 a 和 b 的地址voidswap_right(int*px,int*py){inttmp=*px;// tmp = px 便签指向的房间里的值 = 10*px=*py;// px 便签指向的房间里的值 = 20*py=tmp;// py 便签指向的房间里的值 = 10}intmain(){inta=10;intb=20;printf("交换前:a = %d, b = %d\n",a,b);swap_right(&a,&b);// 传入 a 和 b 的地址printf("交换后:a = %d, b = %d\n",a,b);return0;}输出:
交换前:a = 10, b = 20 交换后:a = 20, b = 10 // 成功!2.2 为什么会成功?
在swap_right(&a, &b)被调用的一瞬间:
&a(a 的地址)被拷贝给px——px这张便签上写着「a 在哪间房」&b(b 的地址)被拷贝给py——py这张便签上写着「b 在哪间房」*px = *py:拿着px的便签找到 a 的房间,把里面的值改成 b 的值——a = 20*py = tmp:拿着py的便签找到 b 的房间,把里面的值改成 a 的值——b = 10- 函数结束,两个便签(
px、py)被回收,但a和b的房间里的值已经被改掉了
📸内存快照(调用过程):
传址调用瞬间: a 的房间: [10] ←──────────────┐ b 的房间: [20] ←──────────────┐│ px ──────→ [10] a 的副本(便签指向) py ──────→ [20] b 的副本(便签指向) *px = *py 执行后: a 的房间: [20] ← py 便签改了这里 b 的房间: [20] ← 临时 tmp = 20 *py = tmp 执行后: a 的房间: [20] b 的房间: [10] ← tmp = 10,写进了 b 的房间
2.3 传值 vs 传址——对比总结
| 对比维度 | 传值调用 | 传址调用 |
|---|---|---|
| 传入函数的 | 变量的副本(值拷贝) | 变量的地址 |
| 形参是什么 | 普通变量int x | 指针变量int *px |
| 能修改实参吗 | ❌ 不能 | ✅ 能 |
| 对内存的影响 | 只在函数内部有效 | 直接修改原房间 |
| 传入的是什么楼层号 | 无楼层号(副本不是房间) | 真正的楼层号(地址) |
🔑一句话总结:传值调用把房间里的东西(值)抄了一份给函数;传址调用把楼层号(地址)告诉函数,让函数自己找过去。
第三部分:传址调用的典型应用场景
3.1 在函数内部修改多个返回值
函数只能有一个返回值(return只能写一个),但如果需要函数一次返回多个结果呢?用传址调用!
#include<stdio.h>// 一次返回两个计算结果(求和 + 求积)voidcalc(inta,intb,int*p_sum,int*p_product){*p_sum=a+b;// 把和写入 p_sum 指向的房间*p_product=a*b;// 把积写入 p_product 指向的房间}intmain(){intx=3,y=5;intsum=0,product=0;calc(x,y,&sum,&product);printf("和 = %d, 积 = %d\n",sum,product);// 和 = 8, 积 = 15return0;}💡技巧:如果你发现一个函数需要「返回」两个以上的值,首先想到传址调用——让调用者提供「房间」,函数负责「写入」。
3.2 判断两个数的大小并返回最大值
#include<stdio.h>// 返回两个数中较大的,同时把较小的存到 min 指向的位置intmax_min(inta,intb,int*p_min){*p_min=(a<b)?a:b;// 把较小的值写入 p_min 指向的房间return(a>b)?a:b;// 较大的值通过 return 返回}intmain(){inta=8,b=13;intmin_val;intmax_val=max_min(a,b,&min_val);printf("最大值 = %d, 最小值 = %d\n",max_val,min_val);// 输出:最大值 = 13, 最小值 = 8return0;}3.3 让函数内部访问大型数据结构(避免拷贝)
如果你有一个超大的数组,直接传值调用会把整个数组拷贝一份,效率极低。传地址只传 8 字节(64位系统),效率高得多:
#include<stdio.h>voidprint_big_array(int*arr,intlen)// 传地址,只拷贝 8 字节{for(inti=0;i<len;i++)printf("%d ",arr[i]);}intmain(){intbig[1000000];// 400 万字节的数组// ... 初始化 big 数组print_big_array(big,1000000);// ✅ 只传地址,不拷贝整片内存return0;}⚠️提醒:传地址比传值在大数据量场景下有压倒性的性能优势。另外,如果函数的参数是数组,永远传地址——因为数组名本身就是首元素地址,传给形参时会发生退化(详情见数组与指针章节)。
第四部分:手写 strlen——传址的实际应用
4.1 strlen 是什么?
strlen()是 C 语言标准库函数,专门求字符串的长度——从字符串开头数起,遇到第一个\0(字符串结束标志)之前,一共有多少个字符。
#include<string.h>intmain(){intlen=strlen("hello");printf("%d",len);// 输出:5return0;}4.2 手写 strlen 的思路
核心思路:从字符串起始地址出发,一个字符一个字符往后数,数到\0就停。
怎么判断「是不是\0」?用while (*str != '\0')。
#include<stdio.h>#include<assert.h>// my_strlen:求字符串长度intmy_strlen(constchar*str)// const:防止函数内部意外修改字符串{assert(str!=NULL);// 安检门:str 绝不能是 NULLintcount=0;while(*str!='\0'){// 当前字符不是结束标志,就继续数count++;str++;// 指针往后挪一个字符}returncount;}intmain(){printf("%d\n",my_strlen("hello"));// 5printf("%d\n",my_strlen(""));// 0(空字符串)printf("%d\n",my_strlen("hi\0world"));// 2(遇到 \0 就停了)return0;}4.3 strlen 手写版的两个关键点
关键点一:const 保护参数
intmy_strlen(constchar*str)加了const之后,如果函数里不小心写了*str = 'X',编译器会报错——防止你把调用者传进来的字符串给改了。
关键点二:assert 守卫入口
assert(str!=NULL);如果调用者手滑写了my_strlen(NULL),没有 assert 的话程序会直接崩溃——而且崩溃位置可能在 strlen 内部的任意一行。有了 assert,崩溃位置精确在第几行,一眼定位问题。
⚠️提醒:
assert()只在调试版本(Debug)里生效。在发布版本(Release)里,assert会被编译器直接删除,不影响性能。所以 assert 是专门给开发者用的工具,不是给最终用户的保护机制。
第五部分:经典面试题精讲
面试题一:传值和传址的根本区别是什么?
标准答案:
- 传值调用:把实参的值拷贝一份给形参,形参是独立的新变量。对形参的修改不影响实参。
- 传址调用:把实参的地址拷贝一份给形参(指针),通过这个地址直接访问原变量。对形参指向内容的修改直接影响原变量。
面试加分回答:
传值调用的本质是副本隔离——就像你复印了一份文件给别人,别人在复印件上画再多东西,原件纹丝不动。传址调用的本质是共享坐标——你把房间的楼层号告诉了别人,别人可以直接进那个房间改东西,改完你回来发现房间确实变了。
面试题二:下面的代码输出什么?
#include<stdio.h>voidf(int*p,int*q){p=q;// p 指向了 q 指向的房间*p=*q+2;// p 指向的房间的值 = q 指向的值 + 2}intmain(){inta=1,b=2;f(&a,&b);printf("%d %d",a,b);// 输出什么?return0;}逐步推演:
初始:a = 1, b = 2 f(&a, &b) 被调用: px = &a, py = &b (px 指向 a,py 指向 b) p = q: px = py (px 现在也指向 b 了,px 和 py 指向同一个房间 b) *p = *q + 2: *px = *py + 2 即:b 的房间 = b 的值 + 2 = 2 + 2 = 4 函数返回: a = 1 (a 从未被修改) b = 4 (b 被修改了) 输出:1 4答案:1 4
⚠️提醒:这里
p = q只改变了指针变量p本身(它存的地址),并没有改变a的值。但*p = *q + 2通过q的地址直接修改了b,所以b变了。面试官经常在这个地方设坑,问「p = q会不会影响a」——不会,因为只改了p这个便签的指向,没有改a所在的房间。
面试题三:下面这个函数能交换两个数吗?为什么?
voidswap(int*px,int*py){int*tmp=px;px=py;py=tmp;}答案:不能!
这个函数的意图是把px和py这两张便签对调——但问题是px、py、tmp都是局部变量(函数参数)。p = q只是让局部变量px指向了另一个地址,对调用者毫无影响。
真正能交换的版本用的是int tmp(普通变量),而不是int *tmp(指针):
voidswap(int*px,int*py){inttmp=*px;// ✅ 用普通变量 tmp 存值*px=*py;*py=tmp;}本章总结
传值调用 vs 传址调用:
| 传值调用 | 传址调用 | |
|---|---|---|
| 形参类型 | int x | int *px |
| 实参传入 | 值的拷贝 | 地址的拷贝 |
| 能修改实参吗 | ❌ | ✅ |
| 性能(大数据量) | 差(整块拷贝) | 好(只传 8 字节) |
什么时候必须用传址:
- 函数需要修改多个外部变量
- 函数参数是大数组 / 大结构体
- 需要返回一个以上的值
手写 strlen 的三步:
① assert(str != NULL) → 防守:检查 NULL ② while (*str != '\0') → 循环:不是 \0 就继续 ③ count++ / str++ → 计数:数一个,指针挪一个