C语言 extern 和 static
摘抄AI的,豆包,置顶了。
如果main.c 有个全局变量uint8_t on_set = 9;
other.c 想根据 on_set 的值进行业务操作。
怎么操作
第一种,使用 extern
//main.c
#include <stdint.h>
uint8_t on_set = 9; //【变量定义】分配内存+生成全局符号on_set
//other.c
#include <stdint.h>
extern uint8_t on_set; //【变量声明】只告诉编译器类型,不定义、不开内存
void test(void){
if(on_set ==9){ //业务代码 }
}
阶段 1:预处理 (-E)
main.c: uint8_t on_set =9;
other.c:extern uint8_t on_set; void test(){if(on_set==9){}}
extern是编译期关键字,预处理直接保留,不被优化。
阶段 2:编译 (-S) → 生成汇编.s
main.s汇编:
.globl on_set # .globl:把on_set标记为【全局符号】,可供其他.o链接查找
.data # 放到全局数据段(已初始化全局变量区)
on_set:
.byte 9 # 在.data段分配1字节内存,初始值9
other.s 汇编:
test:
movzbl on_set(%rip),%eax # 引用符号on_set,但**不知道on_set地址**
cmpb $9,%al
ret
阶段 3:汇编 (-c) → 生成 main.o、other.o(ELF 目标文件)
用nm查看符号表:
nm main.o
0000000000000000 D on_set # D=Data段:已定义、占用内存,全局符号
nm other.o
U on_set # U=Undefined:未定义符号,需要链接时去别的.o匹配
0000000000000000 T test # T=Text代码段,函数符号
阶段 4:链接 ld(gcc 最后一步链接)
链接器核心工作:符号重定位
遍历所有.o 符号表,发现other.o(U on_set)需要绑定main.o(D on_set)的实际地址;
把 other.s 里on_set(%rip)的占位符号,替换成 main.o 中 on_set 在进程数据段的真实偏移地址;
最终整个程序只有 1 份 on_set 内存,两个文件读写同一块地址。
易错点:
如果两个.c 都写uint8_t on_set=9;,两个.o 都有D on_set,链接器报多重定义 multiple definition。
方式 2:封装 get/set 函数
common.h
#ifndef COMMON_H
#define COMMON_H
#include <stdint.h>
uint8_t get_on_set(void);
void set_on_set(uint8_t val);
#endif
#include "common.h"
static uint8_t on_set = 9; // static限定只本文件可见
uint8_t get_on_set(void)
{
return on_set;
}
void set_on_set(uint8_t val)
{
on_set = val;
}
#include "common.h"
void func(void)
{
uint8_t val = get_on_set();
if(val ==9)
{
//业务
}
}
阶段 2:编译 (-S) → 生成汇编.s
main.c -> main.s
.local on_set # .local:局部符号,不对外导出(static实现原理)
.data
on_set:
.byte 9
.globl get_on_set # 函数加.globl,导出全局符号
get_on_set:
movb on_set(%rip),%al
ret
static对全局变量作用:取消.globl,符号变成本地.local,链接器在别的.o 看不见 on_set。
other.s 关键:
test:
call get_on_set # 调用未解析函数符号get_on_set
cmpb $9,%al
ret
other.s 关键:
test:
call get_on_set # 调用未解析函数符号get_on_set
cmpb $9,%al
ret
other.c 完全不知道on_set存在,代码只依赖get_on_set函数符号。
阶段 3:汇编成.o,nm 查看符号
nm main.o
0000000000000000 d on_set # 小写d:本地私有符号,外部不可见
0000000000000000 T get_on_set # 大写T:全局导出函数
nm other.o
U get_on_set # 未定义函数符号
on_set被 static 隐藏,other.o 从符号表里根本找不到这个变量。
阶段 4:链接
链接器匹配:other.oU get_on_set ↔ main.oT get_on_set,填充函数真实地址;
other 想要读 on_set → 跳转执行 main 里的 get_on_set 函数,由函数内部去访问私有 on_set;
内存:仍然只有 1 份 on_set,但外部无法直接通过变量名篡改,所有读写必须经过接口函数。
补充:
C 规则:全局函数原型,默认隐式 extern,头文件不用手动加 extern。
规则 1:函数分「声明」和「定义」
函数声明(原型):只有uint8_t get_on_set(void); 分号结尾,无 {}
作用:告知编译器函数名字、入参、返回值;
不生成任何汇编代码,不占用程序空间;
头文件专属。
函数定义(实现):uint8_t get_on_set(void){xxx} 带大括号
生成汇编指令、代码;
一个工程只能定义 1 次,放在某个.c;
其余所有文件只用声明。
规则 2:extern 对函数的作用
uint8_t get_on_set(void); ≡ extern uint8_t get_on_set(void);
C 语言全局函数默认 extern 修饰,写不写 extern 完全一样。
变量不一样:变量不加 extern 就是定义,会分配内存。
补充:extern 在汇编里做了啥?
other.c 里extern uint8_t on_set;
编译阶段不产生任何 .data、不定义符号;
汇编里直接使用符号on_set,符号标记 U (Undefined);
链接器去别的.o 找带.globl on_set的符号做地址回填。
浙公网安备 33010602011771号