C ++ vtables。 第2部分(虚拟继承+编译器生成的代码)

本文的翻译是专门为“ C ++开发人员”课程的学生准备的。 朝这个方向发展是否有趣? 观看Google测试框架练习班的录音!



第3部分-虚拟继承


在本文的第一部分和第二部分中 ,我们讨论了vtables如何在最简单的情况下工作,然后在多重继承中工作。 虚拟继承使情况更加复杂。


您可能还记得,虚拟继承意味着在特定类中,只有一个基类实例。 例如:


class ios ... class istream : virtual public ios ... class ostream : virtual public ios ... class iostream : public istream, public ostream 

如果不是上面的virtual关键字, iostream实际上iostream有两个ios实例,它们可能在同步期间引起头痛,并且根本无效。


为了理解虚拟继承,我们将考虑以下代码片段:


 #include <iostream> using namespace std; class Grandparent { public: virtual void grandparent_foo() {} int grandparent_data; }; class Parent1 : virtual public Grandparent { public: virtual void parent1_foo() {} int parent1_data; }; class Parent2 : virtual public Grandparent { public: virtual void parent2_foo() {} int parent2_data; }; class Child : public Parent1, public Parent2 { public: virtual void child_foo() {} int child_data; }; int main() { Child child; } 

让我们来探索child 。 首先,像在前面的部分中一样,在vtable Child确切位置处转储大量内存,然后分析结果。 我建议快速浏览一下这里的结果,并在我透露以下详细信息时再返回。


 (gdb) p child $1 = {<Parent1> = {<Grandparent> = {_vptr$Grandparent = 0x400998 <vtable for Child+96>, grandparent_data = 0}, _vptr$Parent1 = 0x400950 <vtable for Child+24>, parent1_data = 0}, <Parent2> = {_vptr$Parent2 = 0x400978 <vtable for Child+64>, parent2_data = 4195888}, child_data = 0} (gdb) x/600xb 0x400938 0x400938 <vtable for Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400940 <vtable for Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400948 <vtable for Child+16>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400950 <vtable for Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400958 <vtable for Child+32>: 0xa0 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400960 <vtable for Child+40>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400968 <vtable for Child+48>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400970 <vtable for Child+56>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400978 <vtable for Child+64>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400980 <vtable for Child+72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400988 <vtable for Child+80>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400990 <vtable for Child+88>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400998 <vtable for Child+96>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a0 <VTT for Child>: 0x50 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a8 <VTT for Child+8>: 0xf8 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b0 <VTT for Child+16>: 0x18 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b8 <VTT for Child+24>: 0x98 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c0 <VTT for Child+32>: 0xb8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c8 <VTT for Child+40>: 0x98 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d0 <VTT for Child+48>: 0x78 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e0 <construction vtable for Parent1-in-Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e8 <construction vtable for Parent1-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009f0 <construction vtable for Parent1-in-Child+16>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009f8 <construction vtable for Parent1-in-Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a00 <construction vtable for Parent1-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a08 <construction vtable for Parent1-in-Child+40>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400a10 <construction vtable for Parent1-in-Child+48>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a18 <construction vtable for Parent1-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a20 <typeinfo name for Parent1>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x31 0x400a28 <typeinfo name for Parent1+8>: 0x00 0x31 0x31 0x47 0x72 0x61 0x6e 0x64 0x400a30 <typeinfo name for Grandparent+7>: 0x70 0x61 0x72 0x65 0x6e 0x74 0x00 0x00 0x400a38 <typeinfo for Grandparent>: 0x50 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a40 <typeinfo for Grandparent+8>: 0x29 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a50 <typeinfo for Parent1>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a58 <typeinfo for Parent1+8>: 0x20 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a60 <typeinfo for Parent1+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400a68 <typeinfo for Parent1+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a70 <typeinfo for Parent1+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400a78: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a80 <construction vtable for Parent2-in-Child>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a88 <construction vtable for Parent2-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a90 <construction vtable for Parent2-in-Child+16>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a98 <construction vtable for Parent2-in-Child+24>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400aa0 <construction vtable for Parent2-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400aa8 <construction vtable for Parent2-in-Child+40>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400ab0 <construction vtable for Parent2-in-Child+48>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ab8 <construction vtable for Parent2-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400ac0 <typeinfo name for Parent2>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x32 0x400ac8 <typeinfo name for Parent2+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400ad0 <typeinfo for Parent2>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400ad8 <typeinfo for Parent2+8>: 0xc0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ae0 <typeinfo for Parent2+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400ae8 <typeinfo for Parent2+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400af0 <typeinfo for Parent2+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400af8 <typeinfo name for Child>: 0x35 0x43 0x68 0x69 0x6c 0x64 0x00 0x00 0x400b00 <typeinfo for Child>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400b08 <typeinfo for Child+8>: 0xf8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b10 <typeinfo for Child+16>: 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x400b18 <typeinfo for Child+24>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b20 <typeinfo for Child+32>: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b28 <typeinfo for Child+40>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b30 <typeinfo for Child+48>: 0x02 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x400b38 <vtable for Grandparent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b40 <vtable for Grandparent+8>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Grandparent+16>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 

