前面几节课咱们把函数定义、调用、参数传递都说完了。但有一个事情咱们一直没细聊——变量在内存里到底是怎么存放的?函数执行完了,变量去哪儿了?为什么有的变量你出了函数就找不着了,有的变量却能一直活着?
今天咱们就来说道说道这个事儿。
先说两个词:作用域和生命周期。
这俩词长得很像,但说的是完全不同的东西。
-
作用域:你的变量在“代码的哪个范围内”能被访问到。简单说就是“在哪儿能用”。
-
生命周期:你的变量在“内存里活多长时间”。简单说就是“活了多久”。
打个比方你就懂了。你家楼下的便利店有个店员叫小王。你只能在便利店那一片见到他(作用域就是“便利店”)。他每天早上8点到晚上10点上班(生命周期就是“8点到10点”)。两个概念独立——他在店里待着(作用域),他待的时间是从早到晚(生命周期),但范围和时长是两码事。
在C语言里,变量的存储类别(也叫存储类型)决定了它的作用域和生命周期。有四种存储类别:auto、static、register、extern。咱们一个个说。
一、auto——自动变量,用完就扔
先说auto。这个是你最熟悉的,只是你可能不知道它叫这个名字。
你在函数里定义的普通局部变量,默认就是auto类型的。说白了就是:函数执行到变量定义的地方,它出生了;函数执行完,它当场死亡。
加不加auto关键字效果一样,因为C语言默认就是auto。所以你平时写的int a = 10;,其实完整写法是auto int a = 10;,只是大家都省略了auto。
#include <stdio.h> void func() { auto int a = 10; // 完整写法 int b = 20; // 省略写法,效果一模一样 printf("%d %d\n", a, b); // 函数结束,a和b一起死掉 } int main() { func(); // printf("%d\n", a); // 错误!a已经死了,访问不到 return 0; }
auto变量的特点:
-
作用域:定义它的那个函数或代码块。出了花括号,就找不着了。
-
生命周期:从定义开始,到函数结束。每次进入函数都是新创建一块内存,每次退出都释放。
-
不初始化的话,里面的值是垃圾数据(随机值),因为每次分配的内存里可能残留着别的东西留下的数据。
你看下面这段代码:
#include <stdio.h> void count() { int counter = 0; // auto变量 counter++; printf("counter = %d\n", counter); } int main() { count(); // 输出1 count(); // 输出1 count(); // 输出1 return 0; }
每次调用count,counter都是新创建的,初始化为0,然后变成1,函数一结束就销毁了。下次再来,又是从0开始。它不“记住”上次的值。
二、static——静态变量,盯上就不走了
static跟auto正好相反。加了static的变量,一旦出生,就一直活到程序结束,不会因为函数结束而消失。
#include <stdio.h> void count() { static int counter = 0; // static变量,只初始化一次 counter++; printf("counter = %d\n", counter); } int main() { count(); // 输出1 count(); // 输出2 count(); // 输出3 return 0; }
这回不一样了。counter第一次被创建的时候初始化为0,然后变成1。函数结束,如果它是auto变量,这时候已经死了。但它是static,它不死,那块内存一直留着。第二次调用count的时候,程序不会再执行static int counter = 0;这行初始化语句了(因为它只初始化一次),直接拿过来就用,counter现在是1,加1变成2。第三次再拿过来是2,加1变成3。
这就是static的第一个特点:生命周期延长到程序结束,函数结束它也不死。
那static变量的作用域呢?作用域不变,还是在定义它的那个函数里。
你看上面的代码,counter虽然一直活着,但你只能在count函数里访问它,在main里是访问不到的。static只延长寿命,不扩大地盘。
static还有第二个用法:修饰全局变量,限制它的作用域只在当前文件。
你定义一个全局变量,默认情况下,其他文件也能通过extern来用它。但如果你加上static,它就变成了“文件内部的私有变量”,别的文件访问不了。
// file1.c static int hidden = 100; // 只在本文件可见 void func() { hidden = 200; // 可以 }
// file2.c extern int hidden; // 错误!链接的时候找不到hidden,因为file1.c里它是static的
这个特性在实际开发中很有用——你不想让别人乱动你的内部数据,就给它加个static,把它锁在当前文件里。
关于static初始化,还有一个点得说清楚:static变量如果不显式初始化,编译器会自动把它初始化为0。
-
auto变量不初始化,值是随机的垃圾数据。 -
static变量不初始化,值自动是0。
#include <stdio.h> void func() { static int a; // 不初始化,自动变成0 int b; // 不初始化,值是随机的 printf("%d %d\n", a, b); }
三、register——寄存器变量,想住进CPU里
register关键字是给编译器的“一个建议”:“这个变量我频繁使用,你如果方便的话,能不能把它放到CPU的寄存器里,别放在内存里?”寄存器读写比内存快得多,如果你有个变量在循环里被反复读写几百万次,放寄存器里能快不少。
#include <stdio.h> int main() { register int i; // 请求把i放到寄存器里 for (i = 0; i < 1000000; i++) { // 反复使用i,放寄存器里更快 } return 0; }
但是你要知道:register只是一个“请求”,编译器听不听不一定。现在的编译器优化做得非常好,很多时候你不用写register,编译器自己就会判断哪些变量适合放寄存器。
register有两个限制你得知道:
-
不能对
register变量取地址(因为它可能在寄存器里,没有内存地址)。 -
不能定义成全局的
register。
register int a = 10; int *p = &a; // 错误!不能取地址
现在register用得越来越少了,你知道有这回事就行,不用刻意用。
四、extern——告诉编译器“这个变量在别处”
extern是专门用来“跨文件访问变量”的。它的作用是声明:这个变量不是在这里定义的,它在别的文件里,你链接的时候去那边找。
// file1.c #include <stdio.h> int global = 100; // 定义全局变量 void print_global() { printf("%d\n", global); }
// file2.c #include <stdio.h> extern int global; // 声明:global在别的文件里 int main() { printf("%d\n", global); // 输出100,找到了file1.c里的global return 0; }
注意区分:
-
定义:
int global = 100;—— 分配内存,真正创建了这个变量。 -
声明:
extern int global;—— 只是告诉编译器“有这个变量存在,不分配内存,你到时候去别处找”。
声明可以多次,定义只能有一次。你可以在多个文件里写extern int global;,但只能在一个文件里写int global = 100;,否则链接会报重复定义错误。
五、全局变量——默认情况下的“跨界”
刚才说到extern,就得顺带说说全局变量。全局变量是在函数外面定义的变量,它的默认行为是:
-
作用域:从定义处开始,到文件末尾。其他文件如果想用,可以通过
extern声明来访问。 -
生命周期:程序一开始运行就出生,程序结束才死亡。
#include <stdio.h> int global = 10; // 全局变量:程序启动就活着 void func() { global = 20; // 可以访问 } int main() { printf("%d\n", global); // 输出20 return 0; } // 程序结束,global才死
如果你在函数里定义了一个跟全局变量同名的局部变量,局部变量会“遮蔽”全局变量:
#include <stdio.h> int a = 100; // 全局变量 int main() { int a = 200; // 局部变量,名字跟全局一样 printf("%d\n", a); // 输出200,局部变量覆盖了全局的 return 0; }
在局部作用域里,你的a永远是局部的那个,全局那个a被“藏”起来了。这种情况最好避免——起不一样的名字,一目了然,不出错。
六、各个变量的总结对比
咱们把四种存储类别拉一张表出来,一目了然:
| 存储类别 | 作用域 | 生命周期 | 默认初始值 | 典型用法 |
|---|---|---|---|---|
auto(局部变量) |
所在函数或代码块 | 函数执行期间 | 垃圾值 | 函数内部临时数据 |
static(静态局部) |
所在函数或代码块 | 整个程序运行期间 | 0 | 需要“记住”上次值的变量 |
static(静态全局) |
当前文件 | 整个程序运行期间 | 0 | 限制全局变量只能在文件内用 |
register |
所在函数或代码块 | 函数执行期间 | 垃圾值 | 高频使用的变量(较少用) |
extern(外部声明) |
整个程序 | 取决于被声明变量的定义 | 不分配内存 | 跨文件使用变量 |
| 普通全局变量 | 整个程序(所有文件) | 整个程序运行期间 | 0 | 多文件共享数据 |
再强调一下作用域和生命周期这两个概念的区别:
-
static修饰局部变量:作用域不变(还是在函数里),但生命周期变长(延到程序结束)。 -
static修饰全局变量:作用域缩小(限制在当前文件),生命周期不变(还是程序结束)。 -
auto局部变量:作用域小(函数内部),生命周期短(函数结束就死)。 -
全局变量:作用域大(所有文件),生命周期长(程序结束才死)。
最后讲一个实际开发里常见的问题:什么时候该用全局变量,什么时候不该用?
我给你的建议是:尽量少用全局变量,优先用局部变量。
为啥?因为全局变量是个“双刃剑”。它确实方便——你在任何函数里都能访问它,不用传来传去。但它的麻烦更大:
第一,全局变量在程序的任何地方都可能被修改。你程序跑着跑着值突然变了,你都不知道是哪个函数干的,找Bug找到崩溃。
第二,用全局变量太多会让函数之间的依赖关系变得复杂,一个函数改了全局变量,可能影响几十个完全不相关的函数。
第三,多文件编程的时候,全局变量容易重名,一不小心就冲突了。
所以实际开发中,我建议:
-
能用局部变量就用局部变量,通过参数传递数据。
-
如果多个函数需要共享数据,优先考虑把它们放在一个结构体里,用指针传递。
-
实在没办法了再用全局变量,而且加上
static限制在当前文件内,尽量减少“污染范围”。
好了,这节课的核心就这么多。同学们回去之后,动手改改咱们课上的代码,把auto改成static看看输出有啥变化。把你写的程序里的局部变量加上static,观察函数多次调用时值的变化。亲手跑一遍,你就明白它们到底有什么区别了。