Skip to content

作用域、存储期与 static:名字看得见、变量活得久

引言:两个常被搅混的概念

「这个变量在哪能用?」和「这个变量活多久?」是两个不同的问题,但新手经常把它们搅成一团。C 用两个独立的概念分别回答:**作用域(scope)**回答「名字在哪段代码里能被看见」、存储期(storage duration)回答「变量占的这块内存活多久」。而 static 这个关键字之所以让人迷糊,就是因为它同时影响这两个概念、而且在「局部」和「文件作用域」两种位置上效果不一样。这一章把作用域、存储期、staticextern 一次讲透。

作用域:名字的可见范围

作用域规定「一个名字从哪到哪能被引用」。C 有四种作用域(ISO/IEC 9899 §6.2.1),最常用的是块作用域文件作用域。块作用域就是「在一对花括号 {} 内声明的名字,只在这对花括号里可见」;文件作用域是「在所有函数之外声明的名字(全局变量),从声明处到文件末尾都可见」。真跑一个块作用域的经典现象——内层块的同名变量「隐藏」外层的

c
#include <stdio.h>

int global = 100; /* 文件作用域:整个文件可见 */

int main(void) {
    int x = 1; /* main 块作用域 */
    {
        int x = 2; /* 内层块:这个 x 隐藏外层的 x */
        printf("块内 x = %d\n", x);
    }
    printf("外层 x = %d, global = %d\n", x, global);
    return 0;
}
text
$ gcc -std=c11 -Wall scope.c -o sp && ./sp
块内 x = 2
外层 x = 1, global = 100

内层 { int x = 2; } 声明了一个新的 x,它隐藏了外层的 x——所以在内层块里打印 x2;一出内层块,内层那个 x 就「没了」,外层的 x(值为 1)重新可见。global 在函数外声明、是文件作用域,函数里随时能用(得 100)。这种「内层隐藏外层」是合法的,但写同名变量隐藏外层是坏习惯(容易让人读代码时搞混「这个 x 到底是哪个」),除非有特别理由,别这么干;要避免「名字冲突」,给变量起有区分度的名字比靠作用域隐藏靠谱。

另外两种作用域顺便提一下:函数作用域只有「语句标号」(给 goto 跳的 label:)才有,整个函数内有效;原型作用域是函数原型参数列表里的名字(int f(int x); 里的 x),只在那个原型里有效(其实你完全可以写 int f(int); 不写名字)。

存储期:变量活多久

存储期规定「变量占的那块内存什么时候分配、什么时候释放」(§6.2.4)。C 有四种。自动存储期:普通的局部变量,进函数/块时在栈上分配、出了就回收——这是默认的、最常见的。静态存储期:全局变量、static 局部变量,它们在程序启动时就分配好(落在 .data/.bss 段)、一直活到程序结束——所以它们的值能跨函数调用保持(第 8 章那个 static int count 就是)。动态存储期:靠 malloc/free(阶段 2)手动管理的内存——你决定它什么时候分配、什么时候释放,堆上。线程存储期(C11 的 _Thread_local):每个线程一份,多线程时用。

关键区分:作用域是「编译期」的概念(编译器检查名字可见性)、存储期是「运行期」的概念(程序运行时内存的生命周期)。一个变量可以「作用域很小、但活得很久」(static 局部:只在函数内可见、但活到程序结束),也可以「作用域很大、活得短」(没有这种合法组合,但理解两者独立就行)。

static 的三重含义

static 是这一章的主角,它根据用在哪、效果不同,所以要分位置记:

用在局部变量上(函数体内):改变的是存储期——从「自动」变成「静态」,变量只在程序启动时初始化一次、之后值跨调用保持(第 8 章已演示 counter() 连调得 1/2/3)。作用域不变(还是只有这个函数能看见)。

用在文件作用域的变量上(函数外的全局变量)、或用在函数上:改变的是链接性——从「external(外部链接,别的翻译单元能引用)」变成「internal(内部链接,只在本翻译单元可见)」。第 1 章我们已经用 nm 真跑过这个对比:static int secretnm 里是小写 d(internal,只在本文件)、而普通 int visible 是大写 D(external,对外可见)。给函数加 static 同理——这个函数变成「模块私有」,别的 .c 文件链接时看不到它、也调不到。

所以一句口诀:static 在局部改「活多久」(存储期)、在全局/函数改「谁看得见」(链接性)。工程里常用「文件作用域 static 函数/变量」来实现模块的「私有」辅助函数和内部状态——对外只暴露那些没加 static 的「公开接口」,把实现细节藏起来,这是 C 模块化的基本手法(C 没有 private 关键字,static 就是它的私有机制)。

extern:引用别处定义的变量

static 相对的是 extern:它声明「这个变量在别处定义、我这里只是引用它的类型」,本身不分配存储。第 1 章那个 counter.h 里的 extern int counter; 就是——counter 的定义在 counter.c 里,别的翻译单元通过这个 extern 声明知道「有这么个 int counter」、链接时解析到定义。函数原型本身就有「extern」的含义(函数默认 external 链接),所以函数声明不用写 extern(写了也对、但多余)。

小结

作用域(§6.2.1,编译期「名字在哪可见」)和存储期(§6.2.4,运行期「内存活多久」)是两个独立的概念。作用域主要是块作用域({} 内)和文件作用域(全局);内层块的同名变量会隐藏外层(真跑块内 x=2、外层 x=1),但写同名隐藏是坏习惯。存储期有四种:自动(栈上局部,默认)、静态(全局/static 局部,程序全程,落在 .data/.bss)、动态(malloc/free,阶段 2)、线程(_Thread_local)。static 三重含义:用在局部变量改存储期(变静态、跨调用保持,第 8 章)、用在文件作用域变量/函数改链接性(变 internal、只在本翻译单元,第 1 章 nm 小写 d/t)——口诀「局部改活多久、全局改谁看得见」,它是 C 模块化的「私有」机制。extern 反过来,声明「别处定义、我引用」,不分配存储。下一章我们看数组。

参考资源

  • ISO/IEC 9899:2011 §6.2.1(作用域:块/文件/函数/原型)、§6.2.4(存储期:自动/静态/线程/动态)、§6.2.2(链接性:external/internal/none)
  • 第 1 章:程序结构与编译四阶段(链接 + nm 大小写、extern)、第 8 章:函数(static 局部变量的存储期)
  • 阶段 2:指针与内存(动态存储期 malloc/free