Skip to content

第 25 章 变量的领地——作用域与生命周期


🏗️ 前情回顾:参数搞懂了——值传递是复印件,引用传递是原件。但变量自己呢?为什么在 if 里声明的变量出了大括号就消失?老师说的"作用域"到底是什么?如果把变量比作学生,每个 {} 就是一个教室——出了教室门,班长的话就不管用了。这一章,我们来画一张"变量地图"。


🎯 本章目标

学完这一章,你能:

  • 清晰区分全局变量和局部变量,知道各自"地盘"在哪
  • 掌握块作用域规则——变量在定义它的 {} 内有效
  • 理解同名变量屏蔽规则,写出预期行为的代码
  • 使用 static 让局部变量"活得更久"
  • 画出变量的"生命周期线"——创建 → 使用 → 销毁
  • 认清全局变量的三大风险,养成"尽量用局部变量"的好习惯

📖 故事引入

想象你在一座办公大楼里工作:

  • 你的工位上有台私人电脑,只有你能用,别人碰不到——这就像局部变量,只在你自己的函数/代码块里有效。
  • 公司大厅的公告栏,谁都可以贴条、谁都可以改——这就像全局变量,任何函数都能读写。

这就像学校里的班级制度:每个班的班长只能管自己班的事。3 班的班长跑到 5 班发号施令——没人理他。变量也一样,func() 里的局部变量出了 func() 的大括号,外面的代码就当它不存在。

私人电脑很安全:你关掉它,它里的东西就清空了,不会影响别人。公告栏则很危险:你上午贴的公告,下午被人擦掉改成了团建通知,你完全不知情。

编程里的变量也有类似的"居住地"和"寿命"。理解这些规则,你写的代码才能各归其位、互不干扰。


🧱 知识讲解

25.1 两种地盘:全局变量 vs 局部变量

局部变量:定义在函数体内(或代码块内),只在该函数/代码块里有效。

全局变量:定义在所有函数外面(和 main() 平级),从定义点开始到文件末尾的任何函数都能访问它。

跨函数访问——凭空找变量:

cpp
void func() { int secret = 42; }
int main() {
    func();
    cout << secret;  // ❌ 编译错误!secret 在 func() 里,外面看不见
}

每个函数的局部变量就像每个教室的班长——只在自己教室有权威,出了门就没人听他指挥了。

✅ 正确使用局部和全局变量:

cpp
#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++ 的作用域规则非常直观——大括号 {} 就是一道墙。变量在定义它的那对 {} 内有效,出了 } 就消失。

这不只是函数的 {}ifforwhile 甚至你手动加的裸 {} 都算。

cpp
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 循环里的变量也是块作用域的典型:

cpp
for (int i = 0; i < 10; i++) {   // i 的作用域就是整个 for 循环体
    cout << i << " ";
}
// cout << i;                     // ❌ i 已经不存在了

💡 规则:内层块可以访问外层块的变量,但外层块访问不到内层块的变量。


25.3 同名变量屏蔽规则

如果外层和内层定义了同名的变量,会发生"屏蔽"。内层用内层的,外层用外层的——近水楼台先得月

cpp
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;
}

如果想在被屏蔽的内层访问全局变量,用 ::(作用域解析运算符):

cpp
int value = 10;

int main() {
    int value = 20;
    cout << value << endl;       // 20 —— 局部
    cout << ::value << endl;     // 10 —— 全局(:: 前缀代表全局作用域)
    return 0;
}

⚠️ 但同名屏蔽本身是一种代码坏味道——容易让读代码的人(包括三个月后的你自己)头晕。尽量避免。


25.4 static:让局部变量"活更久"

普通的局部变量,函数每次调用时诞生,返回时死去,下次调用再重新生一个。但如果你给局部变量加上 static 关键字,它就"长效"了——只初始化一次,函数返回后也不死,下次调用还保持上次的值

cpp
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

cpp
void counter() {
    int count = 0;              // 普通局部变量
    count++;
    cout << "第 " << count << " 次调用" << endl;
}

每次输出都是 第 1 次调用——因为 count 每次都被重新初始化为 0。

static 的实战用途:记录函数被调用的次数、保持跨调用的状态、实现单例模式等。


25.5 变量的生命周期

把变量的一生梳理成一条时间线:

                   创建        使用                   销毁
全局变量:  ──●─────────[────────────────●──────────]────→ 程序结束
                      
局部变量:           ●调用函数  [────使用──●]  函数返回
                    └─ 创建              └─ 销毁
                    
static局部:  ──●─────────[────────────────●──────────]────→ 程序结束
              (首次调用时创建,之后不重建,程序结束才销毁)

一段综合演示代码

cpp
#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 找不到凶手

cpp
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:函数不独立,不能复用

cpp
int maxScore = 0;                // 全局最高分

int calculateGrade(int score) {
    if (score > maxScore * 0.9)  // 依赖全局变量!
        return 5;
    return 3;
}

这个 calculateGrade 函数看起来独立,实际上偷偷依赖了 maxScore。如果你想把这段逻辑拷贝到另一个程序里——对不起,还得把 maxScore 也搬过去,而且不知道哪里会偷偷改它。

风险 3:命名冲突

cpp
// 文件 A
int count = 0;

// 文件 B
int count = 0;  // ❌ 链接错误!两个全局变量同名

在大型项目里,几十个文件,难免有人定义同名的全局变量,导致链接错误。

信息学竞赛中的"特例":在某些竞赛题里,你会看到全局大数组(如 int a[100005])。这主要是为了避免栈溢出(局部大数组可能撑爆仅有几 MB 的栈空间),以及减少参数传递的麻烦。但即使如此,也只用全局数组存数据,核心逻辑依然靠局部变量。


25.7 最佳实践:尽量用局部变量

学编程久了,你会发现一个铁律——数据的活动范围越小,程序越安全。就像家里值钱的东西不放在门口。

推荐写法

cpp
// ✅ 好:所有变量都在需要它们的最小范围内
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;
}

不推荐写法

cpp
// ❌ 差:全局满天飞,把不需要全局的也拉成全局
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;
}

决策清单

  1. 这个变量只在一个函数里用?→ 放函数内部
  2. 这个变量只在一个代码块(if/for)里用?→ 放代码块内部
  3. 这个变量需要跨函数共享?→ 作为参数传进去(必要时用引用),而不是设成全局
  4. 真的必须全局?→ 放到最后考虑,并加注释说明原因

✋ 动手试试

试试 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)以下代码输出什么?

cpp
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. 读代码写结果

cpp
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)

cpp
int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

这个函数有一个著名的效率问题——很多值被重复计算了。请用 static 变量统计 fib(n) 在整个递归过程中实际被进入了多少次(每进入函数体一次算一次)。

输入 n,输出两个数:斐波那契值、函数进入次数。

示例n=5fib=5, 进入次数=15

(提示:在 fib 函数第一行让 static int 计数器 +1)

9. 编程题 — 作用域迷宫

阅读以下代码,写出最终输出,并解释每个 cout 输出的是什么作用域的变量:

cpp
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 章,用短短几行代码解决看似复杂的问题。