物体布局中的情况下虚拟的功能和多个继承
-
19-09-2019 - |
题
我最近被要求在一次采访对象的布局与的虚拟的功能和多个继承涉及。
我解释了它在上下文中是如何实施,没有多个继承涉及的(即如何编译器而产生的虚拟表,插入一个秘密的指针指向虚拟表在每个对象等)。
它似乎对我有东西丢失在我的解释。
因此,这里的问题(参见下面的例子)
- 什么是确切的记忆布局的目的C类
- 虚表的条目的类C.
- 大小(如返回的sizeof)的目的课程A、B和C。(8,8,16??)
- 如果虚拟继承权是使用。肯定的尺寸和虚表的条目应受到影响?
例编码:
class A {
public:
virtual int funA();
private:
int a;
};
class B {
public:
virtual int funB();
private:
int b;
};
class C : public A, public B {
private:
int c;
};
谢谢!
解决方案
内存布局和VTable布局取决于您的编译器。例如,使用我的GCC,它们看起来像这样:
sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20
请注意,尺寸(int)以及VTable指针所需的空间也可能因编译器到编译器和平台而异。 sizeOf(c)== 20而不是16的原因是GCC为A子对象提供8个字节,B子对象的8个字节和其成员的4个字节 int c
.
Vtable for C C::_ZTV1C: 6u entries 0 (int (*)(...))0 4 (int (*)(...))(& _ZTI1C) 8 A::funA 12 (int (*)(...))-0x00000000000000008 16 (int (*)(...))(& _ZTI1C) 20 B::funB Class C size=20 align=4 base size=20 base align=4 C (0x40bd5e00) 0 vptr=((& C::_ZTV1C) + 8u) A (0x40bd6080) 0 primary-for C (0x40bd5e00) B (0x40bd60c0) 8 vptr=((& C::_ZTV1C) + 20u)
使用虚拟继承
class C : public virtual A, public virtual B
布局更改为
Vtable for C C::_ZTV1C: 12u entries 0 16u 4 8u 8 (int (*)(...))0 12 (int (*)(...))(& _ZTI1C) 16 0u 20 (int (*)(...))-0x00000000000000008 24 (int (*)(...))(& _ZTI1C) 28 A::funA 32 0u 36 (int (*)(...))-0x00000000000000010 40 (int (*)(...))(& _ZTI1C) 44 B::funB VTT for C C::_ZTT1C: 3u entries 0 ((& C::_ZTV1C) + 16u) 4 ((& C::_ZTV1C) + 28u) 8 ((& C::_ZTV1C) + 44u) Class C size=24 align=4 base size=8 base align=4 C (0x40bd5e00) 0 vptridx=0u vptr=((& C::_ZTV1C) + 16u) A (0x40bd6080) 8 virtual vptridx=4u vbaseoffset=-0x0000000000000000c vptr=((& C::_ZTV1C) + 28u) B (0x40bd60c0) 16 virtual vptridx=8u vbaseoffset=-0x00000000000000010 vptr=((& C::_ZTV1C) + 44u)
使用GCC,您可以添加 -fdump-class-hierarchy
获取此信息。
其他提示
1件事要期望多个继承是,您的指针在铸造到(通常不是第一)子类时可能会更改。在调试和回答面试问题时,您应该注意的事情。
首先,一个多晶型类具有至少一个虚拟的功能,因此它具有一个vptr:
struct A {
virtual void foo();
};
编制:
struct A__vtable { // vtable for objects of declared type A
void (*foo__ptr) (A *__this); // pointer to foo() virtual function
};
void A__foo (A *__this); // A::foo ()
// vtable for objects of real (dynamic) type A
const A__vtable A__real = { // vtable is never modified
/*foo__ptr =*/ A__foo
};
struct A {
A__vtable const *__vptr; // ptr to const not const ptr
// vptr is modified at runtime
};
// default constructor for class A (implicitly declared)
void A__ctor (A *__that) {
__that->__vptr = &A__real;
}
注:C++可以编制向另一个高水平的语言,比如C(如cfront做的)或者甚至C++子集的(这里是C++没有 virtual
).我把 __
在编译器而产生的名字。
注意,这是一个 简单的 模型在哪里RTTI不支持;真正的编纂者将增加数据的虚表支持 typeid
.
现在,一个简单的源类:
struct Der : A {
override void foo();
virtual void bar();
};
非虚拟的(*)基类子对象是子对象喜欢件子对象,但同时会员子对象是完整的对象,即。他们真实的(动态)的类型是他们声明的类型,基类子对象是未完成的,并且他们的实际类型更改的施工期间。
(*)虚拟基地有很大的不同,如虚拟件的功能是不同的非虚拟的成员
struct Der__vtable { // vtable for objects of declared type Der
A__vtable __primary_base; // first position
void (*bar__ptr) (Der *__this);
};
// overriding of a virtual function in A:
void Der__foo (A *__this); // Der::foo ()
// new virtual function in Der:
void Der__bar (Der *__this); // Der::bar ()
// vtable for objects of real (dynamic) type Der
const Der__vtable Der__real = {
{ /*foo__ptr =*/ Der__foo },
/*foo__ptr =*/ Der__bar
};
struct Der { // no additional vptr
A __primary_base; // first position
};
这里的"第一位"意味着成员必须是第一(其他成员可能被重新排序):他们的位置偏移量为零,所以我们可以 reinterpret_cast
指针,类型兼容机;在非零抵消,我们将必须做的指针调整运算上的 char*
.
缺乏调整可能似乎不是一个大问题,在任期产生的代码(只是一些立即添加asm说明),但它意味着更多,它意味着这样的指针可以被看作是具有不同的类型:一个目的类型 A__vtable*
可以包含一个指向 Der__vtable
并且被当作一个 Der__vtable*
或 A__vtable*
.同样的指的对象供应作为一个指向 A__vtable
在功能处理类型的对象 A
并且作为一个指向 Der__vtable
在功能处理类型的对象 Der
.
// default constructor for class Der (implicitly declared)
void Der__ctor (Der *__this) {
A__ctor (reinterpret_cast<A*> (__this));
__this->__vptr = reinterpret_cast<A__vtable const*> (&Der__real);
}
你看到的动态型的,因为定义的vptr、改变施工期间作为我们分配一个新的价值vptr(在这种特定情况下的呼吁的基类的构造什么都不做有用,并可以优化远,但它并不是这种情况与非微不足道的构造).
与多个继承:
struct C : A, B {};
一个 C
实例将含有一个 A
和一个 B
, 像是:
struct C {
A base__A; // primary base
B base__B;
};
请注意,只有一个这些基类子对象可以有幸坐在偏零;这是重要的,在许多方面:
转换指针的其他基类(upcasts)将需要一个 调整;相反,upcasts需要相对的调整;
这意味着当做一个虚拟电话与一个基类 指针,
this
有正确的价值的条目的来 类护角.
所以,下列代码:
void B::printaddr() {
printf ("%p", this);
}
void C::printaddr () { // overrides B::printaddr()
printf ("%p", this);
}
可编
void B__printaddr (B *__this) {
printf ("%p", __this);
}
// proper C::printaddr taking a this of type C* (new vtable entry in C)
void C__printaddr (C *__this) {
printf ("%p", __this);
}
// C::printaddr overrider for B::printaddr
// needed for compatibility in vtable
void C__B__printaddr (B *__this) {
C__printaddr (reinterpret_cast<C*>(reinterpret_cast<char*> (__this) - offset__C__B));
}
我们看到的 C__B__printaddr
声明的类型和语义是兼容机与 B__printaddr
, 所以我们可以使用 &C__B__printaddr
在虚表的 B
; C__printaddr
不兼容,但是可以用于涉及一个电话 C
对象,或类源自 C
.
非虚拟件的功能,像一个免费的功能,以访问内部的东西。一个虚拟件的功能是"灵活地点",这可以通过重写。虚拟件的功能宣言》发挥特殊作用的定义一类:其他成员一样,他们是合同的一部分,与外部世界,但是与此同时,他们是合同的一部分,与源类。
非虚拟基类就像是一件对象,在这里我们可以改进行为通过复盖(我们也可以访问受保护的成员)。对于外部世界,继承 A
在 Der
意味着隐性衍生到基本的转换将存在为指针, A&
可以开一个 Der
左值,等等。进一步的源类别(从 Der
),这也意味着虚拟的功能 A
是遗传的 Der
:虚拟的功能 A
可以重写进一步的衍生课程。
当一个类进一步来说 Der2
是来自 Der
, ,隐性转换一个指针的类型 Der2*
要 A*
在语义上执行步骤:第一,一个转换到 Der*
验证(访问控制以及继承有关的 Der2
从 Der
检查与通常的公共/保护/私人/朋友的规则),然后访问控制的 Der
要 A
.非虚拟的继承有关的无法以完善或改写衍生课程。
非虚成员的职能可以直接称为和虚拟的成员必须是所谓的间接地通过虚表(除非真正的对象类型发生了被称为compiler),所以 virtual
关键字增加了一个间接成员的职能访问。就像功能的成员, virtual
关键字增加了一个间接为基础对象进行访问;就像功能,虚拟基础课程中添加一点的灵活性在继承。
当做非虚拟的,重复,多个继承:
struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };
只有两个 Top::i
子对象在 Bottom
(Left::i
和 Right::i
),作为与成员对象:
struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }
没有人感到惊讶的是,有两个 int
子成员(l.t.i
和 r.t.i
).
与虚拟功能:
struct Top { virtual void foo(); };
struct Left : Top { }; // could override foo
struct Right : Top { }; // could override foo
struct Bottom : Left, Right { }; // could override foo (both)
这意味着有两种不同的(无关的)虚拟的功能叫 foo
, 与不同虚表的条目(既作为他们有同样的签名,他们可以有一个共同护角).
语义不虚拟基类如下事实,即基本的,不虚拟的,继承的是一个独特的关系:继承关系之间建立的左边和上无法修改通过进一步的推导,这样一个事实,即类似关系之间存在 Right
和 Top
不能影响这种关系。特别是,这意味着 Left::Top::foo()
可以重写 Left
在 Bottom
, 但 Right
, 没有继承关系 Left::Top
, 不设置此制点。
虚拟基类是不同的:一个虚拟的继承是一种共同的关系,可以自定义中得出类:
struct Top { int i; virtual void foo(); };
struct vLeft : virtual Top { };
struct vRight : virtual Top { };
struct vBottom : vLeft, vRight { };
在这里,这仅仅是一个基类子对象 Top
, ,只有一个 int
部件。
执行:
房间不虚拟基类分配的基础上一个静态的布局与固定抵消在源类。注意到布局的一个源类是包括在的布局,更多的源类别,因此的确切位置的子对象并不取决于实(动态)类型的对象(就像一个地址不虚拟功能是恒定)。另一方面,位置的子对象中的一类,与的虚拟的继承是由动态型(只是喜欢的地址的执行情况的一个虚拟的功能是已知的,只有当的动态型是已知的)。
该位置的子对象将在运行时确定与vptr和虚表(重复使用的现有vptr意味着较少的空间开销),或者直接的内部指子对象(更多的开销,较少的间接需要的)。
因为偏移的一个虚拟的基类仅判定为一个完整的对象,并无法得知对于给定的声明的类型, 虚拟基础不能被分配在偏移量为零,并从来都不是一个主要基地.一个源类将永远不再使用的vptr的虚拟基础作为其自己的vptr.
中期可能的翻译:
struct vLeft__vtable {
int Top__offset; // relative vLeft-Top offset
void (*foo__ptr) (vLeft *__this);
// additional virtual member function go here
};
// this is what a subobject of type vLeft looks like
struct vLeft__subobject {
vLeft__vtable const *__vptr;
// data members go here
};
void vLeft__subobject__ctor (vLeft__subobject *__this) {
// initialise data members
}
// this is a complete object of type vLeft
struct vLeft__complete {
vLeft__subobject __sub;
Top Top__base;
};
// non virtual calls to vLeft::foo
void vLeft__real__foo (vLeft__complete *__this);
// virtual function implementation: call via base class
// layout is vLeft__complete
void Top__in__vLeft__foo (Top *__this) {
// inverse .Top__base member access
char *cp = reinterpret_cast<char*> (__this);
cp -= offsetof (vLeft__complete,Top__base);
vLeft__complete *__real = reinterpret_cast<vLeft__complete*> (cp);
vLeft__real__foo (__real);
}
void vLeft__foo (vLeft *__this) {
vLeft__real__foo (reinterpret_cast<vLeft__complete*> (__this));
}
// Top vtable for objects of real type vLeft
const Top__vtable Top__in__vLeft__real = {
/*foo__ptr =*/ Top__in__vLeft__foo
};
// vLeft vtable for objects of real type vLeft
const vLeft__vtable vLeft__real = {
/*Top__offset=*/ offsetof(vLeft__complete, Top__base),
/*foo__ptr =*/ vLeft__foo
};
void vLeft__complete__ctor (vLeft__complete *__this) {
// construct virtual bases first
Top__ctor (&__this->Top__base);
// construct non virtual bases:
// change dynamic type to vLeft
// adjust both virtual base class vptr and current vptr
__this->Top__base.__vptr = &Top__in__vLeft__real;
__this->__vptr = &vLeft__real;
vLeft__subobject__ctor (&__this->__sub);
}
对于一个目的已知类型,访问基类通过 vLeft__complete
:
struct a_vLeft {
vLeft m;
};
void f(a_vLeft &r) {
Top &t = r.m; // upcast
printf ("%p", &t);
}
翻译:
struct a_vLeft {
vLeft__complete m;
};
void f(a_vLeft &r) {
Top &t = r.m.Top__base;
printf ("%p", &t);
}
这里真正的(动态)型的 r.m
是已知的,因此是相对位置的子对象是已知在汇编的时间。但在这里:
void f(vLeft &r) {
Top &t = r; // upcast
printf ("%p", &t);
}
真正的(动态)型的 r
不知道,这样的访问是通过vptr:
void f(vLeft &r) {
int off = r.__vptr->Top__offset;
char *p = reinterpret_cast<char*> (&r) + off;
printf ("%p", p);
}
这种功能可以接受的任何源类有不同的布局:
// this is what a subobject of type vBottom looks like
struct vBottom__subobject {
vLeft__subobject vLeft__base; // primary base
vRight__subobject vRight__base;
// data members go here
};
// this is a complete object of type vBottom
struct vBottom__complete {
vBottom__subobject __sub;
// virtual base classes follow:
Top Top__base;
};
注意, vLeft
基类是在一个固定的位置 vBottom__subobject
, ,所以 vBottom__subobject.__ptr
是用作vptr全 vBottom
.
语义:
继承有关的所有源类;这意味着正确的,以复盖共享,所以 vRight
可以重写 vLeft::foo
.这将创建一个分享的职责: vLeft
和 vRight
必须商定如何,他们自定义 Top
:
struct Top { virtual void foo(); };
struct vLeft : virtual Top {
override void foo(); // I want to customise Top
};
struct vRight : virtual Top {
override void foo(); // I want to customise Top
};
struct vBottom : vLeft, vRight { }; // error
在这里,我们看到一个冲突: vLeft
和 vRight
寻求定义的行为的唯一foo虚拟的功能, vBottom
定义是错误缺乏一个共同护角.
struct vBottom : vLeft, vRight {
override void foo(); // reconcile vLeft and vRight
// with a common overrider
};
执行:
该建筑的班与非虚拟基础课程与非虚拟基础课程涉及调基类的构造在相同的顺序,因为做了件变,改变的动态型的,每次我们输入一个构造函数.在施工期间,基类子对象真正地采取行动,如果他们完成的对象(这是甚至真实的不可能完成抽象的基类子对象:他们的对象的定义(纯)虚拟的功能)。虚拟的职能和RTTI可以被称为在施工期间(当然除了纯粹的虚拟功能)。
建设一类是与非虚拟基类虚拟基地是更复杂:在施工期间,动态型的基类型,但布的虚拟基础仍然是布局的大多数源类型的尚未构成的,所以我们需要更多的vtable都从来描述这种状态:
// vtable for construction of vLeft subobject of future type vBottom
const vLeft__vtable vLeft__ctor__vBottom = {
/*Top__offset=*/ offsetof(vBottom__complete, Top__base),
/*foo__ptr =*/ vLeft__foo
};
虚拟的功能是那些的 vLeft
(在建设、vBottom目生命期没有开始),而虚拟基地点是那些的 vBottom
(定义的 vBottom__complete
翻译的反对).
语义:
在初始化,显而易见的是,我们必须小心,不要使用对象之前,它是初始化。因为C++给了我们一个名称之前的一个目的是充分初始化,这是很容易做到的是:
int foo (int *p) { return *pi; }
int i = foo(&i);
或与这一指在构造:
struct silly {
int i;
std::string s;
static int foo (bad *p) {
p->s.empty(); // s is not even constructed!
return p->i; // i is not set!
}
silly () : i(foo(this)) { }
};
这是很明显的是,任何使用 this
在构造函数-init列必须仔细检查。后初始化的所有成员, this
可以通过到其他职务和注册在某些组(直到的破坏开始)。
什么是不太明显的是,当建设的一类涉及共同的虚拟基地,子对象别构成:在建设过程中的一个 vBottom
:
第一个虚拟基地的构成:时
Top
建造的,它是构成像一个正常问题(Top
甚至不知道它是虚拟基础)然后基类构成在左到右顺序:的
vLeft
子对象是构造和功能变得作为一个正常的vLeft
(但是有一个vBottom
布局),这样的Top
基类子对象,现在有一个vLeft
动态的类型;的
vRight
子对象的施工的开始,和动态型的基类变化vRight;但vRight
不是来自vLeft
, 不知道什么vLeft
, ,所以vLeft
基地是现在破碎;当身体的
Bottom
构造的开始,这种类型的所有子对象已经稳定,vLeft
是的功能。
我不确定如何在不提及对齐或填充位的情况下如何将此答案作为完整的答案。
让我给予一些协调背景:
“当a是n个字节的倍数时(n是2的幂)时,内存地址a被称为n字节。在此上下文中,一个字节是内存访问的最小单元,即每个内存地址指定在二进制中表达时,一个不同的字节。
替代措辞b-bit对准指定AB/8字节对准地址(Ex。64位对齐为8个字节对齐)。
据说,当访问的基准为n字节时,内存访问是对齐的,并且基准地址是n个字节对齐的。当不对准内存访问时,据说它是未对准的。请注意,根据定义,字节内存访问始终是对齐的。
一个记忆指针指的是n个字节长的原始数据,据说只有仅允许包含n字节对齐的地址,否则据说它是不协调的。当(并且仅当)将汇总的每个原始基准对齐时,将指向数据聚合(数据结构或数组)的内存指针对齐。
请注意,上面的定义假定每个原始基准是两个字节长的幂。如果不是这种情况(如x86上的80位浮点),上下文会影响基准是否对齐的条件。
数据结构可以存储在存储器中,其静态大小称为有界或堆,其动态大小称为无限。” - 来自Wiki ...
为了保持对齐方式,编译器将填充位插入结构/类对象的编译代码中。 “尽管编译器(或解释器)通常会在对齐边界上分配单个数据项,但数据结构通常具有不同对齐要求的成员。为了保持正确的对齐,翻译器通常插入其他未命名的数据成员,以便每个成员都适当地对齐。此外,此外整个数据结构可以用最终的未命名成员填充。这允许将一系列结构的每个成员正确对齐。....... ....
仅当结构构件之后是具有较大对齐要求的成员或在结构末端的插入时,才插入填充物。
要获取有关GCC如何做的更多信息,请查看
http://www.delorie.com/gnu/docs/gcc/gccint_111.html
并搜索文本“基本align”
现在让我们解决这个问题:
使用示例类,我为在64位Ubuntu上运行的GCC编译器创建了此程序。
int main() {
cout << "!!!Hello World!!!" << endl; // prints !!!Hello World!!!
A objA;
C objC;
cout<<__alignof__(objA.a)<<endl;
cout<<sizeof(void*)<<endl;
cout<<sizeof(int)<<endl;
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
cout<<sizeof(C)<<endl;
cout<<__alignof__(objC.a)<<endl;
cout<<__alignof__(A)<<endl;
cout<<__alignof__(C)<<endl;
return 0;
}
该程序的结果如下:
4
8
4
16
16
32
4
8
8
现在让我解释一下。由于A&B都具有虚拟函数,因此它们将创建单独的VTABLES,并且将在其对象的开头添加VPTR。
因此,A类的对象将具有VPTR(指向A的VTable)和一个INT。指针将长8个字节,INT将为4个字节长。因此,在编译大小为12个字节之前。但是编译器将在int a作为填充位时添加额外的4个字节。因此,在编译后,A的对象大小为12+4 = 16。
同样,对于B类的对象。
现在,C的对象将有两个VPTR(每个A级A和B类)和3个INT(A,B,C)。因此,大小应该为8(vptr a) + 4(int a) + 4(填充字节) + 8(vptr b) + 4(int b) + 4(int c)= 32个字节。因此,C的总大小为32个字节。