最近在技术群里看到一个关于C语言static关键字的有趣讨论,正好抽时间整理出来。说起来,这个问题并不算复杂,但背后牵扯的编译原理细节,确实值得拿出来好好掰扯掰扯。
1 问题来源
就在上个星期,群里收到一个有关static的讨论,原问题如下:
大佬们,问一个比较低智问题,static修饰的变量不是只能被当前文件所调用吗?为什么这里在其他.c文件可以调用get_board_led_opr这个函数获取board_demo_led_opr这个变量的值。

2 关于上述问题的答疑
群里当时给出了不少参考答案。不过一开始回答时,并没有直接给答案,而是先抛出了三个追问:
- get函数返回什么地址?语法层面对各种地址有约束吗?
- static修饰的全局变量存放在哪里?它的地址是什么样的?
- C语言里想访问任意一个地址,从语法层面上,能阻止得了吗?
提问者自己的理解是:
(1)get_board_led_opr函数返回的是一个自定义的结构体,但这个结构体被static修饰了。
(2)结构体放在board_demo.c中,而调用函数是在leddrv.c中。
(3)后来群里还有人说,static仅仅在编译期间有效,运行时就没什么限制了。所以虽然leddrv.c无法直接使用那个结构体,但可以通过函数间接获取它,因为函数和结构体在同一个.c文件里。
问题的关键其实就两点:
- 被static修饰之后,这个变量变成了什么?
- C语言的万能指针。
首先,被static修饰意味着只有当前C文件能直接引用访问它,但并不代表其他文件就无法访问。直接访问不行,但间接方式完全可行。方法就像上面的代码片段:把static变量的地址以指针形式传出去给其他模块。还有一点要理解:被static修饰的变量存放在全局数据区,它的生命周期是整个工程运行期间。所以你通过指针去访问它,就成了可能——这正是学习指针时提到的“间接访问”最真实的体现。
3 教科书里的static
乘此机会,来复习一下教科书里对static的描述。手头标准教材难找,索性问了问ChatGPT,它的总结大致如下:
在C语言中,static关键字有两种主要用法:静态变量和静态函数。
静态变量:
静态变量在程序运行期间一直存在,生命周期从声明处到程序结束。它被存储在静态数据区,不会受函数调用和返回的影响,每次进入函数都保留上一次的值。静态变量可以在函数内部或全局作用域中声明。函数内部声明的静态变量只能在该函数内访问,全局作用域中声明的静态变量可以在整个文件中访问。主要作用:维护变量的持久状态、限制作用域、避免与同名全局变量冲突。
void func()
{
static int count = 0; // 定义一个静态变量
count++;
printf("Count: %d\n", count);
}
int main()
{
func(); // 输出:Count: 1
func(); // 输出:Count: 2
func(); // 输出:Count: 3
return 0;
}
静态函数:
静态函数只能在当前源文件中使用,作用域限于声明所在的文件,无法被其他文件调用。使用static修饰函数,可以实现函数的隐藏和封装,避免命名冲突,提高代码可读性和可维护性。
static void util_func(int value)
{
printf("Value: %d\n", value);
}
int main()
{
util_func(10); // 可以在当前文件中调用util_func函数
return 0;
}
ChatGPT的总结基本到位,不过有一点它没提:static修饰的静态变量有两种情况——修饰全局变量叫静态全局变量,修饰局部变量叫静态局部变量(其实从生命周期看,它也是全局的)。上面演示的静态局部变量,作用域仅限于定义它的函数内;一旦出了这个作用域,就没办法访问到了。而静态全局变量的作用域更宽泛,允许定义它的整个C文件内访问,但其他C文件无法访问。总之,static关键字主要强调“静态”这个含义,本质就是作用域的大小问题。
4 C语言的static究竟限制了谁?
从编译原理和编译流程来拆解这个问题,答案很快就出来了。以前写过一篇关于C代码编译流程的文章,这里直接贴一张图作为参考:

