make 链接 multiple definition 重复定义速查
更新时间:2026-10-24。FAQ 速查页,面向用 GNU make 构建 C/C++ 工程的同学。multiple definition 重复定义指链接器(
ld)在合并各.o时发现同一个符号有两份实体定义,打出multiple definition of 'xxx'并拒绝产出可执行文件。它与 undefined reference 链接错误速查 是一对镜像病:一个是「符号没人定义」,一个是「符号定义了太多次」。最常见的病根是在头文件里写了非 inline 的变量/函数定义,被多个.cppinclude 后每份 TU 各生成一份实体;其次是 Makefile 把同一个.o或同一份代码(源码 + 静态库)链接了两遍。本篇以multiple definition of原文为锚逐条定位。
本文要回答的问题
multiple definition of 'g_config'与first defined here两行怎么读?- 头文件里为什么不能直接定义变量和函数?
inline、static、extern各管什么? - Makefile 里同一个
.o被链接两次是怎么发生的? warning: "XXX" redefined宏重复定义和链接重复定义是一回事吗?- 老的
-fcommon行为为什么升级 gcc 后突然报错?
一、报错原文与典型现象
典型现场(工程路径以 /proj/demo 占位),链接阶段翻车:
$ make
g++ -O2 -c src/main.cpp -o build/main.o
g++ -O2 -c src/config.cpp -o build/config.o
g++ build/main.o build/config.o -o bin/app
/usr/bin/ld: build/config.o:(.bss+0x0): multiple definition of 'g_config'
/usr/bin/ld: build/main.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
make: *** [Makefile:20: bin/app] Error 1宏重复定义则是编译期的另一种长相(只是警告但同样要治):
src/util.h:7: warning: "MAX_CONN" redefined
7 | #define MAX_CONN 1024
|
In file included from src/main.cpp:3:
src/config.h:5: note: this is the location of the previous definition
5 | #define MAX_CONN 256读懂现场抓住两件东西:符号名(multiple definition of 'g_config' 直接点明是谁重了)与两个出处(first defined here 指向第一份定义所在的 .o,上一行指向第二份)。拿着符号名 grep -rn "g_config" src/,凡是头文件里出现不带 extern 不带 inline 的定义,基本就是病灶。链接器报的是 .o 级出处,而病根往往在它们共同 include 的那个头文件里。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| 头文件里的全局变量被多个 .cpp include 后报重复 | 头文件直接定义非 inline 变量 | nm build/*.o | grep " B g_config" 多处出现 | 头文件改 extern 声明,定义挪进唯一一个 .cpp;C++17 起可用 inline 变量 |
| 头文件里写了函数实现(非模板非 inline) | 每个 include 它的 TU 各生成一份函数实体 | nm build/*.o | grep " T func" 多处出现 | 加 inline,或实现挪进 .cpp、头文件只留声明 |
Makefile 的链接行里同一 .o 出现两次 | 通配符重复展开或手工列表重复 | make -n 看真实链接命令是否含重复 .o | 用 sort 去重:$(sort $(OBJS));检查 OBJS 拼接 |
| 源码 .cpp 与其编成的 .a 同时进链接行 | 同一份代码以两种形态进链 | make -n 输出里既有 util.o 又有 libutil.a | 二选一:要么链库要么链源码 |
| 多个静态库各自内嵌同一第三方代码 | 两份库都编进了同一份实现 | nm liba.a | grep 符号、nm libb.a 都有定义 | 抽取公共依赖为独立库;或用 --allow-multiple-definition 临时绕过(不推荐) |
| 升级 gcc 10+ 后老工程突然报重复 | 老代码依赖 -fcommon 合并 tentative 定义,新版默认 -fno-common | 报错符号是头文件里无 extern 的全局变量 | 正解是加 extern;过渡可 CFLAGS += -fcommon |
编译期 warning: "X" redefined | 两个头文件定义同名宏 | 警告里 note 行指出前一个定义位置 | 统一宏出处;或 #undef 后重定义;用 #pragma once/include guard |
| C++ 模板显式实例化在多文件重复 | template class Foo<int>; 写在多个 .cpp | 报错符号是模板实例化名 | 显式实例化只放一处,其余文件用 extern template 声明 |
三、排查决策图
下面这张图按「符号被谁定义了两份」分岔:红是报错入口与命中的根因,蓝是排查动作,绿是验证通过,橙是修复动作,紫是防复发沉淀。
读这张图抓一条主线:先用 nm 看清两份定义各在哪个 .o 里,再判断是「头文件被多份包含」还是「链接行本身重复」。前者改头文件(extern/inline/挪实现),后者改 Makefile(去重、源码与库二选一)。修复后务必 make clean 全量重链——残留的旧 .o 会让「修好了」成为假象;最终用 nm bin/app | grep 符号 确认只剩一份定义才算闭环。
四、逐条排查展开
头文件定义变量是第一大病根。 头文件被 N 个 .cpp include 就生成 N 份实体。正解是「声明与定义分离」:
// config.h:只放声明
extern int g_config;
// config.cpp:唯一一份定义
int g_config = 0;C++17 起可以给头文件里的变量加 inline(inline int g_config = 0;),链接器会把多份合并为一份;C 语言没有这个待遇,老老实实用 extern 方案。函数同理:头文件里的函数实现加 inline,或把实现挪进 .cpp。
gcc 10 的 -fcommon 变更是老工程集中爆雷点。 C 语言允许「tentative definition」——多个文件各写 int g_x;(无 extern 无初始化)老标准下由链接器合并成一份,gcc 9 及之前默认 -fcommon 容忍,gcc 10 起默认 -fno-common 直接报 multiple definition。升级工具链后老工程成片报错基本就是它。正解是把其中一份以外的都加 extern;过渡方案 CFLAGS += -fcommon 只能当创可贴。
Makefile 链接行重复的三种来源。 ① OBJS = $(wildcard src/*.o) util.o 这种通配符与手工列表叠加;② 链接行写成 $(OBJS) libutil.a,而 libutil.a 本来就是 $(OBJS) 里那批源码编的;③ 子目录递归 make 各自产出同名符号的 .o。验证统一用 make -n 看真实命令,去重统一用 $(sort ...)(make 的 sort 函数顺带去重):
OBJS := $(sort $(OBJS))库与库撞车。 两个静态库各自 vendored 同一份第三方代码(如都内嵌了某 JSON 库),链接时报第三方符号重复。nm -A liba.a libb.a | grep " T 符号名" 能定位到两个库都有定义。正解是把公共依赖抽成独立库、两边改为链接它;临时绕过是 -Wl,--allow-multiple-definition,但合并的是哪一份、行为是否符合预期全凭运气,只用于救火。
宏重复定义不是链接病但常被混进来。 #define MAX_CONN 1024 在两个头文件里各写一份,编译期报 warning: "MAX_CONN" redefined。它只是预处理器警告,不阻断构建,但两个值不一致时程序行为取决于 include 顺序,是隐患。处置:宏统一收口到公共头文件;必须覆盖时先 #undef 再 #define,并在注释里写明覆盖意图。所有头文件补 #pragma once 或 include guard,顺带把「同一头文件被重复包含」这类低频 variant 也堵死。
C++ 模板显式实例化重复。 template class Foo<int>; 写在多个 .cpp 里,每处都生成一份实例化实体。规则:显式实例化定义只放一个 TU,其它需要它的文件写 extern template class Foo<int>; 声明。
五、规范做法与预防
- 头文件纪律进评审:头文件只放声明、inline/模板实现与常量;任何非 inline 变量/函数定义出现在头文件一律打回。
- CI 用
-fno-common与-Wl,--no-undefined双闸:前者防 tentative 定义蒙混,后者防漏定义,方向正好把两类符号病都锁在构建期。 - 链接行在 Makefile 里去重:
$(sort $(OBJS))成为肌肉记忆;库依赖画图理清楚,杜绝源码与库同进。 - 公共第三方依赖只 vendored 一次:monorepo 里设统一的 third_party 目录,各库链接它而不是各自内嵌。
nm排查写进团队手册:nm的符号类型字母(T 已定义函数、B/D 已定义变量、U 未定义)是每个 C/C++ 工程师的排错基本功。
这一篇在知识体系里的位置
本篇是 Makefile 排错速查簇的链接重复符号篇,与镜像病「符号缺失」篇配对:符号没人定义的看 undefined reference 链接错误速查;编译器被系统杀掉的看 make 编译内存超限速查;编译器自身崩溃的看 make 编译器内部崩溃速查;CMake 依赖查找失败的看 CMake 找不到依赖包速查;类型接口报错的看 error: cannot convert 类型不匹配速查;语法、路径、权限、规则、卡死、乱码、版本与编译器探测等其它岔路见 Makefile missing separator 速查、fatal error 头文件找不到速查、构建 Permission denied 速查、make: No rule to make target 速查、make 构建卡死无响应速查、make 构建输出乱码速查、CMake 版本不兼容速查、CMake 找不到编译器速查、CMake FetchContent 拉取失败速查。「nm 看符号、声明与定义分离」这对基本功对一切链接类报错通用。
一句话总结
multiple definition 重复定义先拿 nm build/*.o | grep 符号 定位两份定义出处:来自同一头文件就是「头文件里写了非 inline 定义」,改成 extern 声明加唯一 .cpp 定义(C++17 变量可用 inline);来自链接行重复就 make -n 查真实命令,$(sort $(OBJS)) 去重、源码与库二选一;老工程升级 gcc 10+ 成片报错是 -fno-common 新默认,正解加 extern、-fcommon 只当创可贴;修完 make clean 全量重链并 nm 确认符号只剩一份才算闭环。