GCC编译:多源文件包含的同一头文件内容是否会在可执行文件重复
GCC编译流程下头文件内容的存储与引用逻辑
多源文件包含同一头文件时,内容是否会在可执行文件重复存储
不会无脑重复存储,也不是全部走符号引用,会按内容类型走不同处理逻辑:
- 普通非
static、非inline的函数定义、非static全局变量定义:这类内容属于全局强符号,如果直接写在头文件被多个源文件包含,每个源文件编译生成的目标文件都会携带同名强符号,链接阶段会直接抛出multiple definition重复定义错误,根本无法生成可执行文件,不存在重复存储的可能。你当前单编译test.c能通过,只是因为项目里只有这一个源文件引入了该定义。 - 宏定义、
typedef、结构体/枚举声明这类语法层面的内容:预处理阶段结束后就会被编译器解析消化,不会生成实际的机器指令或数据,完全不会进入最终可执行文件。 static修饰的函数、全局变量:这类符号是编译单元(单个源文件预处理后形成的独立代码块)私有,若被多个源文件包含,每个目标文件都会生成一份独立副本,最终可执行文件中会存在多份实体,调用时直接绑定本单元内的私有地址,不走全局符号引用。inline函数、纯声明(函数原型、extern变量声明):inline函数由编译器按需选择内联展开或生成单份全局共享代码,正常不会重复存储;纯声明仅用于告知编译器符号存在规则,不会生成实际代码/数据,最终仅保留符号引用,不占用实体存储。
预处理器将#include替换为完整代码内容的原因
C语言从设计之初就采用独立编译单元模型,编译器核心cc1本身完全不感知头文件、#include指令这类概念,它只能接收连续、无预处理指令的完整C代码文本作为输入。
预处理器cpp的核心定位就是做文本层面的预处理:递归展开#include的文件内容、替换宏、处理条件编译指令,最终把分散在多个文件里的代码拼接成一个完整的、符合编译器输入要求的纯C代码流,再传给cc1。这种设计把文件查找、文本替换这类逻辑和编译器核心的语法解析、优化、代码生成逻辑完全解耦,大幅简化了编译器核心的实现复杂度,是C编译模型沿袭几十年的标准设计。
示例中kids函数的完整处理逻辑
你测试用例里的kids函数定义直接写在头文件中,单文件编译时的完整流程如下:
- 预处理阶段:
cpp检测到#include指令,直接把头文件里的kids函数完整定义插入到指令对应位置,和test.c里的main函数等内容拼接成无预处理指令的完整C代码。 - 编译阶段:
cc1接收到完整代码后,识别到kids是全局函数,直接生成对应的汇编实现(就是你贴出的test.s里从kids:标签开始的整段函数指令);解析到main函数里的call kids调用时,因为kids和main在同一个编译单元内,编译器已知该符号的存在,默认未开启优化时不会做内联展开,直接生成普通的函数调用指令。 - 汇编、链接阶段:汇编器把汇编代码转成目标文件,若项目只有
test.c一个源文件,链接器会把kids函数代码、main函数代码、依赖的C标准库函数代码整合到可执行文件中,同时把call kids指令里的地址填充为kids函数在可执行文件中的实际加载地址,运行时直接跳转到对应地址执行,也就是你所说的“快捷方式”引用。
你贴出的
test.s汇编里已经包含了kids的完整实现,这是因为函数定义和main在同一个编译单元。如果遵循规范把kids的定义单独放到kids.c源文件,头文件里只保留int kids();的函数声明,你再生成test.s时就看不到kids的完整实现了,只会保留一个未解析的call kids符号引用,等链接阶段再绑定到kids.o里唯一的kids函数实体地址,完全不会出现重复存储。
内容的提问来源于stack exchange,提问作者hasanghaforian
相关产品推荐
相关产品推荐

