C++类的成员函数对应的链接器符号的解析
在讨论之前,先对一些本文可能涉及到的与链接器符号解析相关的基础知识作简要介绍。请注意,这些论述都是针对 gcc (C语言)而言的。
1. 编译器会导出每个编译单元中的全局符号(即全局链接器符号; 全局变量、全局函数具有全局链接器符号,可被其他编译单元访问,具有外部链接属性),这些符号在链接阶段完成解析。
2. 全局符号有强(strong)弱(weak)之分,全局函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。符号的属性保存在可重定位的目标文件(.o文件)的符号表里,可通过 nm 命令查看。
3. 链接器在解析符号时,会对所有编译单元中出现的全局符号进行决议,将这些符号与唯一的内存地址相关联 (binding),即确定符号的定义。
4. 对多重定义的符号,采用以下规则来处理:(1) 不允许有多个同名强符号,否则链接器会报“符号重定义”错(linking error);(2) 若有一个强符号和多个弱符号同名,则选择强符号;(3) 若有多个弱符号同名,则从这些弱符号中任意选择一个。
想要了解更具体的细节,可以参考《深入理解计算机系统》第7章。
C++在C语言的基础上引入了类的概念,使得符号解析变得较复杂,这里只讨论类的成员函数对应的符号解析。下面是一段示例代码。
1 #include <cstdio> 2 3 class X 4 { 5 public: 6 void foo() { 7 printf("calling foo()\n"); 8 } 9 10 void bar(); 11 void baz(); 12 }; 13 14 15 inline void X::bar() 16 { 17 printf("calling bar()\n"); 18 } 19 20 void X::baz() 21 { 22 printf("calling baz()\n"); 23 }
程序中X::foo() 和 X::bar() 均被指定为内联函数,默认情况下,在C++中,内联函数具有外部链接属性,是弱符号(为了便于理解,沿用上面的称谓)。X::baz() 则对应强符号。但当这段代码所在的源文件(或编译单元)中不包含对 X::foo() 和 X::bar() 的调用时,它们对应的符号是不会被导出的。
若上述代码在一个x.h的头文件中,有y.cpp和z.cpp同时包含(#include)了 x.h,则在链接阶段会报错,因为y.o和z.o中包含同名的强符号(对应于X::baz),符号重定义。我们一般将不需要内联的成员函数的定义编写在.cpp文件中,这样可以避免此类错误。 X::foo() 和 X::bar() 均对应弱符号。以 X::foo() 为例,若 y.o 和 z.o 中均导出了X::foo(), 链接器在解析对应的符号时,将选择两个弱符号中的任意一个(可以这样认为,实际上,在链接时符号会与编译器遇到的第一个定义绑定),最终这两个弱符号对应同一个定义(即同一个函数地址)。
参考:
https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_72/rzarg/inline_linkage.htm