
适用对象学过函数、指针基础理解地址概念的同学本章目标彻底搞懂「传值调用」为什么换不了两个变量「传址调用」为什么可以引入换房间的故事先来玩一个思维实验。场景你现在在 23 楼 A 房间你朋友在 25 楼 B 房间。你想让你们俩换房间住。传值调用错误方式你给酒店前台打电话说「把我房间里的东西和 25 楼 B 换一下」。前台派了两个新服务员去你的房间和朋友的房间把两边的家具互换了一下然后走了。但你呢你还是待在原来的 23 楼 A 房间你人没动动的只是房间里临时出现又消失的两套家具。传址调用正确方式你给前台打电话说「把我的房间和朋友的房间对调前台把我们的房卡换一下」。前台收了你的房卡和朋友的手续把两张房卡上的楼层号对调——你现在真的住进了 25 楼。这个故事就是传值调用和传址调用的本质区别。第一部分传值调用——换了个寂寞1.1 交换两个变量的值失败版#includestdio.h// 传值调用版本x 和 y 是 a、b 的副本voidswap_wrong(intx,inty){inttmpx;xy;ytmp;}intmain(){inta10;intb20;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 10y 20swap_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 交换两个变量的值成功版#includestdio.h// 传址调用版本px 和 py 存的是 a 和 b 的地址voidswap_right(int*px,int*py){inttmp*px;// tmp px 便签指向的房间里的值 10*px*py;// px 便签指向的房间里的值 20*pytmp;// py 便签指向的房间里的值 10}intmain(){inta10;intb20;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)被调用的一瞬间aa 的地址被拷贝给px——px这张便签上写着「a 在哪间房」bb 的地址被拷贝给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只能写一个但如果需要函数一次返回多个结果呢用传址调用#includestdio.h// 一次返回两个计算结果求和 求积voidcalc(inta,intb,int*p_sum,int*p_product){*p_sumab;// 把和写入 p_sum 指向的房间*p_producta*b;// 把积写入 p_product 指向的房间}intmain(){intx3,y5;intsum0,product0;calc(x,y,sum,product);printf(和 %d, 积 %d\n,sum,product);// 和 8, 积 15return0;}技巧如果你发现一个函数需要「返回」两个以上的值首先想到传址调用——让调用者提供「房间」函数负责「写入」。3.2 判断两个数的大小并返回最大值#includestdio.h// 返回两个数中较大的同时把较小的存到 min 指向的位置intmax_min(inta,intb,int*p_min){*p_min(ab)?a:b;// 把较小的值写入 p_min 指向的房间return(ab)?a:b;// 较大的值通过 return 返回}intmain(){inta8,b13;intmin_val;intmax_valmax_min(a,b,min_val);printf(最大值 %d, 最小值 %d\n,max_val,min_val);// 输出最大值 13, 最小值 8return0;}3.3 让函数内部访问大型数据结构避免拷贝如果你有一个超大的数组直接传值调用会把整个数组拷贝一份效率极低。传地址只传 8 字节64位系统效率高得多#includestdio.hvoidprint_big_array(int*arr,intlen)// 传地址只拷贝 8 字节{for(inti0;ilen;i)printf(%d ,arr[i]);}intmain(){intbig[1000000];// 400 万字节的数组// ... 初始化 big 数组print_big_array(big,1000000);// ✅ 只传地址不拷贝整片内存return0;}⚠️提醒传地址比传值在大数据量场景下有压倒性的性能优势。另外如果函数的参数是数组永远传地址——因为数组名本身就是首元素地址传给形参时会发生退化详情见数组与指针章节。第四部分手写 strlen——传址的实际应用4.1 strlen 是什么strlen()是 C 语言标准库函数专门求字符串的长度——从字符串开头数起遇到第一个\0字符串结束标志之前一共有多少个字符。#includestring.hintmain(){intlenstrlen(hello);printf(%d,len);// 输出5return0;}4.2 手写 strlen 的思路核心思路从字符串起始地址出发一个字符一个字符往后数数到\0就停。怎么判断「是不是\0」用while (*str ! \0)。#includestdio.h#includeassert.h// my_strlen求字符串长度intmy_strlen(constchar*str)// const防止函数内部意外修改字符串{assert(str!NULL);// 安检门str 绝不能是 NULLintcount0;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 是专门给开发者用的工具不是给最终用户的保护机制。第五部分经典面试题精讲面试题一传值和传址的根本区别是什么标准答案传值调用把实参的值拷贝一份给形参形参是独立的新变量。对形参的修改不影响实参。传址调用把实参的地址拷贝一份给形参指针通过这个地址直接访问原变量。对形参指向内容的修改直接影响原变量。面试加分回答传值调用的本质是副本隔离——就像你复印了一份文件给别人别人在复印件上画再多东西原件纹丝不动。传址调用的本质是共享坐标——你把房间的楼层号告诉了别人别人可以直接进那个房间改东西改完你回来发现房间确实变了。面试题二下面的代码输出什么#includestdio.hvoidf(int*p,int*q){pq;// p 指向了 q 指向的房间*p*q2;// p 指向的房间的值 q 指向的值 2}intmain(){inta1,b2;f(a,b);printf(%d %d,a,b);// 输出什么return0;}逐步推演初始a 1, b 2 f(a, b) 被调用 px a, py b px 指向 apy 指向 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*tmppx;pxpy;pytmp;}答案不能这个函数的意图是把px和py这两张便签对调——但问题是px、py、tmp都是局部变量函数参数。p q只是让局部变量px指向了另一个地址对调用者毫无影响。真正能交换的版本用的是int tmp普通变量而不是int *tmp指针voidswap(int*px,int*py){inttmp*px;// ✅ 用普通变量 tmp 存值*px*py;*pytmp;}本章总结传值调用 vs 传址调用传值调用传址调用形参类型int xint *px实参传入值的拷贝地址的拷贝能修改实参吗❌✅性能大数据量差整块拷贝好只传 8 字节什么时候必须用传址函数需要修改多个外部变量函数参数是大数组 / 大结构体需要返回一个以上的值手写 strlen 的三步① assert(str ! NULL) → 防守检查 NULL ② while (*str ! \0) → 循环不是 \0 就继续 ③ count / str → 计数数一个指针挪一个