本文的翻译是专门为“ 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
? 我们将尽快答复他们。
让我们从子内存结构开始:
实际上, Child
只有1个祖父母或外祖父母。 不平凡的是,他是记忆中的最后一位,尽管他是层次结构中最高的。
这是vtable
结构:
上面有一个新概念- virtual-base offset
。 很快,我们将了解他在那做什么。
接下来,让我们探索这些看起来很奇怪的construction vtables
。 这是vtable for Parent1-in-Child
的构造vtable for Parent1-in-Child
:
目前,我认为描述该过程比在您身上堆放更多带有随机数的表更容易理解。 因此:
想象你是一个Child
。 要求您在新的内存中构建自己。 由于您直接继承Grandparent
(这是虚拟继承的意思),因此您将直接直接调用其构造函数(如果不是虚拟继承,则将调用构造函数Parent1
,而后者又将调用Grandparent
构造函数)。 设置this += 32
字节,因为这是Grandparent
数据所在的位置,然后调用构造函数。 很简单
然后是时候构建Parent1
。 Parent1
可以放心地假设,到他构造自己的时候,已经创建了Grandparent
,因此,例如,他可以访问Grandparent
数据和方法。 但是,等等,他怎么知道在哪里可以找到这些数据? 它们与变量Parent1
不在同一个位置!
construction table for Parent1-in-Child
的construction table for Parent1-in-Child
进入场景。 该表用于告诉Parent1
在哪里可以找到它可以访问的数据。 this
指向Parent1
的数据。 virtual-base offset
指示您可以在哪里找到祖父母数据:从此向前32个字节,您将找到Grandparent
内存。 你明白了吗? 虚拟基偏移量与top_offset相似,但适用于虚拟类。
现在我们了解了这一点,仅使用construction table for Parent2-in-Child
的construction table for Parent2-in-Child
的construction table for Parent2-in-Child
基本相同。 实际上, Parent2-in-Child
具有16字节的virtual-base offset
。
让信息吸收一下。 您准备好继续了吗? 好啊
现在让我们回到VTT
。 这是VTT
结构:
VTT
代表virtual-table table
,这意味着它是一个vtable。 例如,这是一个转换表,它知道Parent1
针对单个对象, Parent1-in-Child
对象还是Parent1-in-SomeOtherObject
Parent1
了构造函数Parent1
。 它总是在vtable
之后立即出现,以便编译器知道在哪里可以找到它。 因此,无需在对象本身中存储另一个指针。
嗯...有很多细节,但是我认为我们涵盖了我想介绍的所有内容。 在第四部分中,我们将讨论更高级别的vtables
的细节。 不要跳过,因为这可能是本文中最重要的部分!
第4部分-编译器生成的代码
在本文的这一点上,我们了解了vtables
和typeinfo
如何适合我们的二进制文件以及编译器如何使用它们。 现在,我们将了解编译器自动为我们完成的部分工作。
建设者
对于任何类的构造函数,都会生成以下代码:
- 调用父结构(如果有);
- 设置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; }
让我们为每个类的构造函数编写伪代码:
鉴于此,不足为奇的是,在类构造函数的上下文中,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? }
我的四部分文章到此结束。 希望您像我一样学到新东西。