关于每个变量的两个问题
C 语言中的每个变量都有两个不同的属性,把它们混为一谈正是本页大多数意外的根源:
- 作用域:这个名字可以在哪里使用。这是编译期的问题。
- 生存期:这块存储存在多久。这是运行期的问题。
两者通常是同步的(普通局部变量在它的块中可见,也恰好存在那么久),但 static 会把它们分开,这就是为什么这个关键字值得在这里弄懂。
块作用域
在花括号内声明的变量,从声明处到右花括号之间可见,其他地方都不可见。
内层块能看到 outer,因为它嵌套在 outer 所在的块中。外层块看不到 inner。这一规则适用于每一对花括号:函数体、if 语句体、循环体,或者像上面那样单独的块。
循环让这一点更具体。在 for 头部声明的计数器属于这个循环:
for (int i = 0; i < 5; i++) {
/* i lives here */
}
/* i does not exist here */
这正是你想要的:同一个函数中的两个循环可以各自使用 i,互不干扰,循环结束后也不会意外读到这个计数器。如果你确实需要在循环之后用到它的值,比如查找停下来时的下标,就在循环之前声明它。
函数参数的作用域是整个函数体,所以它们的行为就像在函数体开头声明的局部变量。
C99 允许在块中的任何位置声明变量,而不仅限于开头。在第一次需要时再声明变量;作用域越短的变量,出错的方式就越少。
生存期:自动存储
普通局部变量具有自动存储期:控制流进入它所在的块时它被创建,离开时被销毁。它的内存位于栈上。
它会打印三次 1。每次调用都会得到一个全新的 count,初始化为 0,返回时被丢弃。这也是为什么递归函数的每个栈帧都有自己的一份局部变量副本。
有两个后果值得明确指出。未初始化的局部变量包含的是垃圾值,而不是零,也就是栈上原本的那些字节。另外,返回指向局部变量的指针是未定义行为,因为函数一返回,这块存储就消失了:
/* BUG: buf does not exist after the return */
char *broken(void) {
char buf[64] = "hello";
return buf;
}
static 局部变量:在多次调用之间保留值
给局部变量加上 static,它的生存期就变成整个程序运行期间,而作用域完全不变。
现在计数器打印 1、2、3。= 0 只在 main 开始之前执行一次,而不是每次调用都执行。
static 局部变量的三条规则:
- 初始化表达式必须是常量表达式,因为它是在程序启动时应用的,而不是在运行期。
- 没有初始化表达式时,
static变量会被初始化为零(这和保存垃圾值的自动局部变量不同)。 - 这个变量仍然只属于该函数。其他代码都无法通过名字访问它。
最后一点正是它的全部魅力:函数可以在多次调用之间记住某些东西,而不必暴露一个谁都能修改的全局变量。用它来做调用计数器、首次使用时构建的缓存查找表,以及只执行一次的初始化标志。
代价是这个函数不再是纯粹的“输入到输出”的映射,相同的参数可能得到不同的结果,这让它更难测试,而且在没有保护的情况下,多个线程同时调用它也不安全。要有意识地使用 static 局部变量,而不是图方便。
文件作用域:全局变量
在所有函数之外声明的变量具有文件作用域。它从声明处到文件末尾都可见,下面的每个函数都能看到它,它的生存期是整个程序运行期间。
全局变量默认初始化为零,所以即使没有 = 0,totalOperations 也从 0 开始。
全局变量默认还具有外部链接:同一程序中的其他 .c 文件可以通过 extern 声明访问它:
/* in stats.c */
int totalOperations = 0; /* the definition - exactly one in the program */
/* in main.c */
extern int totalOperations; /* a declaration: "it exists somewhere" */
把 extern 声明放在头文件里,让每个使用者看到的都是同一个声明。注意这种不对称:extern 声明只是说明这个变量存在,而必须有且只有一个 .c 文件真正定义它。
全局变量为什么会坑人
全局变量是共享数据最简单的方式,也是制造无法定位的 bug 最简单的方式。四个具体问题:
任何代码都能修改它们。 当 totalOperations 的值不对时,罪魁祸首可能是任何文件中的任何一行。如果用参数,嫌疑对象就只是你能看到的那些调用处。
函数变得无法测试。 读取全局变量的函数无法单独调用;你得先设置好全局状态,之后还要记得重置它。
名字在整个程序中有效。 叫 count 或 buffer 的全局变量迟早会和别人的撞名。
它们的初始化表达式必须是编译期常量。 在 C 语言中,全局变量只能用常量表达式初始化:int limit = readConfig(); 无法编译,文件作用域中的 int b = a * 2; 也不行。任何需要计算的值都只能在运行时由某个初始化函数赋值,而在那次调用之前访问这个全局变量的每个文件,看到的都是一个悄无声息的 0。
替代方案几乎总是存在的:把值作为参数传入,返回结果,并把相关的状态打包成一个结构体显式地传来传去。确实存在例外,比如程序级的配置对象、日志句柄,但即便是这些,更好的做法也是把它们设为文件作用域的 static,再提供访问函数,这就是下一节的内容。
文件作用域中的 static:私有化
用在全局变量或函数上时,static 的含义和用在局部变量上时完全不同:它表示内部链接。这个名字变为它所在 .c 文件私有,其他任何文件都无法访问。
/* counter.c */
#include "counter.h"
static int count = 0; /* private to this file - no other file can touch it */
static void validate(void) { /* a private helper, not part of the interface */
if (count < 0) count = 0;
}
void increment(void) { /* public: declared in counter.h */
count++;
validate();
}
int getCount(void) { /* public */
return count;
}
其他文件可以调用 increment 和 getCount,但看不到 count,也无法调用 validate,链接器不会解析这些名字。这就是 C 语言(简陋的)模块系统:一个声明公开函数的头文件,其余一切都加上 static。
除了整洁之外,还有两个实际好处。两个文件可以各自有一个 static void validate(void) 而不会冲突,如果这些函数是公开的就做不到。另外,编译器知道 static 函数在文件之外没有调用者,所以可以更积极地内联它,或者在它未被使用时发出警告。
所以这个关键字有两种截然不同的含义,由它出现的位置决定:
static on a LOCAL variable -> lifetime becomes permanent (scope unchanged)
static at FILE scope -> linkage becomes internal (lifetime unchanged)
变量遮蔽
在内层作用域中声明一个外层作用域已经存在的名字,就会遮蔽外层的名字:在内层作用域中,这个名字指的是新变量,外层的那个无法访问。
这是合法的,偶尔也是有意为之。但更多时候它是个意外,而且代价不小:一个本应更新全局变量的函数,实际更新的却是同名的局部变量,全局变量始终没变。局部变量遮蔽参数时也是一样,你本想赋给输入参数的值落了空。
gcc -Wshadow 会报告每一处遮蔽。它不包含在 -Wall 中,所以要显式打开:
gcc -Wall -Wextra -Wshadow program.c -o program
彻底避免这个问题的习惯是按用途而不是按类型命名:totalScore 和 itemScore 不会互相遮蔽,而两个都叫 n 的变量迟早会。
速查表
declared inside a block block scope, automatic lifetime, garbage if uninitialised
function parameter block scope of the function body, a copy of the argument
static inside a function block scope, PROGRAM lifetime, zero-initialised, kept between calls
declared outside all functions file scope, program lifetime, zero-initialised, visible to other files
static outside all functions file scope, program lifetime, PRIVATE to this .c file
extern declaration names a variable defined in another file
常见问题
C 语言中的作用域是什么?
一个名字在代码中可见的区域。在块(一对花括号)内声明的变量只在这个块中可见,这就是块作用域。在所有函数之外声明的变量具有文件作用域,从声明处到文件末尾都可见。
static 对 C 语言局部变量有什么作用?
它改变变量的生存期,而不改变它的作用域。static 局部变量只创建一次、只初始化一次,并在多次调用之间保留它的值,但它仍然只在所在的函数内可见。这是让函数拥有“记忆”而又不使用全局变量的方法。
C 语言中 static 和全局变量有什么区别?
普通全局变量对程序中的每个文件都可见,其他文件可以通过 extern 访问它。文件作用域中的 static 变量或函数只属于那一个 .c 文件,其他文件无法链接到它。文件作用域中的 static 关乎隐藏;局部变量上的 static 关乎生存期。
C 语言中为什么全局变量不好?
任何函数都能修改它们,所以 bug 可能来自程序的任何地方;读取它们的函数无法单独测试;而且这个名字占据整个程序,容易引起冲突。改为把值作为参数传入并返回结果;在确实需要共享状态的地方,把它设为文件作用域的 static,让只有一个文件能访问它。