13、函数(二):值传递与地址传递、局部变量与全局变量、变量的作用域与生命周期
好,咱们接着聊函数。上一章我们把函数的定义、声明、调用这些基本功捋了一遍。这一章要聊的,是C语言里特别容易让人栽跟头的几个概念——值传递与地址传递、局部变量与全局变量,还有作用域和生命周期。
说实话,我当年刚学C的时候,这几个概念绕了我好一阵子。尤其是值传递和地址传递,光看书总觉得差不多,一写代码就翻车。后来在项目里调试一个诡异的bug,折腾了两天才发现是传参方式搞错了……嗯,从那以后我就再也不敢小看这几个概念了。
核心一句话:值传递是“我给你复印件,你随便改”,地址传递是“我给你原件地址,你改了我也跟着变”。
13.1 值传递与地址传递
13.1.1 值传递——你改你的,我改我的
值传递,说白了就是把变量的值复制一份传给函数。函数里拿到的是个副本,你在函数里怎么折腾这个副本,外面的原变量纹丝不动。
我见过不少新手写交换函数,写成这样:
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
printf("函数内部:a = %d, b = %d\n", a, b);
}
int main() {
int x = 10, y = 20;
printf("调用前:x = %d, y = %d\n", x, y);
swap(x, y);
printf("调用后:x = %d, y = %d\n", x, y);
return 0;
}
运行结果:
调用前:x = 10, y = 20
函数内部:a = 20, b = 10
调用后:x = 10, y = 20
看到了吗?函数内部确实交换了,但main里的x和y纹丝未变。为什么会这样?因为swap拿到的是x和y的副本,你交换的是副本,原件当然不动。
我的习惯:如果函数不需要修改外部变量,就用值传递。安全、清晰、不容易出幺蛾子。
13.1.2 地址传递——我给你钥匙,你进来改
那如果我真的想让函数修改外面的变量呢?就得用地址传递——把变量的地址传给函数,函数通过指针来操作原变量。
正确的交换函数应该这么写:
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
printf("函数内部:*a = %d, *b = %d\n", *a, *b);
}
int main() {
int x = 10, y = 20;
printf("调用前:x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("调用后:x = %d, y = %d\n", x, y);
return 0;
}
运行结果:
调用前:x = 10, y = 20
函数内部:*a = 20, *b = 10
调用后:x = 20, y = 10
这次交换成功了。因为swap拿到的是x和y的地址,通过解引用操作符*,直接修改了地址里存的值。
我曾经踩过的坑:有一次我写链表插入函数,传了指针变量进去,结果函数里改了指针指向,但外面的头指针没变。查了半天才发现——指针本身也是值传递!想改指针本身,得传指针的指针(二级指针)。
13.1.3 什么时候用值传递,什么时候用地址传递?
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 函数不需要修改参数 | 值传递 | 安全、简单、避免意外修改 |
| 函数需要修改参数 | 地址传递 | 通过指针直接操作原变量 |
| 参数是大型结构体 | 地址传递(传指针) | 避免拷贝开销,提高性能 |
| 参数是数组 | 地址传递(数组名即地址) | C语言中数组不能按值传递 |
| 需要函数返回多个值 | 地址传递 | 通过指针参数“带出”结果 |
13.2 局部变量与全局变量
13.2.1 局部变量——我的地盘我做主
局部变量定义在函数内部,只在函数执行期间存在。你想想看,你在一个函数里定义的变量,另一个函数能用吗?不能。这就是局部变量的“地盘意识”。
#include <stdio.h>
void func() {
int local_var = 100; // 局部变量
printf("func中:%d\n", local_var);
}
int main() {
// printf("%d\n", local_var); // 编译错误!local_var未定义
func();
return 0;
}
局部变量的好处是隔离性好。我在项目里写模块时,能用局部变量绝不用全局变量。为什么?因为局部变量不会污染命名空间,不会出现“这个变量到底被谁改了”的悬案。
13.2.2 全局变量——人人都能碰,人人都可能搞砸
全局变量定义在函数外面,从定义位置开始,到文件结束,所有函数都能访问它。
#include <stdio.h>
int global_count = 0; // 全局变量
void increment() {
global_count++;
}
void reset() {
global_count = 0;
}
int main() {
increment();
increment();
printf("计数:%d\n", global_count); // 输出2
reset();
printf("重置后:%d\n", global_count); // 输出0
return 0;
}
看起来很方便对吧?但我要说一句:全局变量是万恶之源(之一)。
我曾经的血泪教训:在一个嵌入式项目里,有个全局变量被三个中断服务程序和两个任务同时读写。结果程序跑着跑着就随机崩溃,查了整整一周才发现是全局变量被非原子操作修改了。从那以后,我定了个规矩:全局变量必须加锁访问,能不用就不用。
13.2.3 局部变量 vs 全局变量:怎么选?
| 对比维度 | 局部变量 | 全局变量 |
|---|---|---|
| 作用范围 | 函数内部 | 整个文件(甚至整个程序) |
| 生命周期 | 函数调用期间 | 程序运行期间 |
| 默认初始值 | 未初始化时为垃圾值 | 未初始化时为0 |
| 存储位置 | 栈 | 静态存储区 |
| 多函数共享 | 不能 | 能 |
| 推荐使用 | 优先使用 | 尽量少用 |
13.3 变量的作用域与生命周期
13.3.1 作用域——变量能见范围
作用域,说白了就是变量在代码的哪些地方能被“看见”和使用。C语言的作用域分这么几种:
- 块作用域:在{}内定义的变量,只在这个块内有效
- 文件作用域:在函数外面定义的变量,从定义点到文件末尾都有效
- 函数作用域:只有goto标签有这个作用域,咱们先不展开
- 函数原型作用域:函数声明中的参数名,只在声明内有效
#include <stdio.h>
int file_scope_var = 1; // 文件作用域
void func() {
int block_scope_var = 2; // 块作用域(函数块)
if (1) {
int inner_var = 3; // 块作用域(if块)
printf("内部块:%d, %d, %d\n", file_scope_var, block_scope_var, inner_var);
}
// printf("%d\n", inner_var); // 错误!inner_var在这里不可见
}
int main() {
func();
printf("文件作用域:%d\n", file_scope_var);
// printf("%d\n", block_scope_var); // 错误!block_scope_var不可见
return 0;
}
13.3.2 生命周期——变量活多久
生命周期和存储类型密切相关。C语言有四种存储类型:
| 存储类型 | 关键字 | 生命周期 | 作用域 | 默认初始值 |
|---|---|---|---|---|
| 自动 | auto(默认) | 块执行期间 | 块作用域 | 垃圾值 |
| 静态 | static | 程序运行期间 | 块作用域或文件作用域 | 0 |
| 寄存器 | register | 块执行期间 | 块作用域 | 垃圾值 |
| 外部 | extern | 程序运行期间 | 文件作用域 | 0 |
这里我想重点说说static。很多人觉得static就是“静态”的意思,其实它在不同场景下作用完全不同:
- static修饰局部变量:生命周期变成整个程序运行期间,但作用域不变(还是块作用域)。函数退出时变量不销毁,下次调用时值还在。
- static修饰全局变量:作用域限制在当前文件内,其他文件不能通过extern访问。
- static修饰函数:函数只在本文件内可见,其他文件不能调用。
#include <stdio.h>
void counter() {
static int count = 0; // 静态局部变量,只初始化一次
count++;
printf("第%d次调用\n", count);
}
int main() {
counter(); // 第1次调用
counter(); // 第2次调用
counter(); // 第3次调用
return 0;
}
运行结果:
第1次调用
第2次调用
第3次调用
你看,count的值在函数调用之间被保留了。这就是静态局部变量的典型用法——计数器、状态标志等场景特别好用。
我个人的建议:static是个好东西,但别滥用。我一般在两种情况下用static:一是需要函数记住上次的状态(比如计数器),二是想隐藏内部实现细节(static函数和static全局变量)。其他情况,能用局部变量就用局部变量。
13.4 知识体系总览
说了这么多,咱们用一张图把这一章的核心逻辑串起来:
这张图把咱们这一章的核心脉络理清楚了。从上往下看:函数参数传递方式决定了数据怎么流动,变量类型决定了数据存在哪、谁能访问,而作用域和生命周期则决定了变量“在哪儿能用”和“能活多久”。
嗯,这一章的内容就到这儿。记住一句话:写函数时想清楚数据怎么传,定义变量时想清楚它该活多久、给谁用。这两件事想明白了,C语言函数这块你就拿捏住了。
公众号:蓝海资料掘金营,微信deep3321