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 知识体系总览

说了这么多,咱们用一张图把这一章的核心逻辑串起来:

函数核心概念:传递方式、变量类型、作用域与生命周期 函数参数传递方式 变量类型 值传递 地址传递 局部变量 全局变量 副本操作 指针操作原件 栈上分配 静态存储区 作用域与生命周期 块作用域 文件作用域 自动/静态存储 extern/register

这张图把咱们这一章的核心脉络理清楚了。从上往下看:函数参数传递方式决定了数据怎么流动,变量类型决定了数据存在哪、谁能访问,而作用域和生命周期则决定了变量“在哪儿能用”和“能活多久”。

嗯,这一章的内容就到这儿。记住一句话:写函数时想清楚数据怎么传,定义变量时想清楚它该活多久、给谁用。这两件事想明白了,C语言函数这块你就拿捏住了。


公众号:蓝海资料掘金营,微信deep3321