C语言指针核心应用:传址调用——让函数真正「动手」
2026/8/6 7:06:08 网站建设 项目流程

适用对象:学过函数、指针基础,理解地址概念的同学
本章目标:彻底搞懂「传值调用」为什么换不了两个变量,「传址调用」为什么可以


引入:换房间的故事

先来玩一个思维实验。

场景:你现在在 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)被调用的一瞬间,发生了以下事情:

  1. 创建两个新房间xy,分别从ab那里拷贝了值——x = 10y = 20
  2. swap_wrong函数在xy这两个新房间里倒腾,把值换了过来
  3. 函数结束,xy这两个临时房间被系统回收
  4. 原来的ab房间纹丝不动——因为它们根本没有参与交换

📸内存快照(调用瞬间)

传值调用前: 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)被调用的一瞬间:

  1. &a(a 的地址)被拷贝给px——px这张便签上写着「a 在哪间房」
  2. &b(b 的地址)被拷贝给py——py这张便签上写着「b 在哪间房」
  3. *px = *py拿着px的便签找到 a 的房间,把里面的值改成 b 的值——a = 20
  4. *py = tmp拿着py的便签找到 b 的房间,把里面的值改成 a 的值——b = 10
  5. 函数结束,两个便签(pxpy)被回收,但ab的房间里的值已经被改掉了

📸内存快照(调用过程)

传址调用瞬间: 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;}

答案不能!

这个函数的意图是把pxpy这两张便签对调——但问题是pxpytmp都是局部变量(函数参数)。p = q只是让局部变量px指向了另一个地址,对调用者毫无影响

真正能交换的版本用的是int tmp(普通变量),而不是int *tmp(指针):

voidswap(int*px,int*py){inttmp=*px;// ✅ 用普通变量 tmp 存值*px=*py;*py=tmp;}

本章总结

传值调用 vs 传址调用:

传值调用传址调用
形参类型int xint *px
实参传入值的拷贝地址的拷贝
能修改实参吗
性能(大数据量)差(整块拷贝)好(只传 8 字节)

什么时候必须用传址:

  • 函数需要修改多个外部变量
  • 函数参数是大数组 / 大结构体
  • 需要返回一个以上的值

手写 strlen 的三步:

① assert(str != NULL) → 防守:检查 NULL ② while (*str != '\0') → 循环:不是 \0 就继续 ③ count++ / str++ → 计数:数一个,指针挪一个

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询