C 语言里的 static 不只有"限制符号的链接性,只在本编译单元可见"这一种功能。
更准确地说,static 在 C 里根据出现位置不同,主要有几类含义:
-
文件作用域的变量/函数:赋予内部链接性
也就是"只在当前翻译单元可见"。
-
块作用域的局部变量:改变存储期为静态存储期
变量只初始化一次,生命周期贯穿整个程序运行期间,但名字仍然只在当前块内可见。
-
函数参数中的数组声明:表示调用者传入的数组至少有指定长度
这是 C99 引入的特性,例如
int a[static 10]。 -
和
inline组合时,C99/C11/C17/C23 与 GNU89 语义有差异这是 GNU C 扩展/不同标准模式下一个很容易踩坑的地方。
下面逐个说明。
1. 文件作用域下的 static:内部链接性
这是你提到的那种情况。
例如:
c
// a.c
static int g = 10;
static void foo(void)
{
printf("foo\n");
}
这里:
c
static int g;
static void foo(void);
表示 g 和 foo 具有 internal linkage,内部链接性。
也就是说,它们只能在 a.c 这个翻译单元中使用,别的 .c 文件无法通过 extern 访问它们。
例如:
c
// a.c
static int g = 10;
int get_g(void)
{
return g;
}
c
// b.c
extern int g;
int main(void)
{
return g;
}
编译链接时通常会报错:
text
undefined reference to `g`
因为 a.c 里的 g 是 static 的,对外不可见。
所以在文件作用域下,static 的确常常用于:
c
static int global_var;
static void helper_function(void);
目的是隐藏符号,避免污染全局命名空间。
2. 块作用域下的 static:静态存储期,而不是内部链接性
例如:
c
#include <stdio.h>
void counter(void)
{
static int x = 0;
x++;
printf("%d\n", x);
}
int main(void)
{
counter();
counter();
counter();
}
输出:
text
1
2
3
这里的:
c
static int x = 0;
不是说 x 具有"内部链接性"。事实上,块作用域里的局部变量没有链接性。
它的作用是:
x的生命周期从程序开始持续到程序结束;- 但名字
x只能在counter函数内部访问; - 初始化只发生一次;
- 多次调用
counter时,x保留上次的值。
对比普通局部变量:
c
void counter(void)
{
int x = 0;
x++;
printf("%d\n", x);
}
每次调用都会输出:
text
1
1
1
所以块作用域下的 static 主要改变的是 存储期 storage duration,不是链接性。
3. 函数参数数组中的 static:至少需要多少元素
这是 C99 引入的功能。
例如:
c
void f(int a[static 10])
{
for (int i = 0; i < 10; i++) {
a[i] = i;
}
}
这里:
c
int a[static 10]
表示调用者传进来的 a 至少指向 10 个 int 元素。
它和下面这个声明在类型层面非常接近:
c
void f(int *a);
但 static 10 给编译器额外信息:
c
void f(int a[static 10]);
相当于告诉编译器:
我保证
a不是空指针,并且至少有 10 个元素可访问。
例如:
c
int arr[10];
f(arr); // OK
但是:
c
int arr[5];
f(arr); // 违反约定,行为未定义或至少不符合函数要求
再比如:
c
f(NULL); // 不符合 static 10 的要求
一些编译器可能会给警告。
完整例子:
c
#include <stdio.h>
void fill(int a[static 5])
{
for (int i = 0; i < 5; i++) {
a[i] = i + 1;
}
}
int main(void)
{
int x[5];
fill(x);
for (int i = 0; i < 5; i++) {
printf("%d\n", x[i]);
}
}
输出:
text
1
2
3
4
5
这种用法只允许出现在函数参数的数组声明中,例如:
c
void f(int a[static 10]);
void g(double matrix[static 3][5]);
注意,下面这种不是同一种意思:
c
static int a[10];
这是静态存储期或者内部链接性的变量声明,取决于作用域。
4. static 和 inline 的组合:标准 C 和 GNU C 有差异
这部分是最容易体现"不是所有版本都一样"的地方。
例如头文件里常见:
c
// util.h
static inline int add(int a, int b)
{
return a + b;
}
这通常表示:
- 每个包含这个头文件的
.c文件都拥有自己的一份add; - 函数具有内部链接性;
- 编译器可以选择内联,也可以不内联;
- 不会产生多个外部定义冲突。
因此 static inline 是头文件中定义小函数的常见写法。
例如:
c
// util.h
#ifndef UTIL_H
#define UTIL_H
static inline int square(int x)
{
return x * x;
}
#endif
c
// a.c
#include "util.h"
int fa(int x)
{
return square(x);
}
c
// b.c
#include "util.h"
int fb(int x)
{
return square(x);
}
这是安全的,因为 square 在每个翻译单元中都是内部链接。
但是 inline 本身在 GNU89 和 C99 语义不同
例如:
c
inline int foo(int x)
{
return x + 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 有这些模式:
bash
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 的直观效果通常仍然是:
c
static inline int f(int x)
{
return x + 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 引入了线程局部存储:
c
_Thread_local int x;
可以和 static 组合。
例如文件作用域:
c
static _Thread_local int counter;
表示:
counter具有内部链接性;- 每个线程都有自己的一份
counter。
例如:
c
static _Thread_local int tls_value = 0;
这和普通:
c
static int value = 0;
不同。
普通 static int value 是整个程序共享一个对象。
而:
c
static _Thread_local int tls_value;
是每个线程一份对象,并且符号只在本翻译单元可见。
7. 几个对比例子
例 1:文件作用域变量
c
static int x = 1;
如果写在函数外:
c
// file scope
static int x = 1;
含义:
- 静态存储期;
- 内部链接性;
- 只能在当前
.c文件中通过名字访问。
例 2:函数内局部变量
c
void f(void)
{
static int x = 1;
}
含义:
- 静态存储期;
- 无链接性;
- 名字
x只在f内可见; - 不是"只在本编译单元可见"的问题,而是只在这个块里可见。
例 3:函数声明
c
static void helper(void);
如果在文件作用域:
c
static void helper(void)
{
}
含义:
helper具有内部链接性;- 只能在当前翻译单元调用;
- 常用于
.c文件内部的辅助函数。
例 4:函数参数数组
c
void sort(int a[static 100])
{
// 可以假设 a 至少有 100 个 int
}
含义:
a仍然会调整为指针参数;- 但调用者必须保证至少有 100 个元素;
- 这是 C99 之后才有的语义。
例 5:头文件中的 static inline
c
// math_util.h
static inline int max_int(int a, int b)
{
return a > b ? a : b;
}
含义:
- 每个包含此头文件的
.c文件都有自己的max_int; - 不会造成多个定义的链接错误;
- 编译器可以内联。
总结
你的说法:
static的功能是"限制符号的链接性,只在本编译单元可见"
只对一部分情况成立,主要是:
c
static int global_var;
static void function(void);
也就是 文件作用域 下的变量和函数。
但 static 还有其他功能:
c
void f(void)
{
static int x;
}
这里表示 静态存储期,不是链接性。
c
void g(int a[static 10]);
这里表示函数参数 a 至少指向 10 个元素,这是 C99 引入的数组参数约束。
所以不能简单地说 C 语言中的 static 就是"限制链接性"。
更准确的总结是:
static是一个存储类说明符。在文件作用域下,它通常赋予标识符内部链接性;在块作用域下,它让对象具有静态存储期;在函数参数数组声明中,它表示实参数组至少具有指定长度。不同 C 标准和 GNU C 模式下,特别是与inline组合时,语义存在差异。