上一篇文章我们学会了把代码封装成函数,还注意到一个奇怪的现象:函数里定义的变量,出了函数就“消失”了;函数参数在里面不管怎么改,都影响不到外面。这背后其实是 C 语言的两条看不见的规则在起作用:作用域和生命周期。
理解它们,你才能回答:
- 为什么在
main里不能直接用另一个函数的局部变量? - 为什么有的变量每次调用函数都重新来一遍,有的却能记住上一次的值?
- 为什么全局变量这么“方便”,却总被前辈劝说要少用?
这篇文章,我们就来揭开这些谜底,把变量的“生存空间”搞清楚。这也是学习指针之前必须打好的一层地基。
一、作用域:你能在哪里访问这个变量?
作用域(Scope) 规定了变量名在代码的哪些区域是可见的、可访问的。你可以把它理解成“这个名字的有效势力范围”。
C 语言的作用域主要有三类:
1. 块作用域(Block Scope)
块是指用一对花括号 {} 包裹的区域。函数体是一个块,if 后面的大括号里也是块,甚至你可以凭空写一对花括号来创建一个独立的块。
在块内定义的变量,作用域就限制在这个块内,从定义处到块的右花括号 } 结束。
#include <stdio.h>
int main(void) {
int a = 10; // a 的作用域开始
if (a > 5) {
int b = 20; // b 的作用域只在这个 if 块里
printf("a=%d, b=%d\n", a, b); // OK
}
printf("a=%d\n", a); // OK
// printf("b=%d\n", b); // 错误!b 不在作用域里
return 0;
}
b 只能在它所在的 if 块内使用,出了这个块编译器就不认识它了。
函数的形参虽然写在花括号外面,但它们也被视为属于函数体的块作用域。也就是说,形参只在函数内部有效。
2. 文件作用域(File Scope)
在所有函数外面定义的变量,具有文件作用域,从定义处到整个文件结束都可见。我们常把这样的变量叫全局变量。
#include <stdio.h>
int global_counter = 0; // 文件作用域,从这行往下全文件可见
void increment(void) {
global_counter++;
}
int main(void) {
increment();
printf("%d\n", global_counter); // 1
return 0;
}
如果多个 .c 文件分别定义了同名的全局变量,会怎样?那就要用到 extern 和 static 来协调,这是下一篇“多文件编译”的核心话题。
3. 函数原型作用域(了解即可)
你在函数原型里写的参数名,作用域仅限于那个括号内。写不写名字都无所谓,它只是个占位符:
int max(int a, int b); // a 和 b 的范围仅限于这个括号内
二、生命周期:这个变量能活多久?
如果说作用域回答了“你在哪里能访问我”,**生命周期(Storage Duration,存储期)**则回答了“我在内存里存活多长时间”。
C 语言有三种主要的存储期:
1. 自动存储期(Automatic Storage Duration)
我们在函数内定义的普通局部变量(包括形参),都具有自动存储期。它们的内存空间在函数被调用时自动分配,在函数返回时自动释放。
这就是为什么局部变量不能跨函数使用——不是“看不见”,而是它本身已经消失了。栈帧被销毁,变量所在的地址重归系统。
int* bad_pointer(void) {
int x = 42;
return &x; // 危险!x 在函数返回后消失
}
这个经典的错误正是因为返回值指向了一个自动存储期的变量。
2. 静态存储期(Static Storage Duration)
具有静态存储期的变量,在整个程序运行期间一直存在——从程序启动时创建,到程序结束时销毁。它们的空间分配在静态数据区(不在栈上)。
哪些变量有静态存储期?
- 所有全局变量(不管是否加
static) - 用
static关键字修饰的局部变量 - 字符串字面量(如
"Hello")也可以视为具有静态存储期
全局变量的生命周期从 main 执行之前就开始,到 main 执行结束后才结束,持续整个程序。
3. 动态存储期(Dynamic Storage Duration)
这种变量不由编译器自动管理,而是由程序员用 malloc 等函数手动申请,用 free 手动释放。它的空间在堆(Heap)上,生命周期由你决定。我们会在后面的动态内存分配专题里详细讲。
三、static 关键字:让局部变量拥有“记忆力”
在函数内部,用 static 修饰一个局部变量,它就会被从“自动存储期”升级到“静态存储期”。也就是说,虽然作用域还是函数内部(外部不可见),但它的生命周期变成了整个程序运行期间。
这意味着什么?它能在函数调用之间“记住”上一次的值。
看这个经典的计数器例子:
#include <stdio.h>
void count_calls(void) {
static int counter = 0; // 静态局部变量,只初始化一次
counter++;
printf("这个函数被调用了 %d 次\n", counter);
}
int main(void) {
for (int i = 0; i < 5; i++) {
count_calls();
}
return 0;
}
输出:
这个函数被调用了 1 次
这个函数被调用了 2 次
这个函数被调用了 3 次
这个函数被调用了 4 次
这个函数被调用了 5 次
注意 static int counter = 0; 这行,只在第一次调用时执行初始化,之后每次进入函数,counter 保留上次的值。这就像一个“私有的永久储物柜”——别人碰不到,但它一直在那儿。
如果不用 static,把 static 去掉,counter 就变成普通自动变量,每次调用都重新初始化为 0,永远只能输出 1。试试看,体会一下差别。
四、块作用域的嵌套:名字遮蔽
块作用域可以嵌套。当内外两个块定义了同名的变量时,内层变量会遮蔽外层的同名变量——在内层作用域里,你访问的是内层的那个,外层的暂时不可见。
#include <stdio.h>
int main(void) {
int value = 10;
printf("外层 value = %d\n", value);
{
int value = 20; // 这个 value 遮蔽了外层的 value
printf("内层 value = %d\n", value);
} // 内层 value 生命结束
printf("外层 value = %d\n", value); // 仍然 10
return 0;
}
输出:
外层 value = 10
内层 value = 20
外层 value = 10
同一个名字出现在不同层级时,编译器按“最近优先”的规则查找。这种遮蔽有时候方便,但也容易造成迷惑,所以尽量避免在嵌套块中定义同名变量。
五、全局变量:力量与代价
全局变量(文件作用域变量)看起来很方便——到处都能访问,不用传参了。但这条捷径往往通向“混乱”。随着程序增大,任何一个函数都可能偷偷修改全局变量,导致 bug 难以追踪。
一条经验法则:能不用全局变量,就不用。优先把数据在函数之间传递,而不是放在外面共享。
如果确实需要用一个全局变量(比如整个程序配置),用 static 限制它的作用域范围(下一章会讲),并且给它起一个清楚、不易冲突的名字。
六、作用域与生命周期速查表
| 变量位置 | 作用域 | 生命周期 | 存储位置 | 初始化情况 |
|---|---|---|---|---|
函数内局部变量(无 static) | 块作用域 | 函数调用期间 | 栈 | 不自动初始化(垃圾值) |
函数内局部变量(static) | 块作用域 | 整个程序运行期 | 静态数据区 | 自动初始化为 0 |
全局变量(无 static) | 文件作用域(可被其他文件引用) | 整个程序运行期 | 静态数据区 | 自动初始化为 0 |
全局变量(static) | 文件作用域(仅本文件可见) | 整个程序运行期 | 静态数据区 | 自动初始化为 0 |
注意:全局变量和 static 局部变量,如果你不手动初始化,它们会被自动初始化为 0(或空字符、空指针),这和普通局部变量(垃圾值)不同。
七、常见错误与陷阱
1. 返回局部变量的地址(再强调一遍!)
int* create_int(void) {
int n = 100;
return &n; // n 在函数结束后消失,返回的指针是悬空指针
}
要返回指针,可以返回 static 局部变量的地址(因为它的生命周期是全程的),或返回动态分配的内存地址,或返回传入的参数地址。这个“大坑”在后面指针专题还会填。
2. 滥用全局变量导致逻辑混乱
int count; // 全局
void funcA(void) { count += 2; }
void funcB(void) { count *= 3; }
int main(void) {
count = 1;
funcA();
funcB();
printf("%d\n", count); // 输出 9 还是什么?你要在心里跟踪
return 0;
}
当程序变大,任何一个函数都可能悄悄改变全局变量,你完全搞不清它怎么变成了这个值。用参数传递和返回值来代替全局状态,是更安全的设计。
3. 误解静态局部变量的初始化
static int x = 0; 在函数中只执行一次。但如果你写成 static int x; x = 0; 就是另一回事了——赋值语句每次函数调用都会执行。保持用初始化语法。
4. 在嵌套块中无意遮蔽
当你写 int i = 5; 在一个块里,而外层已经有个 i,你可能不小心改了逻辑。给变量起有意义的名字,尽量避免重用。
八、小结与预告
今天我们搞清楚了变量的“地盘”和“寿命”。作用域决定了在哪里能访问,生命周期决定了能活多久。static 让局部变量有了跨调用的记忆,全局变量让全文件共享,但要谨慎使用。你还看到了局部变量在栈上分配、函数返回即销毁的根本原因。
理解了这些,你就不会再困惑“为什么这个变量这里不可见”“为什么这个值没有保留下来”。更重要的是,你已经为下一篇文章做好了铺垫——当多个 .c 文件需要共享变量和函数时,extern 和 static 是怎么协作的?这就是多文件编译与头文件要讲的东西。
再下一步,当我们进入指针的深处时,你才会真正体会到:透彻地理解变量住在哪里、活多久,是安全使用指针的前提。
课后小练习
- 写一个函数
next_id(void),每次调用返回一个递增的整数(第一次返回 1,第二次返回 2……),要求在函数内使用static变量实现。 - 下面的代码有错误,找出并解释:
int* trouble(void) { int temp = 10; return &temp; } int main(void) { int *p = trouble(); printf("%d\n", *p); return 0; } - 定义一个全局变量
int mode = 0;,再写两个函数:一个把mode设为 1,一个设为 2。在main里交替调用它们并打印mode,观察结果。然后思考:这种依赖全局状态的写法,有什么不方便的地方? - (思考)写一段代码,在内层块中定义一个和外部同名的变量,并在内外分别打印。验证名字遮蔽的效果。
我们下期见!
247

被折叠的 条评论
为什么被折叠?