顺着这个思路,来跟踪一下代码中的static。
编写C代码
// main.c
#include
static int g_count = 0;
void func()
{
static int l_count = 0; // 静态局部变量
l_count++;
g_count++;
printf("l_count: %d, g_count: %d\n", l_count, g_count);
}
static void util_func(int value) //静态函数
{
printf("Value: %d\n", value);
}
extern void test_func(void);
int main()
{
func(); //输出:l_count:1
func(); //输出:l_count:2
func(); //输出:l_count:3
util_func(10); //可以在当前文件中调用util_func函数,输出:Value: 10
test_func(); // 调用test.c中的函数
return 0;
}
// test.c
// 假设它们都是可以访问的
extern int l_count;
extern int g_count;
extern void util_func(int value);
void test_func(void)
{
#if 0
l_count++; // 访问l_count静态局部变量,编译报错
g_count++; // 访问g_count静态全局变量,编译报错
util_func(100); // 调用main.c中的static函数,编译报错
#endif
}
当test_func中的if-0打开后,编译会报链接错误:
test_static$ gcc -o test *.c
/usr/bin/ld: /tmp/ccawkyDR.o: in function `test_func': test.c:(.text+0xa): undefined reference to `l_count'
/usr/bin/ld: test.c:(.text+0x13): undefined reference to `l_count'
/usr/bin/ld: test.c:(.text+0x19): undefined reference to `g_count'
/usr/bin/ld: test.c:(.text+0x22): undefined reference to `g_count'
/usr/bin/ld: test.c:(.text+0x2c): undefined reference to `util_func'
collect2: error: ld returned 1 exit status
这正是找不到变量或函数实现体的链接错误。去掉这些引用(if-0打开)后就能编译成功且正常运行:
test_static$ gcc -o test *.c
test_static$ ./test
l_count: 1, g_count: 1
l_count: 2, g_count: 2
l_count: 3, g_count: 3
Value: 10
分步拆解1:预处理后的static
使用命令:gcc -c -o main.o main.c -sa ve-temps=obj,得到main.i、main.s、main.o。查看main.i,static还在,代码基本保留原样(没有用到宏定义)。
拆解第2步:汇编文件里还有static的影子吗?
查看main.s文件,内容较长,这里只抓取关键部分:
.file "main.c"
.text
.local g_count
.comm g_count,4,4
...
.type util_func, @function
util_func:
...
.local l_count.2316
.comm l_count.2316,4,4
可以看到,在汇编文件中,static关键字消失了,但取而代之的是.local和.comm等伪指令。也就是说,static已经转为其他信息了。
拆解第3步:obj文件中确认没有static?
用readelf -a main.o查看,找到以下符号:
5: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 g_count
7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 l_count.2316
8: 0000000000000048 40 FUNC LOCAL DEFAULT 1 util_func
g_count、l_count、util_func都被标记为LOCAL,而不是GLOBAL。这就是关键所在。
拆解第4步:链接后的可执行文件反汇编
执行objdump -l -d -x -s -S test,结果很长,但照样能找到l_count、g_count、util_func的地址信息。从最终的反汇编看,static的修饰效果在链接阶段体现为符号的可见性控制。
综上可知,static的修饰主要是给编译器做了提示,生成的obj文件中带上了local属性,告诉链接器不能扩大这些内容的使用范围,从而达到了教科书上说的static的作用效果。另外还有一个方法:写一个带static和不带static的全局变量,通过反汇编对比分析,也能快速看出核心区别,有兴趣的读者可以自己试试看。
再留一个疑问:编译器、链接器如何做到一个static修饰的局部变量只能被当前函数内访问?这个问题有点深入,也留给读者去探索吧。
5 一种绕开static限制的方法
说到绕开static的限制,其实第一小节的那个问题例子就是一个好思路。方法无非两种:
- 将被static修饰的函数转换为没有被static修饰的函数
- 将被static修饰的变量转换为没有被static修饰的变量
这种做法在行业里叫“封装”。第一小节的代码演示了将static变量通过传出地址的方式导出给外部模块使用。下面再展示一个例子:将被static修饰的函数转换为不被static修饰的函数,从而给外部模块使用。
// test_a.c
static int test_func_in_a(int a)
{
return a * 2;
}
int test_func_in_a_ex(int a)
{
return test_func_in_a(a);
}
// test_b.c
int test_func_in_b(void)
{
int a = 1;
int b;
//call test_func_in_a function fail because of "static"
//b = test_func_in_a(a)
//call test_func_in_a_ex function ok without "static"
b = test_func_in_a_ex(a);
return b;
}
代码很简单:在test_a.c中写一个不被static修饰的函数test_func_in_a_ex,让它调用被static修饰的test_func_in_a,这样就轻松绕开了static的限制。你学会了吗?
6 拓展延伸:如何调用静态库里的被static修饰的函数?
关于这个拓展延伸,其实很早就想写一篇博文,但一直拖到现在。索性在这里做个牵引吧。个人认为,这个问题应该分为两个场景:
- 静态库的代码你有修改权限,或者能找到人来修改(比如公司内部不同团队之间的公有库代码)。
- 静态库的代码你完全没办法查看源码或修改源码(比如第三方库)。
针对这两种不同场景,采取的方式可能不一样。你觉得应该如何做最好呢?
