首先,多态类至少有一个虚函数,所以它有一个 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的简单模型;真正的编译器会在 vtable 中添加数据以支持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
指针,类型是兼容的;在非零偏移处,我们必须使用算术 on 进行指针调整char*
。
就生成的代码而言,缺少调整可能看起来没什么大不了的(只是一些添加了立即的 asm 指令),但它的意义远不止于此,这意味着这些指针可以被视为具有不同的类型:类型的对象A__vtable*
可以包含指向Der__vtable
并被视为 aDer__vtable*
或 a的指针A__vtable*
。同一个指针对象用作指向A__vtable
处理类型对象的 in 函数的指针和指向处理类型对象的 in 函数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 分配了一个新值(在这种特殊情况下,对基类构造函数的调用没有任何用处,可以优化掉,但它不是t 具有非平凡构造函数的情况)。
多重继承:
struct C : A, B {};
一个C
实例将包含 aA
和 a B
,如下所示:
struct C {
A base__A; // primary base
B base__B;
};
请注意,这些基类子对象中只有一个可以拥有偏移量为零的特权;这在很多方面都很重要:
所以下面的代码:
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
在 ; 的 vtable 中使用B
。C__printaddr
不兼容,但可用于涉及C
对象或从C
.
非虚拟成员函数就像一个可以访问内部内容的自由函数。虚拟成员函数是可以通过重写来定制的“灵活性点”。虚成员函数声明在类的定义中起着特殊的作用:像其他成员一样,它们是与外部世界的契约的一部分,但同时它们也是与派生类的契约的一部分。
非虚拟基类就像一个成员对象,我们可以通过覆盖来改进行为(我们也可以访问受保护的成员)。对于外部世界,A
in的继承Der
意味着指针将存在隐式派生到基的转换,aA&
可以绑定到Der
左值等。对于进一步的派生类(派生自Der
),这也意味着A
在 : 中继承的Der
虚函数A
可以在进一步的派生类中被覆盖。
当一个类进一步派生时,比如Der2
派生自,隐式转换类型为 toDer
的指针在语义上逐步执行:首先,验证转换为(对from的继承关系的访问控制与通常的 public/protected /private/friend 规则),然后是to的访问控制。不能在派生类中细化或覆盖非虚拟继承关系。Der2*
A*
Der*
Der2
Der
Der
A
非虚成员函数可以直接调用,而虚成员必须通过 vtable 间接调用(除非编译器恰好知道真实对象类型),因此该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
,具有不同的 vtable 条目(由于它们具有相同的签名,因此它们可以具有共同的覆盖器)。
非虚基类的语义源于基本的、非虚的继承是一种排他关系:Left 和 Top 之间建立的继承关系不能通过进一步的推导来修改,因此存在相似关系的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
成员。
执行:
非虚拟基类的空间是基于派生类中具有固定偏移量的静态布局分配的。请注意,派生类的布局包含在更多派生类的布局中,因此子对象的确切位置不取决于对象的真实(动态)类型(就像非虚函数的地址是常量一样)。OTOH,具有虚拟继承的类中子对象的位置由动态类型确定(就像只有知道动态类型时才知道虚函数的实现地址一样)。
子对象的位置将在运行时通过 vptr 和 vtable(重用现有 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
基类位于 a 中的固定位置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
};
执行:
具有非虚拟基类和非虚拟基类的类的构造涉及以与成员变量相同的顺序调用基类构造函数,每次我们输入一个ctor时都会改变动态类型。在构造过程中,基类子对象的行为就像它们是完整的对象一样(这对于不可能的完整抽象基类子对象也是如此:它们是具有未定义(纯)虚函数的对象)。构造过程中可以调用虚函数和RTTI(当然纯虚函数除外)。
具有非虚基类的类的构造与虚基类比较复杂:构造时动态类型是基类类型,但虚基的布局仍然是尚未构造的最派生类型的布局,所以我们需要更多的虚表来描述这种状态:
// 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);
或在构造函数中使用 this 指针:
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
在 ctor-init-list 中的任何使用都必须仔细检查。初始化所有成员后,this
可以传递给其他函数并在某个集合中注册(直到销毁开始)。
不太明显的是,当构造一个涉及共享虚拟基的类时,子对象停止构造:在构造 a 期间vBottom
:
首先构造虚拟基础:Top
构造时,它像普通主体一样构造(Top
甚至不知道它是虚拟基础)
然后基类按从左到右的顺序构造:vLeft
子对象被构造并成为正常功能vLeft
(但具有vBottom
布局),因此Top
基类子对象现在具有vLeft
动态类型;
子vRight
对象构造开始,基类的动态类型更改为 vRight;butvRight
不是从 派生的vLeft
,不知道任何关于vLeft
的,所以vLeft
基础现在被打破了;
当Bottom
构造函数的主体开始时,所有子对象的类型都已稳定并vLeft
再次起作用。