4.4 作用域与存储类型:变量的生命周期


4.4 作用域与存储类型:变量的生命周期

本节摘要:作用域管"名字在哪里可见",存储类型管"变量住在哪、活多久"——两个正交的维度,组合出 C 变量的全部生存形态。本节用第 1 章的内存地图逐一定位 auto、static、register、extern 四类存储,讲清 static 的双重身份与全局变量的工程代价。

两个正交的维度

初学者常把作用域和生命周期混为一谈。分开看:

  • 作用域(可见性):编译期概念。这个名字在哪些代码行可用——块作用域、文件作用域、函数原型作用域。
  • 存储期(生命周期):运行期概念。这块内存何时存在——自动存储期(栈)、静态存储期(data/bss 段)、动态存储期(堆)。

组合出的典型形态:

#include <stdio.h> int g = 1; /* 文件作用域 + 静态存储期:data 段,全程序可见 */ static int file_only = 2; /* 文件作用域被 static 收窄 + 静态存储期:私有 */ int counter(void) { int auto_v = 0; /* 块作用域 + 自动存储期:栈,每次进入归零 */ static int calls = 0; /* 块作用域 + 静态存储期:bss,可见性局部但常驻 */ auto_v++; calls++; return calls * 10 + auto_v; } int main(void) { counter(); counter(); counter(); printf("%d\n", counter()); /* 41:calls 累计到 4,auto_v 每次重置为 1 */ return g * 0 + file_only * 0; }

static int calls 是最值得咀嚼的组合:名字只在函数内可见(编译器管),内存却在 bss 段常驻(加载器管)——函数第一次被调用时初始化一次,此后在调用之间保值。这正是"调用计数器"的标准实现。

四种存储类型逐个定位

auto:块内局部变量的默认存储类,写在栈帧里,进入块创建、离开块销毁(帧作废)。几乎从不需要显式写 auto,知道它是默认值即可。

static:双重身份,按位置区分——

static int g_count; /* 文件级:限定链接性,符号私有,其他文件看不见 */ void f(void) { static int local_count; /* 块级:限定存储期,搬到静态区常驻 */ }

文件级 static 是链接器层面的封装(第 1.2 节 nm 输出里小写字母的秘密),相当于其他语言的"模块私有";块级 static 是生命周期延长。两者都住在 data/bss,都只初始化一次。

register:给编译器的建议"这个变量频繁使用,尽量放寄存器"。现代编译器的寄存器分配早已比人聪明,这个关键字的实际用途只剩一条:取不了地址——& 一个 register 变量直接编译错误,偶尔用来强制暴露"这段代码不该有指针指向它"。

extern:声明"这个变量在别处定义,链接时找我"。多文件共享全局变量的标准三步:

/* 定义唯一份(文件 A) */ int table_size = 1024; /* 头文件放声明(供其他文件包含) */ extern int table_size; /* 文件 B 包含头文件后即可使用 */

定义一次、声明多次,与函数的"定义/原型"分工完全同构。写了 extern 又带初值就退化成定义,在两个文件里都这么写就撞车——multiple definition 链接错误。

图 1 声明组合与内存归宿对照

图 1 声明组合与内存归宿对照

全局变量的工程代价

全局变量看似方便,代价清单却不短:

  1. 打破函数纯度:任何代码都可能改它,行为依赖"谁在什么时候动过它"——多线程下是灾难现场;
  2. 初始化顺序陷阱:多个文件的全局对象初始化顺序不确定(C++ 重灾区,C 里全局指针指向其他文件的变量时也要小心);
  3. 命名污染:文件级全局符号暴露给链接器,撞名即冲突;至少加 static 收编为文件私有;
  4. 测试困难:依赖全局状态的函数无法独立测试。

工程折中:全局状态集中到一个结构体,配 get/set 函数访问——可见性收窄、修改点可追溯。操作系统内核也这么做:全局状态包成单一结构,指针传递。

本节要点回顾

  • 作用域与存储期正交:名字可见范围(编译期)与内存生命周期(运行期)分开判断。
  • static 双重身份:文件级限链接性(私有符号),块级限存储期(bss 常驻保值)。
  • extern 声明、定义唯一:多文件共享变量与函数原型同构,头文件放声明。
  • register 现代只剩禁取地址的用途;auto 是默认值。
  • 全局变量代价清单:纯度、初始化顺序、命名污染、可测性——集中封装是折中之道。

第 4 章结束。带着"栈帧、地址、内存分区"的装备,进入全书的巅峰章节:指针。

常见疑问

问:同名变量在内层块遮蔽外层,机器上是怎么回事? 是两个不同的栈位置。内层声明在更深的栈偏移上占了新地盘,外层变量的地盘还在原地;内层块结束后,编译器生成的代码重新指向外层偏移。遮蔽是编译器的符号表把戏,不是内存合并。

问:全局变量会不会和局部变量重名冲突? 不会冲突,但会遮蔽。函数内的同名声明会遮蔽全局名,这容易埋下"以为改了全局其实改了局部"的 bug。工程上常用命名前缀区分(全局加 g 前缀)避免误读。

问:static 局部变量的初始化在每次调用都执行吗? 只有第一次。初始化在程序加载时完成(带初值的进 data,不带进 bss 清零),函数体内不会重新执行初始化语句——这正是调用计数器可靠工作的原因。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U