哇,有很多信息。 马上弹出两个新问题:什么是VTT ?什么是vtable for X-in-Child构造vtable for X-in-Child ? 我们将尽快答复他们。
让我们从子内存结构开始:


尺码价值
8字节_vptr $ Parent1
4字节parent1_data(+ 4个填充字节)
8字节_vptr $ Parent2
4字节parent2_data
4字节child_data
8字节_vptr $祖父母
4字节grandparent_data(+ 4个填充字节)

实际上, Child只有1个祖父母或外祖父母。 不平凡的是,他是记忆中的最后一位,尽管他是层次结构中最高的。
这是vtable结构:


地址价值目录内容
0x4009380x20(32)虚拟基准偏移量(我们将在稍后讨论)
0x4009400top_offset
0x4009480x400b00儿童的typeinfo
0x4009500x400870Parent1 :: parent1_foo()。 vtable指针Parent1指向此处。
0x4009580x4008a0子:: child_foo()
0x4009600x10(16)虚拟基准偏移
0x400968-16top_offset
0x40090x400b儿童的typeinfo
7000
0x4009780x400890Parent2 :: parent2_foo()。 vtable指针Parent2指向此处。
0x4009800虚拟基准偏移
0x400988-32top_offset
0x4009900x400b00儿童的typeinfo
0x4009980x400880祖父母:: grandparent_foo()。 vtable指针Grandparent指向此处。

上面有一个新概念- virtual-base offset 。 很快,我们将了解他在那做什么。
接下来,让我们探索这些看起来很奇怪的construction vtables 。 这是vtable for Parent1-in-Child的构造vtable for Parent1-in-Child


价值目录内容
0x20(32)虚拟基准偏移
0最高偏移
0x400a50Parent1的typeinfo
0x400870Parent1 :: parent1_foo()
0虚拟基准偏移
-32最高偏移
0x400a50Parent1的typeinfo
0x400880祖父母:: grandparent_foo()

目前,我认为描述该过程比在您身上堆放更多带有随机数的表更容易理解。 因此:


想象你是一个Child 。 要求您在新的内存中构建自己。 由于您直接继承Grandparent (这是虚拟继承的意思),因此您将直接直接调用其构造函数(如果不是虚拟继承,则将调用构造函数Parent1 ,而后者又将调用Grandparent构造函数)。 设置this += 32字节,因为这是Grandparent数据所在的位置,然后调用构造函数。 很简单


然后是时候构建Parent1Parent1可以放心地假设,到他构造自己的时候,已经创建了Grandparent ,因此,例如,他可以访问Grandparent数据和方法。 但是,等等,他怎么知道在哪里可以找到这些数据? 它们与变量Parent1不在同一个位置!


construction table for Parent1-in-Childconstruction table for Parent1-in-Child进入场景。 该表用于告诉Parent1在哪里可以找到它可以访问的数据。 this指向Parent1的数据。 virtual-base offset指示您可以在哪里找到祖父母数据:从此向前32个字节,您将找到Grandparent内存。 你明白了吗? 虚拟基偏移量与top_offset相似,但适用于虚拟类。


现在我们了解了这一点,仅使用construction table for Parent2-in-Childconstruction table for Parent2-in-Childconstruction table for Parent2-in-Child基本相同。 实际上, Parent2-in-Child具有16字节的virtual-base offset


让信息吸收一下。 您准备好继续了吗? 好啊
现在让我们回到VTT 。 这是VTT结构:


地址价值记号目录内容
0x4009a00x400950儿童vtable + 24vtable Child中的Parent1条目
0x4009a80x4009f8Parent1-in-Child + 24的构造vtableParent1-in-Child中的Parent1方法
0x4009b00x400a18Parent1-in-Child + 56的构造vtable父母亲孩子的祖父母方法
0x4009b80x400a98适用于Parent2-in-Child + 24的构造vtableParent2-in-Child中的Parent2方法
0x4009c00x400ab8亲子2 + 56的构造vtable`父母双亲的祖父母方法
0x4009c80x400998儿童+ 96的vtable`vtable Child中的祖父母条目
0x4009d00x400978vtable for Child + 64vtable Child中的“ Parent2条目”

