“undefined reference”是链接阶段错误,需先确认错误发生在链接时而非编译前;通过分步编译(-e→-s→-c→链接)可精准定位:若nm在.o中查不到对应符号定义,则属定义缺失或名称修饰不匹配。

分步编译不是为了炫技,而是为了定位错误阶段、控制中间产物、或适配交叉编译等特殊场景。直接 g++ main.cpp -o app 一步到位没问题,但一旦报错说“undefined reference to `foo`”,你就得知道该去查 .o 还是 .s ——这取决于错误发生在链接前还是链接时。
预处理阶段:用 -E 看宏和头文件到底展开了什么
很多“明明写了函数声明却说未定义”的问题,其实压根没进编译器——因为 #include 路径错了,或者条件编译把整段代码剔除了。用 -E 能看到真实传给编译器的文本:
-
g++ -E main.cpp -o main.i生成纯文本main.i,里面全是展开后的代码,没有注释、没有#include行,只有原始 C++ 语句 - 如果
main.i里根本找不到你引用的类定义或函数声明,说明头文件没被正确包含,检查-I路径或#include拼写 - 宏定义是否如预期替换?直接搜
main.i里的宏名,比在源码里猜靠谱得多
编译阶段:用 -S 检查语法和语义,不生成机器码
这个阶段出错,基本就是代码本身的问题,比如类型不匹配、模板实例化失败、C++17 特性在老编译器上不支持等。生成的 .s 是人类可读的汇编,但你通常不需要读它:
-
g++ -S main.cpp -o main.s或g++ -S main.i -o main.s都可以,前者跳过预处理,后者确保输入干净 - 如果这里报错,错误信息明确指向某行 C++ 代码(比如
error: ‘std::string_view’ is not a member of ‘std’),说明是语言标准或头文件问题,和链接无关 - 注意:
-S不检查函数是否真的有定义,只管当前文件能不能翻译成汇编。所以即使漏了.cpp文件,它也能成功生成.s
汇编阶段:用 -c 生成目标文件,验证符号是否导出
.o 文件是二进制,但带符号表。这是判断“定义是否存在”和“名字修饰是否匹配”的关键节点:
-
g++ -c main.cpp -o main.o生成main.o;若多个源文件,每个都走一遍-c - 用
nm main.o查看导出符号,比如你写了void foo();并实现了,应该能看到类似0000000000000000 T _Z3foov(T表示定义在文本段) - C++ 函数名会被修饰(mangled),如果你在链接时报 “undefined reference to `foo`”,但
nm显示的是_Z3foov,说明调用方可能用了extern "C"或头文件没加inline/static修饰,导致符号不一致 - 注意:
-c不链接,所以就算main.o里调用了未定义的bar(),它也照常生成——这个留到链接阶段才爆
链接阶段:手动调用 g++ 或 ld 合并目标文件
到这里出错,90% 是符号缺失、库路径不对、或 C/C++ 混合时 ABI 不匹配。不要跳过 .o 直接连源码,否则会掩盖问题:
-
g++ main.o utils.o -o app是最常用方式;g++在此阶段自动链接libstdc++和 C 运行时,而gcc不会 - 如果用
ld手动链接(比如嵌入式场景),必须显式指定所有依赖:ld main.o utils.o /usr/lib/x86_64-linux-gnu/crt1.o ... -lstdc++ -lc -o app,缺一不可 - 常见陷阱:
g++ main.o -o app成功,但g++ main.cpp -o app失败——说明main.cpp里某个函数定义被条件编译掉了,而main.o是之前编译的旧版本 - 静态链接加
-static,但要注意libstdc++.a是否存在;动态链接时若提示libxxx.so: cannot open shared object file,不是编译问题,是运行时LD_LIBRARY_PATH或rpath没设对
真正容易被忽略的是:预处理和编译阶段的错误信息看似一样(比如都报“no matching function”),但根源完全不同——前者是宏展开后类型变了,后者是模板推导失败。不拆开看 .i 和 .s,你永远不确定该改头文件还是改调用方式。











