本节摘要:作用域管"名字在哪里可见",存储类型管"变量住在哪、活多久"——两个正交的维度,组合出 C 变量的全部生存形态。本节用第 1 章的内存地图逐一定位 auto、static、register、extern 四类存储,讲清 static 的双重身份与全局变量的工程代价。
初学者常把作用域和生命周期混为一谈。分开看:
组合出的典型形态:
#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 链接错误。

全局变量看似方便,代价清单却不短:
工程折中:全局状态集中到一个结构体,配 get/set 函数访问——可见性收窄、修改点可追溯。操作系统内核也这么做:全局状态包成单一结构,指针传递。
第 4 章结束。带着"栈帧、地址、内存分区"的装备,进入全书的巅峰章节:指针。
问:同名变量在内层块遮蔽外层,机器上是怎么回事? 是两个不同的栈位置。内层声明在更深的栈偏移上占了新地盘,外层变量的地盘还在原地;内层块结束后,编译器生成的代码重新指向外层偏移。遮蔽是编译器的符号表把戏,不是内存合并。
问:全局变量会不会和局部变量重名冲突? 不会冲突,但会遮蔽。函数内的同名声明会遮蔽全局名,这容易埋下"以为改了全局其实改了局部"的 bug。工程上常用命名前缀区分(全局加 g 前缀)避免误读。
问:static 局部变量的初始化在每次调用都执行吗? 只有第一次。初始化在程序加载时完成(带初值的进 data,不带进 bss 清零),函数体内不会重新执行初始化语句——这正是调用计数器可靠工作的原因。