深夜十一点,我就着咖啡看终端里那行 g++ 报错:undefined reference to 'foo(int)'。源码就在眼前,foo(42) 明明白白写在那里,可链接器就是翻脸不认人。这不是库没找到,也不是路径配错,而是 C++ 里的 Name Mangling 在作祟——编译器悄悄把 foo 改成了 _Z3fooi 这串暗号,再拿给链接器去对号入座。
“C++ 面试题”和“C++ 八股文”里常把 Name Mangling 列成考点,但我不太喜欢背概念。真正让我把它吃透的,是那几个半夜排链接错误的经历。这篇文章不打算把 Mangling 规则讲成一部字典,那既没必要也记不住;我想把这几年实打实踩过、解过的符号问题串起来,讲清楚它是什么、怎么来的、GCC/Clang 与 MSVC 差在哪、遇到了该怎么查。
无论你在 Windows 上用 MSVC,还是在 Linux/Android 上用 GCC/Clang,下面这些内容都用得上。读完之后你会多一个习惯:看到 undefined reference、LNK2019、LNK2001 这类报错时,先别急着翻库和依赖,先把符号本身解剖一遍,问题往往就浮出水面了。
1. 从“undefined reference”说起:链接器眼中的C++符号世界
1.1 C语言时代为什么没这个问题:函数名就是符号名
先退回 C 语言。C 没有函数重载,没有命名空间,也没有模板,所以一个函数名在整个程序里基本是全局唯一的。编译 .c 文件生成目标文件时,符号表里 int foo(int x) 对应的符号就是 foo,链接器把引用方和定义方做文本匹配,名字一致就连接成功。
c复制/* foo.c */
int foo(int x) { return x + 1; }
/* main.c */
extern int foo(int);
int main() { return foo(42); }
编译后用 nm 看一眼目标文件,你会看到类似 0000000000000000 T foo 的输出,干净利落。C 编译器偶尔也会做点简单修饰,比如老式 Unix 编译器会在函数名前加下划线变成 _foo,但这本质上还是可读的名字,远没有 C++ 那么夸张。
1.2 C++编译器必须回答的问题:同名函数如何对应不同符号
C++ 一出来,这个简单世界就崩了。函数重载允许 foo(int) 和 foo(double) 同时存在,如果两个函数的符号都叫 foo,链接器根本没法区分;命名空间让 my::foo 和 their::foo 逻辑上完全无关,符号却可能撞名;类成员函数更是重灾区,不同类的 Sample::show 和 Other::show 如果都导出成 show,全局链接必然冲突;模板更不用说了,`std::vector<int
