C 语言的 static 关键字作用
C 语言里的static不只有“限制符号的链接性,只在本编译单元可见”这一种功能。
更准确地说,static在 C 里根据出现位置不同,主要有几类含义:
文件作用域的变量/函数:赋予内部链接性
也就是“只在当前翻译单元可见”。块作用域的局部变量:改变存储期为静态存储期
变量只初始化一次,生命周期贯穿整个程序运行期间,但名字仍然只在当前块内可见。函数参数中的数组声明:表示调用者传入的数组至少有指定长度
这是 C99 引入的特性,例如int a[static 10]。和
inline组合时,C99/C11/C17/C23 与 GNU89 语义有差异
这是 GNU C 扩展/不同标准模式下一个很容易踩坑的地方。
下面逐个说明。
1. 文件作用域下的static:内部链接性
这是你提到的那种情况。
例如:
// a.cstaticintg=10;staticvoidfoo(void){printf("foo\n");}这里:
staticintg;staticvoidfoo(void);表示g和foo具有internal linkage,内部链接性。
也就是说,它们只能在a.c这个翻译单元中使用,别的.c文件无法通过extern访问它们。
例如:
// a.cstaticintg=10;intget_g(void){returng;}// b.cexternintg;intmain(void){returng;}编译链接时通常会报错:
undefined reference to `g`因为a.c里的g是static的,对外不可见。
所以在文件作用域下,static的确常常用于:
staticintglobal_var;staticvoidhelper_function(void);目的是隐藏符号,避免污染全局命名空间。
2. 块作用域下的static:静态存储期,而不是内部链接性
例如:
#include<stdio.h>voidcounter(void){staticintx=0;x++;printf("%d\n",x);}intmain(void){counter();counter();counter();}输出:
1 2 3这里的:
staticintx=0;不是说x具有“内部链接性”。事实上,块作用域里的局部变量没有链接性。
它的作用是:
x的生命周期从程序开始持续到程序结束;- 但名字
x只能在counter函数内部访问; - 初始化只发生一次;
- 多次调用
counter时,x保留上次的值。
对比普通局部变量:
voidcounter(void){intx=0;x++;printf("%d\n",x);}每次调用都会输出:
1 1 1所以块作用域下的static主要改变的是存储期 storage duration,不是链接性。
3. 函数参数数组中的static:至少需要多少元素
这是 C99 引入的功能。
例如:
voidf(inta[static10]){for(inti=0;i<10;i++){a[i]=i;}}这里:
inta[static10]表示调用者传进来的a至少指向 10 个int元素。
它和下面这个声明在类型层面非常接近:
voidf(int*a);但static 10给编译器额外信息:
voidf(inta[static10]);相当于告诉编译器:
我保证
a不是空指针,并且至少有 10 个元素可访问。
例如:
intarr[10];f(arr);// OK但是:
intarr[5];f(arr);// 违反约定,行为未定义或至少不符合函数要求再比如:
f(NULL);// 不符合 static 10 的要求一些编译器可能会给警告。
完整例子:
#include<stdio.h>voidfill(inta[static5]){for(inti=0;i<5;i++){a[i]=i+1;}}intmain(void){intx[5];fill(x);for(inti=0;i<5;i++){printf("%d\n",x[i]);}}输出:
1 2 3 4 5这种用法只允许出现在函数参数的数组声明中,例如:
voidf(inta[static10]);voidg(doublematrix[static3][5]);注意,下面这种不是同一种意思:
staticinta[10];这是静态存储期或者内部链接性的变量声明,取决于作用域。
4.static和inline的组合:标准 C 和 GNU C 有差异
这部分是最容易体现“不是所有版本都一样”的地方。
例如头文件里常见:
// util.hstaticinlineintadd(inta,intb){returna+b;}这通常表示:
- 每个包含这个头文件的
.c文件都拥有自己的一份add; - 函数具有内部链接性;
- 编译器可以选择内联,也可以不内联;
- 不会产生多个外部定义冲突。
因此static inline是头文件中定义小函数的常见写法。
例如:
// util.h#ifndefUTIL_H#defineUTIL_Hstaticinlineintsquare(intx){returnx*x;}#endif// a.c#include"util.h"intfa(intx){returnsquare(x);}// b.c#include"util.h"intfb(intx){returnsquare(x);}这是安全的,因为square在每个翻译单元中都是内部链接。
但是inline本身在 GNU89 和 C99 语义不同
例如:
inlineintfoo(intx){returnx+1;}在 C99/C11/C17/C23 标准语义和 GNU89 语义下,外部定义生成规则不同。
简单说:
| 写法 | C99 语义 | GNU89 语义 |
|---|---|---|
inline int f() | 通常不提供外部定义 | 通常会生成外部定义 |
extern inline int f() | 通常提供外部定义 | 通常不生成外部定义 |
static inline int f() | 内部链接,较稳定 | 内部链接,较稳定 |
因此在跨 C 标准和 GNU C 模式时,static inline通常最不容易出问题。
例如 GCC 有这些模式:
gcc-std=c89 gcc-std=c99 gcc-std=c11 gcc-std=c17 gcc-std=c2x gcc-std=gnu89 gcc-std=gnu99 gcc-std=gnu11 gcc-std=gnu17 gcc-std=gnu2x在gnu89模式和c99/gnu99之后,inline的语义可能不同。
不过static inline的直观效果通常仍然是:
staticinlineintf(intx){returnx+1;}即:
f具有内部链接性;- 每个翻译单元可有自己的
f; - 编译器可以内联,也可以不内联。
5. 不同 C 标准中static功能是否完全一样?
不完全一样。
大致如下:
| C 版本 | static的主要功能 |
|---|---|
| C89/C90 | 文件作用域内部链接性;块作用域静态存储期 |
| C99 | 增加函数参数数组中的static;引入标准inline,和static inline配合使用 |
| C11 | 基本继承 C99;增加_Thread_local,可与static组合 |
| C17/C18 | 基本继承 C11 |
| C23 | 基本继承上述语义,语法细节有所现代化 |
| GNU C | 支持标准功能,另有 GNU inline 语义等扩展差异 |
6. C11 之后:static和_Thread_local
C11 引入了线程局部存储:
_Thread_localintx;可以和static组合。
例如文件作用域:
static_Thread_localintcounter;表示:
counter具有内部链接性;- 每个线程都有自己的一份
counter。
例如:
static_Thread_localinttls_value=0;这和普通:
staticintvalue=0;不同。
普通static int value是整个程序共享一个对象。
而:
static_Thread_localinttls_value;是每个线程一份对象,并且符号只在本翻译单元可见。
7. 几个对比例子
例 1:文件作用域变量
staticintx=1;如果写在函数外:
// file scopestaticintx=1;含义:
- 静态存储期;
- 内部链接性;
- 只能在当前
.c文件中通过名字访问。
例 2:函数内局部变量
voidf(void){staticintx=1;}含义:
- 静态存储期;
- 无链接性;
- 名字
x只在f内可见; - 不是“只在本编译单元可见”的问题,而是只在这个块里可见。
例 3:函数声明
staticvoidhelper(void);如果在文件作用域:
staticvoidhelper(void){}含义:
helper具有内部链接性;- 只能在当前翻译单元调用;
- 常用于
.c文件内部的辅助函数。
例 4:函数参数数组
voidsort(inta[static100]){// 可以假设 a 至少有 100 个 int}含义:
a仍然会调整为指针参数;- 但调用者必须保证至少有 100 个元素;
- 这是 C99 之后才有的语义。
例 5:头文件中的static inline
// math_util.hstaticinlineintmax_int(inta,intb){returna>b?a:b;}含义:
- 每个包含此头文件的
.c文件都有自己的max_int; - 不会造成多个定义的链接错误;
- 编译器可以内联。
总结
你的说法:
static的功能是“限制符号的链接性,只在本编译单元可见”
只对一部分情况成立,主要是:
staticintglobal_var;staticvoidfunction(void);也就是文件作用域下的变量和函数。
但static还有其他功能:
voidf(void){staticintx;}这里表示静态存储期,不是链接性。
voidg(inta[static10]);这里表示函数参数a至少指向 10 个元素,这是 C99 引入的数组参数约束。
所以不能简单地说 C 语言中的static就是“限制链接性”。
更准确的总结是:
static是一个存储类说明符。在文件作用域下,它通常赋予标识符内部链接性;在块作用域下,它让对象具有静态存储期;在函数参数数组声明中,它表示实参数组至少具有指定长度。不同 C 标准和 GNU C 模式下,特别是与inline组合时,语义存在差异。