VTT代表virtual-table table ,这意味着它是一个vtable。 例如,这是一个转换表,它知道Parent1针对单个对象, Parent1-in-Child对象还是Parent1-in-SomeOtherObject Parent1了构造函数Parent1 。 它总是在vtable之后立即出现,以便编译器知道在哪里可以找到它。 因此,无需在对象本身中存储另一个指针。


嗯...有很多细节,但是我认为我们涵盖了我想介绍的所有内容。 在第四部分中,我们将讨论更高级别的vtables的细节。 不要跳过,因为这可能是本文中最重要的部分!


第4部分-编译器生成的代码


在本文的这一点上,我们了解了vtablestypeinfo如何适合我们的二进制文件以及编译器如何使用它们。 现在,我们将了解编译器自动为我们完成的部分工作。


建设者


对于任何类的构造函数,都会生成以下代码:


  • 调用父结构(如果有);
  • 设置vtable指针(如果有);
  • 根据初始化器列表初始化成员;
  • 构造函数括号内的代码执行。

如果没有显式代码,以上所有情况都可能发生:


  • 除非另有说明,否则父构造函数默认情况下会自动启动。
  • 如果成员没有默认值或初始化器列表中的条目,则默认情况下会对其进行初始化;
  • 整个构造函数可以标记为= default;
  • 仅vtable分配始终处于隐藏状态。

这是一个例子:


 #include <iostream> #include <string> using namespace std; class Parent { public: Parent() { Foo(); } virtual ~Parent() = default; virtual void Foo() { cout << "Parent" << endl; } int i = 0; }; class Child : public Parent { public: Child() : j(1) { Foo(); } void Foo() override { cout << "Child" << endl; } int j; }; class Grandchild : public Child { public: Grandchild() { Foo(); s = "hello"; } void Foo() override { cout << "Grandchild" << endl; } string s; }; int main() { Grandchild g; } 

让我们为每个类的构造函数编写伪代码:


父母小孩儿孙子
1. vtable = vtable父级;1.调用默认的构造函数Parent;1.调用默认的构造函数Child;
2. i = 0;2. vtable = vtable子级;2. vtable = vtable孙子;
3.调用Foo();3. j = 1;3,调用默认构造函数s;
4.调用Foo();4.调用Foo();
5.调用s的=运算符;

鉴于此,不足为奇的是,在类构造函数的上下文中,vtable指向此类本身的vtable,而不是其特定类。 这意味着可以解决虚拟呼叫,就好像没有任何继承人一样。 因此,结论


 Parent Child Grandchild 

纯虚拟功能呢? 如果未实现(是的,您可以实现纯虚拟功能,但是为什么需要此功能?),您可能(并希望)直接进行段错误。 一些编译器忽略了该错误,这很酷。


破坏者


可以想象,析构函数的行为与构造函数相同,只是顺序相反。


这是一个快速思考的练习:析构函数为什么更改vtable指针,使其指向自己的类,而不是将指针留给特定的类? 答:自从析构函数启动时,所有继承类都已被销毁。 此类的调用方法不是您想要的。


隐式转换


正如我们在第二部分和第三部分中所看到的,指向子对象的指针不一定等于同一实例的父指针(如在多重继承的情况下)。


但是,对于您(开发人员)而言,没有任何其他工作来调用接收父指针的函数。 这是因为当您将指针和引用附加到父类时,编译器会隐式地this进行移位。


动态投放(RTTI)


动态转换使用typeinfo表,我们在第一部分中进行了检查。 他们在运行时执行此操作,在vtable指针指向的位置之前查看typeinfo条目的一个指针,然后从那里使用该类检查是否可以进行typeinfo转换。


这解释了经常使用dynamic_cast成本


方法指针


我计划在将来写一篇有关方法指针的完整文章。 在此之前,我想强调指出,指向虚拟函数的方法的指针实际上将调用重写的方法(与指向非成员函数的指针相反)。


 // TODO:  ,     

检查自己!


现在,您可以自己解释以下代码片段的行为方式:


 #include <iostream> using namespace std; class FooInterface { public: virtual ~FooInterface() = default; virtual void Foo() = 0; }; class BarInterface { public: virtual ~BarInterface() = default; virtual void Bar() = 0; }; class Concrete : public FooInterface, public BarInterface { public: void Foo() override { cout << "Foo()" << endl; } void Bar() override { cout << "Bar()" << endl; } }; int main() { Concrete c; c.Foo(); c.Bar(); FooInterface* foo = &c; foo->Foo(); BarInterface* bar = (BarInterface*)(foo); bar->Bar(); //  "Foo()" - WTF? } 

我的四部分文章到此结束。 希望您像我一样学到新东西。

Source: https://habr.com/ru/post/zh-CN480610/


All Articles