引子
在现实编码过程当中,曾经遇到过这样的问题“warning:’Base’ has no out-of-line method definition; its vtable will be emitted in every translation unit”。因为对这个warning感兴趣,因而搜集了相关资料来解释这个warning相关的含义。
C++虚表内部架构
Vague Linkage
out-of-line virtual method
C++虚表内部架构
在C++实现机制RTTI中,咱们大概谈到过C++虚表的组织结构。 可是咱们对C++虚表的详细实现细节并无具体谈及,例如在继承体系下虚表的组织以及在多重继承下虚表的组织方式。
(1)没有继承状况下的类虚表结构ios
#include <iostream> using namespace std; class Base { public: virtual void Add() { cout << "Base Virtual Add()!"<< "\n"; } virtual void Sub() { cout << "Base Virtual Sub()!" << "\n"; } virtual void Div() { cout << "Base Virtual Div()!" << "\n"; } }; int main() { Base* b = new Base(); b->Add(); b->Sub(); b->Div(); return 0; }
因为虚函数调用时的行为由指针或者引用所关联的对象所决定,固然咱们已经知道虚表指针存放在对象头4个字节,对象b的值“0x00e8ac38”,调出内存监视器,查看该内存的状况,以下图所示:c++
对象b只存放了虚表的指针“0x00a3cc74”,后面的“0xfdfdfdfd”为Visual Studio在Debug模式下,堆内存上的守护字节。咱们跳转到“0x00a3cc74”查看该内存到底存放了什么,以下图所示:sass
这三个字存放的数据就是Base类三个虚函数所存放的虚函数地址,咱们验证下,我查看调用”b->Add()”时,跳转地址为“0x00a31483”,以下图所示:架构
和虚表所存放的第一个slot的数据进行比对,是相同的。画出虚表示意图以下所示:函数
注意,这里虚表结构和C++实现机制RTTI中中的略有差别,那里type_info信息存放在虚表头,这里存放在虚表尾,因为虚表实现是编译器相关,只要理解用于RTTI的type_info和虚表相关便可。
(2)存在继承时的虚表结构
布局
#include <iostream> using namespace std; class Base { public: virtual void Add() { cout << "Base Virtual Add()!"<< "\n"; } virtual void Sub() { cout << "Base Virtual Sub()!" << "\n"; } virtual void Div() { cout << "Base Virtual Div()!" << "\n"; } }; class Derive : public Base { public: // 定义Drived类的Sub函数,与父类Base的Sub不一样 virtual void Sub() { cout << "Derive Virtual Sub()!" << "\n"; } }; int main() { Base* b = new Base(); b->Add(); Base* d = new Derive(); d->Add(); return 0; }
Base虚表信息以下图所示:ui
Derived虚表信息以下图所示:this
从这两幅图中能够看到,两张图中虚表惟一的不一样是,虚表第二项不同。Derive自定义了Sub()函数,因此理应相应的虚表应该指向Derive新定义的函数位置,而Derive继承了(没有覆盖Add()和Div()函数)Add()和Div()函数,因此第一项和第三项和Base类虚表的第一项和第三项相同。编码
也就是说子类重写了相应的虚函数,那么虚表中相应位置的地址会指向新的函数,没有重写那么相应位置的地址和父类相同。
(3)在多重继承下
代码以下,Derive继承自Base1和Base2,也就是说Derive从两个父类继承了两个虚表,如今的问题是,两个虚表会不会合并到一块儿呢?以下图所示:spa
这种形式的话,只有从第一个父类继承的虚表的下标和父类虚表的下标是相同的,后面的虚表都要移动必定的偏移量,这样作显然不太漂亮。因此如今Visual Studio不是经过这样的方式,而是将从父类继承的多个虚表分开,以每一个父类为一个单位,以下图所示。
若是子类覆盖了相应父类的虚函数,则会在相应父类的内存区域头部虚表指针所对应的虚表上覆盖掉对应的虚函数地址。
#include <iostream> using namespace std; class Base1 { public: int m_base1; Base1(int para):m_base1(para){} virtual void Add() { cout << "Base1 Virtual Add()!"<< "\n"; } virtual void Sub() { cout << "Base1 Virtual Sub()!" << "\n"; } virtual void Div() { cout << "Base1 Virtual Div()!" << "\n"; } }; class Base2 { public: int m_base2; Base2(int para) : m_base2(para){} virtual void Mul() { cout << "Base2 Virtual Mul()!" << "\n"; } virtual void INC() { cout << "Base2 Virtual INC()!" << "\n"; } virtual void DEC() { cout << "Base2 Virtual DEC()!" << "\n"; } }; class Derive : public Base1, public Base2 { public: int m_derive; Derive(int b1, int b2, int d) : Base1(b1), Base2(b2), m_derive(d){} virtual void Sub() { cout << "Derive Virtual Sub()!" << "\n"; } virtual void INC() { cout << "Derive Virtual INC()!" << "\n"; } }; int main() { Derive* d = new Derive(1, 11, 22); // 此时指针指向的位置不是Derive的开头位置,而是Derive对象中子区域Base2的头部 Base2* b2 = d; // 此时b2只能调用Base2的虚函数 b2->INC(); Base1* b1 = d; return 0; }
以下图所示:
类继承时的内存区域布局时很是重要的,特别是在多继承时很重要的,虚析构函数在虚表中的存放还不是很明确。后面会继续分析。
Vague Linkage
在C++中,有些建立过程须要占用.o文件的空间,例如函数的定义须要占用.o文件的空间。可是函数可以比较明确地建立到指定的.o文件中,有些建立过程却并无明确的指定建立到那个编译单元中。咱们称这些建立过程须要”Vague Linkage”,及模糊连接。一般它们会在任何须要的地方建立,因此这样建立的信息有可能会有冗余。
inline函数(Inline Functions)
虚表(VTables)
类型信息(type_info objects)
模板实例化(Template Instantiations)
(1)inline函数
inline函数一般会定义在头文件中,以便可以被不一样的编译单元包含进来。可是inline只是一个建议,编译器不必定会真的执行inline操做,而且有时候真的会须要一份inline函数的拷贝,好比说获取inline函数的地址或者inline操做失败。在这种状况下,一般咱们会将inline函数的定义散播到全部须要用到该函数的编译单元中。
另外,咱们一般会将附带虚表的inline虚函数(虚函数大部分状况下不会为inline函数)散播到目标文件中,由于虚函数一般须要真正地定义出来。
(2)虚表
对于C++虚函数机制,大部分编译器都是使用查找表(lookup table)实现的,也就是虚表。虚表保存着指向虚函数的指针,另外每一个含有虚函数的类对象都有一个指向虚表的指针(虚表在多重继承下,有可能有多个)。若是class声明了一个非inline,非纯虚的虚函数,那么这些虚函数中的第一个out-of-line方法就被选为关键方法(key method),那么虚表只会散播到(即定义到)这个关键方法所定义的编译单元中。
其实关于关键方法,还有一个有趣的例子,有时候你们会遇到“未定义的外部符号”这样的连接错误,这样的错误是因为你使用了声明可是没有定义的外部符号致使的。虚表其实在必定程度上也能够称为全局变量,只是这个全局变量是隐式地被C++语言机制实现的。虚表只会生成在第一个out-of-line虚函数所在编译单元中,若是没有定义out-of-line虚函数,那么全部include该头文件的编译单元中生成虚表。
// Base.h class Base{ public: // 第一函数print为关键方法,虚表只会散播到(定义在)print所定义在的编译单元中 // 若是print也定义在Base.h,那么全部包含Base.h的全部.cpp都会有一份vtable的拷贝 // 经过连接器来消除冗余数据 virtual int print(); virtual int add(int lhs, int rhs) { return lhs + rhs; } }; // A.cpp #include "Base.h" // vtable会定义在A.cpp编译单元中 int Base::print() { cout << "print" << endl;} // main.cpp #include "Base.h" int main() {return 0;}
(3)type_info对象
为了实现”dynamic_cast”,”type_id”, 异常处理,C++要求类型信息可以完整地写出来(即存储,以便运行时可以获取)。对于多态类(含有虚函数)来讲,”type_info”结构体随着虚表一块儿出现,虚表中会有一个slot来存放type_info结构体的指针,这样才能在运行时,在执行dynamic_cast<>的时候得到对象具体的类型信息。
对于其余类型,咱们只会在须要的时候实现其type_info结构体。好比,当你使用”typeid”来获取表达式的类型信息时,或者抛出对象时和捕获对象信息时。
(4)模板实例化
最多见的就是咱们又可能在多个编译单元中,同时实例化同一个类型的模板。固然连连接器会作冗余处理,或者使用C++11的外部模板。
ouf-of-line virtual method
前面咱们已经知道虚函数知足vague linkage的条件,有可能须要连接器去消除冗余。
若是一个类中全部的虚函数都是inline的,那么编译器就没法知道该挑选哪个编译单元来存放虚表的惟一的一份拷贝,相对应地,每个须要虚表(例如调用虚函数)的目标文件中都会有一份虚表拷贝。在不少平台上,连接器可以统一这些重复的拷贝,要么丢弃重复的定义或者将全部虚表引用指向同一份拷贝,因此只会产生一个warning。
相对应的std::type_info也会使用这种形式,即vague linkage,从字面意思上看就是说type_info并非牢牢地绑定在每一个编译单元中,而是以一个弱连接的形式出现。因此接下来的任务就交给连接器了,确保在最后的可执行文件中只有一份type_info的结构体对象。
these std::type_info objects have what is called vague linkage because they are not tightly bound to any one particular translation unit (object file).
The compiler has to emit them in any translation unit that requires their presence, and then rely on the linking and loading process to make sure that only one of them is active in the final executable.
With static linking all of these symbols are resolved at link time, but with dynamic linking, further resolution occurs at load time. – [GCC Frequently Asked Questions]
上面的连接是GCC关于这方面信息的解释,下面是LLVM在其编码规范中给出的关于Out-of-line虚函数的解释。
If a class is defined in a header file and has a vtable (either it has virtual methods or it derives from classes with virtual methods), it must always have at least one out-of-line virtual method in the class. Without this, the compiler will copy the vtable and RTTI into every .o file that #includes the header, bloating .o file sizes and increasing link times. – [LLVM Coding Standards]
“out-of-line”虚函数是指类中第一个虚函数的实现的可以让编译器选择一个特定的编译单元,来实现这些虚函数或者实现类的具体细节(例如类型信息),并在这个编译单元中存放一份共享的虚表。可是若是有多个”out-of-line”虚函数分别定义在不一样的.cpp文件中,那么编译器就会将虚表以及类型信息,生成在类中声明最靠前的”out-of-line”虚函数所在的TranslationUnit中。
我之前看LLVM源码的时候,看到过一条有趣的注释信息:
以下代码所示:
//===--------------------test.h---------------------===// class Base { public: // virtual函数所有是默认inline virtual int print() { return 0;} virtual int Add() { return 1;} }; //===-----------------------------------------------===// //===-------------------test.cpp--------------------===// #include "test.h" // test.cpp须要用到虚表,因此虚表应该在test.cpp中生成一份儿 int main() { Base* b = new Base(); b->Add(); delete b; return 0; } //===-----------------------------------------------===// //===--------------------foo.cpp--------------------===// #include "test.h" // foo.cpp 也用到了虚表因此在编译的时候,在foo.cpp中也应该产生一份儿 void func() { Base* b = new Base(); b->print(); delete b; } //===-----------------------------------------------===//
咱们编译一下,看一下编译结果是否如此:
$g++ -c test.cpp foo.cpp $objdump -d foo.o // 获得下面结果,说明在foo.o中生成了虚函数定义 Disassembly of section .text$_ZN4Base5printEv: 00000000 <__ZN4Base5printEv>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 04 sub $0x4,%esp 6: 89 4d fc mov %ecx,-0x4(%ebp) 9: b8 00 00 00 00 mov $0x0,%eax e: c9 leave f: c3 ret Disassembly of section .text$_ZN4Base3AddEv: 00000000 <__ZN4Base3AddEv>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 04 sub $0x4,%esp 6: 89 4d fc mov %ecx,-0x4(%ebp) 9: b8 01 00 00 00 mov $0x1,%eax e: c9 leave f: c3 ret $objdump -d test.o // 获得下面的结果 Disassembly of section .text$_ZN4Base5printEv: 00000000 <__ZN4Base5printEv>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 04 sub $0x4,%esp 6: 89 4d fc mov %ecx,-0x4(%ebp) 9: b8 00 00 00 00 mov $0x0,%eax e: c9 leave f: c3 ret Disassembly of section .text$_ZN4Base3AddEv: 00000000 <__ZN4Base3AddEv>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 04 sub $0x4,%esp 6: 89 4d fc mov %ecx,-0x4(%ebp) 9: b8 01 00 00 00 mov $0x1,%eax e: c9 leave f: c3 ret
咱们从上面的结果中看到,确实在test.o和foo.o中都产生了虚函数print()和add()的定义,若是咱们使用”readelf -s test.o”查看更详细的信息的话,会发现虚表和type_info在test.o和foo.o也都存在一份拷贝。
Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS test.cpp 2: 00000000 0 SECTION LOCAL DEFAULT 7 3: 00000000 0 SECTION LOCAL DEFAULT 9 4: 00000000 0 SECTION LOCAL DEFAULT 10 5: 00000000 0 SECTION LOCAL DEFAULT 11 6: 00000000 0 SECTION LOCAL DEFAULT 12 7: 00000000 0 SECTION LOCAL DEFAULT 13 8: 00000000 0 SECTION LOCAL DEFAULT 15 9: 00000000 0 SECTION LOCAL DEFAULT 17 10: 00000000 0 SECTION LOCAL DEFAULT 18 11: 00000000 0 SECTION LOCAL DEFAULT 21 12: 00000000 0 SECTION LOCAL DEFAULT 22 13: 00000000 0 NOTYPE LOCAL DEFAULT 3 _ZN4BaseC5Ev 14: 00000000 0 SECTION LOCAL DEFAULT 20 15: 00000000 0 SECTION LOCAL DEFAULT 1 16: 00000000 0 SECTION LOCAL DEFAULT 2 17: 00000000 0 SECTION LOCAL DEFAULT 3 18: 00000000 0 SECTION LOCAL DEFAULT 4 19: 00000000 0 SECTION LOCAL DEFAULT 5 20: 00000000 0 SECTION LOCAL DEFAULT 6 21: 00000000 10 FUNC WEAK DEFAULT 11 _ZN4Base5printEv 22: 00000000 10 FUNC WEAK DEFAULT 12 _ZN4Base3AddEv 23: 00000000 14 FUNC WEAK DEFAULT 13 _ZN4BaseC2Ev 24: 00000000 16 OBJECT WEAK DEFAULT 15 _ZTV4Base 25: 00000000 14 FUNC WEAK DEFAULT 13 _ZN4BaseC1Ev 26: 00000000 84 FUNC GLOBAL DEFAULT 7 main 27: 00000000 0 NOTYPE GLOBAL DEFAULT UND _Znwj 28: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZdlPv 29: 00000000 8 OBJECT WEAK DEFAULT 18 _ZTI4Base 30: 00000000 6 OBJECT WEAK DEFAULT 17 _ZTS4Base 31: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZTVN10__cxxabiv117__clas
咱们能够看到在test.o中生成了类Base的虚表和type_info结构体,_ZTV表示虚表,_ZTI表示type_info结构, _ZTS表示type name,注意在gcc的设计中,type_info存放在虚表的第一个slot(Visual Studio是存放在虚表的最后一个slot中)。咱们看一下foo.o的相关信息,以下:
Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS foo.cpp 2: 00000000 0 SECTION LOCAL DEFAULT 7 3: 00000000 0 SECTION LOCAL DEFAULT 9 4: 00000000 0 SECTION LOCAL DEFAULT 10 5: 00000000 0 SECTION LOCAL DEFAULT 11 6: 00000000 0 SECTION LOCAL DEFAULT 12 7: 00000000 0 SECTION LOCAL DEFAULT 13 8: 00000000 0 SECTION LOCAL DEFAULT 15 9: 00000000 0 SECTION LOCAL DEFAULT 17 10: 00000000 0 SECTION LOCAL DEFAULT 18 11: 00000000 0 SECTION LOCAL DEFAULT 21 12: 00000000 0 SECTION LOCAL DEFAULT 22 13: 00000000 0 NOTYPE LOCAL DEFAULT 3 _ZN4BaseC5Ev 14: 00000000 0 SECTION LOCAL DEFAULT 20 15: 00000000 0 SECTION LOCAL DEFAULT 1 16: 00000000 0 SECTION LOCAL DEFAULT 2 17: 00000000 0 SECTION LOCAL DEFAULT 3 18: 00000000 0 SECTION LOCAL DEFAULT 4 19: 00000000 0 SECTION LOCAL DEFAULT 5 20: 00000000 0 SECTION LOCAL DEFAULT 6 21: 00000000 10 FUNC WEAK DEFAULT 11 _ZN4Base5printEv 22: 00000000 10 FUNC WEAK DEFAULT 12 _ZN4Base3AddEv 23: 00000000 14 FUNC WEAK DEFAULT 13 _ZN4BaseC2Ev 24: 00000000 16 OBJECT WEAK DEFAULT 15 _ZTV4Base 25: 00000000 14 FUNC WEAK DEFAULT 13 _ZN4BaseC1Ev 26: 00000000 70 FUNC GLOBAL DEFAULT 7 _Z4funcv 27: 00000000 0 NOTYPE GLOBAL DEFAULT UND _Znwj 28: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZdlPv 29: 00000000 8 OBJECT WEAK DEFAULT 18 _ZTI4Base 30: 00000000 6 OBJECT WEAK DEFAULT 17 _ZTS4Base 31: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZTVN10__cxxabiv117__clas
能够发如今foo.o中也生成了虚表和type_info信息,也就是说若是inline虚函数都没有设置成out-of-line的话,那么编译器会向每一个须要用到虚表结构的目标文件中散播虚表,虚函数和type_info定义。直到连接的时候,连接器进行冗余消除操做。因为连接器须要消除冗余的type_info和vtable,因此就要求虚表和type_info的符号必须是弱符号(weak symbols),GCC好像永远会将RTTI信息设置为弱符号,即便虚函数中有关键方法(key method)。 对于目标文件中的符号名,可使用c++filt命令来获得符号名所表示的真正的name,例如: $ c++filt ZNK3MapI10StringName3RefI8GDScriptE10ComparatorIS0_E16DefaultAllocatorE3hasERKS0可是若是派生类没有覆盖掉任何父类的虚函数的话,彻底能够完成虚函数调用时的静态决议,则不须要对象的头4个字节的虚表指针,其实也就不须要虚表了。相关信息请见:LLVM:multiple typeinfo nameGCC Frequently Asked QuestionsLLVM:CodingStandards 原文连接:https://blog.csdn.net/dashuniuniu/article/details/50162903