第 25 章 变量的领地——作用域与生命周期
🏗️ 前情回顾:参数搞懂了——值传递是复印件,引用传递是原件。但变量自己呢?为什么在
if里声明的变量出了大括号就消失?老师说的"作用域"到底是什么?如果把变量比作学生,每个{}就是一个教室——出了教室门,班长的话就不管用了。这一章,我们来画一张"变量地图"。
🎯 本章目标
学完这一章,你能:
- 清晰区分全局变量和局部变量,知道各自"地盘"在哪
- 掌握块作用域规则——变量在定义它的
{}内有效 - 理解同名变量屏蔽规则,写出预期行为的代码
- 使用
static让局部变量"活得更久" - 画出变量的"生命周期线"——创建 → 使用 → 销毁
- 认清全局变量的三大风险,养成"尽量用局部变量"的好习惯
📖 故事引入
想象你在一座办公大楼里工作:
- 你的工位上有台私人电脑,只有你能用,别人碰不到——这就像局部变量,只在你自己的函数/代码块里有效。
- 公司大厅的公告栏,谁都可以贴条、谁都可以改——这就像全局变量,任何函数都能读写。
这就像学校里的班级制度:每个班的班长只能管自己班的事。3 班的班长跑到 5 班发号施令——没人理他。变量也一样,func() 里的局部变量出了 func() 的大括号,外面的代码就当它不存在。
私人电脑很安全:你关掉它,它里的东西就清空了,不会影响别人。公告栏则很危险:你上午贴的公告,下午被人擦掉改成了团建通知,你完全不知情。
编程里的变量也有类似的"居住地"和"寿命"。理解这些规则,你写的代码才能各归其位、互不干扰。
🧱 知识讲解
25.1 两种地盘:全局变量 vs 局部变量
局部变量:定义在函数体内(或代码块内),只在该函数/代码块里有效。
全局变量:定义在所有函数外面(和 main() 平级),从定义点开始到文件末尾的任何函数都能访问它。
❌ 跨函数访问——凭空找变量:
cppvoid func() { int secret = 42; } int main() { func(); cout << secret; // ❌ 编译错误!secret 在 func() 里,外面看不见 }每个函数的局部变量就像每个教室的班长——只在自己教室有权威,出了门就没人听他指挥了。
✅ 正确使用局部和全局变量:
#include <iostream>
using namespace std;
int gold = 100; // ← 全局变量,全文件可见
void spend() {
int price = 30; // ← 局部变量,只属于 spend()
gold -= price; // 修改全局变量
}
int main() {
int wallet = 50; // ← 局部变量,只属于 main()
gold += wallet; // 全局变量可访问
spend();
cout << gold << endl; // 120
// cout << price; // ❌ 错误!price 是 spend 的局部变量
return 0;
}内存中的布局:
┌────────────────────┐
│ 全局 / 静态区 │ ← gold 住这里,程序启动就分配,程序结束才释放
├────────────────────┤
│ 栈 (Stack) │ ← 局部变量住这里,函数调用时创建,返回时销毁
│ wallet, price │
├────────────────────┤
│ 堆 (Heap) │ ← 动态分配(以后学 new / delete)
└────────────────────┘局部变量随函数调用"临时分配"在栈上。函数执行完,栈空间回收,局部变量灰飞烟灭。而全局变量一直住在地基上,雷打不动。
25.2 块作用域:{} 就是边界
C++ 的作用域规则非常直观——大括号 {} 就是一道墙。变量在定义它的那对 {} 内有效,出了 } 就消失。
这不只是函数的 {},if、for、while 甚至你手动加的裸 {} 都算。
int main() {
int x = 10; // x 的作用域:main() 的大括号
if (x > 5) {
int y = 20; // y 的作用域:if 的大括号
cout << x << " " << y << endl; // ✅ 都能访问 (x 在外层,y 在内层)
} // ← y 死在这里
cout << x << endl; // ✅
// cout << y << endl; // ❌ y 已死
return 0;
} // ← x 死在这里for 循环里的变量也是块作用域的典型:
for (int i = 0; i < 10; i++) { // i 的作用域就是整个 for 循环体
cout << i << " ";
}
// cout << i; // ❌ i 已经不存在了💡 规则:内层块可以访问外层块的变量,但外层块访问不到内层块的变量。
25.3 同名变量屏蔽规则
如果外层和内层定义了同名的变量,会发生"屏蔽"。内层用内层的,外层用外层的——近水楼台先得月。
int x = 100; // 外层(全局)
int main() {
cout << x << endl; // 100 —— 访问全局 x
int x = 200; // 局部 x 屏蔽了全局 x
cout << x << endl; // 200 —— 访问局部的 x
{
int x = 300; // 更内层又屏蔽了一层
cout << x << endl; // 300
} // 300 的 x 死亡,200 的 x 重新露面
cout << x << endl; // 200
return 0;
}如果想在被屏蔽的内层访问全局变量,用 ::(作用域解析运算符):
int value = 10;
int main() {
int value = 20;
cout << value << endl; // 20 —— 局部
cout << ::value << endl; // 10 —— 全局(:: 前缀代表全局作用域)
return 0;
}⚠️ 但同名屏蔽本身是一种代码坏味道——容易让读代码的人(包括三个月后的你自己)头晕。尽量避免。
25.4 static:让局部变量"活更久"
普通的局部变量,函数每次调用时诞生,返回时死去,下次调用再重新生一个。但如果你给局部变量加上 static 关键字,它就"长效"了——只初始化一次,函数返回后也不死,下次调用还保持上次的值。
void counter() {
static int count = 0; // static:只在这行第一次执行时初始化
count++;
cout << "第 " << count << " 次调用" << endl;
}
int main() {
counter(); // 第 1 次调用
counter(); // 第 2 次调用 —— count 保留了上次的 1
counter(); // 第 3 次调用 —— count 保留了上次的 2
return 0;
}输出:
第 1 次调用
第 2 次调用
第 3 次调用static 变量的内存位置:虽然定义在函数内部,但 static 变量不放在栈上——它放在全局/静态区,和全局变量住同一栋楼。所以它不会被函数的栈回收干掉。
对比实验——去掉 static:
void counter() {
int count = 0; // 普通局部变量
count++;
cout << "第 " << count << " 次调用" << endl;
}每次输出都是 第 1 次调用——因为 count 每次都被重新初始化为 0。
static 的实战用途:记录函数被调用的次数、保持跨调用的状态、实现单例模式等。
25.5 变量的生命周期
把变量的一生梳理成一条时间线:
创建 使用 销毁
全局变量: ──●─────────[────────────────●──────────]────→ 程序结束
局部变量: ●调用函数 [────使用──●] 函数返回
└─ 创建 └─ 销毁
static局部: ──●─────────[────────────────●──────────]────→ 程序结束
(首次调用时创建,之后不重建,程序结束才销毁)一段综合演示代码:
#include <iostream>
using namespace std;
int globalVar = 1; // 全局:程序开始就活
void test() {
int localVar = 2; // 局部:每次调用 test 时创建
static int staticVar = 3; // static:首次调用时创建,之后保留
cout << "函数内:" << endl;
cout << " globalVar = " << globalVar << endl; // 一直能访问
cout << " localVar = " << localVar << endl;
cout << " staticVar = " << staticVar << endl;
globalVar++;
localVar++;
staticVar++;
} // localVar 死亡,staticVar 不死
int main() {
cout << "=== 第 1 次调用 ===" << endl;
test();
cout << "=== 第 2 次调用 ===" << endl;
test();
cout << "=== 第 3 次调用 ===" << endl;
test();
return 0;
}输出:
=== 第 1 次调用 ===
函数内:
globalVar = 1
localVar = 2
staticVar = 3
=== 第 2 次调用 ===
函数内:
globalVar = 2 ← 上次+1
localVar = 2 ← 重新创建,还是2
staticVar = 4 ← 上次+1,保留了!
=== 第 3 次调用 ===
函数内:
globalVar = 3
localVar = 2
staticVar = 5建议你把这段代码亲手敲一遍运行,三条曲线的变化非常直观。
25.6 全局变量的风险——为什么大佬都避之不及
全局变量看似方便——放那谁都能用——但它的三大风险让有经验的程序员敬而远之:
风险 1:谁都能改,出 bug 找不到凶手
int health = 100; // 全局生命值
void takePotion() { health += 30; }
void takeDamage() { health -= 50; }
void levelUp() { health = 200; }
int main() {
takeDamage();
takePotion();
levelUp(); // 三个函数都改了 health
// health 到底是多少?如果中间某个函数有 bug……
// 你要在三个函数里大海捞针
return 0;
}调试的时候你问:health 什么时候变成 -20 的?你需要在所有能接触到它的函数里排查——如果有几十个函数呢?
风险 2:函数不独立,不能复用
int maxScore = 0; // 全局最高分
int calculateGrade(int score) {
if (score > maxScore * 0.9) // 依赖全局变量!
return 5;
return 3;
}这个 calculateGrade 函数看起来独立,实际上偷偷依赖了 maxScore。如果你想把这段逻辑拷贝到另一个程序里——对不起,还得把 maxScore 也搬过去,而且不知道哪里会偷偷改它。
风险 3:命名冲突
// 文件 A
int count = 0;
// 文件 B
int count = 0; // ❌ 链接错误!两个全局变量同名在大型项目里,几十个文件,难免有人定义同名的全局变量,导致链接错误。
信息学竞赛中的"特例":在某些竞赛题里,你会看到全局大数组(如 int a[100005])。这主要是为了避免栈溢出(局部大数组可能撑爆仅有几 MB 的栈空间),以及减少参数传递的麻烦。但即使如此,也只用全局数组存数据,核心逻辑依然靠局部变量。
25.7 最佳实践:尽量用局部变量
学编程久了,你会发现一个铁律——数据的活动范围越小,程序越安全。就像家里值钱的东西不放在门口。
推荐写法:
// ✅ 好:所有变量都在需要它们的最小范围内
int main() {
int n;
cin >> n;
int sum = 0;
for (int i = 0; i < n; i++) { // i 只在 for 循环里用
int val;
cin >> val;
sum += val;
}
// i 已经死了,没人能误用它
cout << sum << endl;
return 0;
}不推荐写法:
// ❌ 差:全局满天飞,把不需要全局的也拉成全局
int n; // 不需要全局
int sum = 0; // 不需要全局
int val; // 不需要全局
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> val;
sum += val;
}
cout << sum << endl;
return 0;
}决策清单:
- 这个变量只在一个函数里用?→ 放函数内部
- 这个变量只在一个代码块(if/for)里用?→ 放代码块内部
- 这个变量需要跨函数共享?→ 作为参数传进去(必要时用引用),而不是设成全局
- 真的必须全局?→ 放到最后考虑,并加注释说明原因
✋ 动手试试
试试 1:写一段代码,在 main() 里定义一个变量 a = 1,在 if (true) 的大括号里定义同名变量 a = 2,分别在大括号内外输出 a,观察屏蔽效果。
试试 2:写一个 void countCalls() 函数,用 static int cnt 记录自己被调用了多少次。在 main() 里循环调用 5 次,输出每次的结果。
试试 3:定义一个全局变量 total = 0 和一个带参考的函数 add(int x),每次调用把 x 加到 total。同时另写一个 addLocal 函数,用 static int localTotal 做同样的事。对比两者的行为差异。
试试 4:在 for (int i = 0; i < 3; i++) 外面尝试输出 i,看编译器报什么错。理解块作用域。
🦶 你踩过这些坑吗
逐一核对,看你是不是也中过招:
- [ ] 在函数外访问局部变量:在
main()里使用另一个函数的局部变量 → ❌ 编译错误,那个变量出了{}就死了 → ✅ 通过返回值或参数传递数据 - [ ] 同名变量互相干扰:外层和内层定义同名变量,然后在外层使用时期望拿到内层的值 → ❌ 外层只看得到外层的 → ✅ 避免同名,实在要同名就用
::明确指定全局 - [ ] 误以为 static 每次调用都重新初始化:
static int x = 0;写完后以为每次进函数都从 0 开始 → ❌ static 只初始化一次 → ✅ 记住:static 变量初始化只发生在第一次执行到这一行时 - [ ] 全局变量随手定义:把本可以放在函数内的变量全部堆成全局变量——"反正能跑就行" → ❌ 调试时追责困难 → ✅ 抱着"局部优先"的原则:能放块里不放函数里,能放函数里不放全局
📝 练习
基础题
1. 选择题
(1)以下关于局部变量的描述,正确的是?
A. 在程序的任何地方都能访问 B. 只在定义它的 {} 内有效
C. 不能和全局变量同名 D. 必须在 main() 里定义
(2)static 局部变量的特点是?
A. 每次调用函数都重新创建 B. 只初始化一次,函数返回后值保留
C. 只能在 main() 里使用 D. 可以被其他文件的函数访问
(3)以下代码输出什么?
int x = 5;
int main() {
int x = 10;
cout << x;
return 0;
}A. 5 B. 10 C. 15 D. 编译错误
2. 填空题
(1)定义在所有函数外面的变量叫_____变量,定义在函数或代码块内部的叫_____变量。
(2)C++ 中的作用域由_____决定边界。内层可以访问_____层的变量,但反过来则不行。
(3)在被局部变量屏蔽的内层作用域中访问全局变量,需要用_____前缀。
3. 判断题
(1)全局变量在程序的任何位置都可以被访问和修改。( )
(2)static 局部变量存储在栈上,随函数结束而销毁。( )
4. 读代码写结果
int x = 1;
void f() {
static int x = 10;
x++;
cout << x << " ";
}
int main() {
f(); f(); f();
cout << ::x;
}输出:___________
提高题
5. 编程题 — 统计函数调用次数
写一个函数 bool isPrime(int n),判断 n 是否为素数。但这个函数有一个特殊要求:用 static 变量记录它一共被调用了多少次,并在最终输出这个计数。
在 main() 里输入 L 和 R,用循环检查 [L, R] 内每个数是否为素数,最后输出"isPrime 共被调用 __ 次"。
6. 编程题 — 变量生命周期追踪
写一个程序,包含:
- 一个全局变量
globalCounter - 一个函数
void step(),内有局部变量localCounter和 static 变量staticCounter - 每次调用
step()时让三个变量各 +1 并分别输出当前值 - 在
main()里连续调用step()5 次
分析输出结果,说明三种变量生命周期的差异。
7. 编程题 — 函数调用计数通用版
写一个函数 int callCounter(),每次调用时返回它被调用的次数(从 1 开始计数)。在 main() 里循环调用 10 次,验证输出是否正确。
(提示:用 static int count,每次 count++ 后返回。)
挑战题
8. 编程题 — 斐波那契调用计数
用大家熟悉的递归斐波那契函数 int fib(int n):
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}这个函数有一个著名的效率问题——很多值被重复计算了。请用 static 变量统计 fib(n) 在整个递归过程中实际被进入了多少次(每进入函数体一次算一次)。
输入 n,输出两个数:斐波那契值、函数进入次数。
示例:n=5 → fib=5, 进入次数=15
(提示:在 fib 函数第一行让 static int 计数器 +1)
9. 编程题 — 作用域迷宫
阅读以下代码,写出最终输出,并解释每个 cout 输出的是什么作用域的变量:
int a = 1;
int main() {
cout << a << endl; // ①
int a = 2;
cout << a << endl; // ②
{
int a = 3;
cout << a << endl; // ③
cout << ::a << endl; // ④
}
cout << a << endl; // ⑤
}🧠 本章小结
作用域:变量能"被看见"的代码范围
→ 由 {} 决定边界
→ 内层能看外层,外层看不了内层
→ 同名时内层屏蔽外层(:: 可突破屏蔽)
变量分类:
┌─────────────────────────────────────────────────────┐
│ 全局变量 │ 局部变量 │ static 局部 │
├──────────────────┼─────────────────┼────────────────┤
│ 函数外定义 │ 函数/块内定义 │ 函数内 + static│
│ 全文件可见 │ {} 内可见 │ {} 内可见 │
│ 程序启停 │ 函数进出 │ 程序启停 │
│ 慎用! │ 首选! │ 有节制地用 │
└──────────────────┴─────────────────┴────────────────┘
最佳实践:变量的活动范围越小越好——能放块里就不放函数里,能放函数里就不放全局。📝 配套练习
共3题。作用域通过"读bug代码"来学,不需要新题。。★核心(课堂必做) ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | 复用J0104 | — | 局部变量vs全局变量同名bug |
| ◆ 拓展 | 复用J0101 | — | 形参作用域:函数内访问main变量 |
| ◆ 拓展 | 无 | — | 读代码找bug:静态变量累加陷阱 |
💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 2 道◆拓展题,覆盖不同变式和细节。
配套练习
共3题。作用域通过"读bug代码"来学,不需要新题。★核心(课堂必做) · ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | 复用J0104 | — | 局部变量vs全局变量同名bug |
| ◆ 拓展 | 复用J0101 | — | 形参作用域:函数内访问main变量 |
| ◆ 拓展 | 无 | — | 读代码找bug:静态变量累加陷阱 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 2 道◆拓展题,覆盖不同变式和细节。
自查清单:
- [ ] 我能准确说出全局变量、局部变量、static 局部变量的定义位置和生命期
- [ ] 我理解
{}决定作用域,能画出嵌套代码块的"可见范围" - [ ] 我知道同名变量屏蔽规则,并尽量避免同名
- [ ] 我会用
static让局部变量"活过函数返回" - [ ] 我能列出全局变量的三大风险(随意修改、函数不独立、命名冲突)
- [ ] 养成了"局部优先"的习惯
🚀 下章预告:函数写顺手了——它能不能调用自己?像俄罗斯套娃一样层层深入?听起来像"左脚踩右脚飞上天",但这就是计算机科学中最优雅的思想之一——递归。第 26 章,用短短几行代码解决看似复杂的问题。