虚函数和多继承的对象布局

Ank*_*kur 7 c++ multiple-inheritance virtual-inheritance vtable memory-layout

我最近在接受采访时询问了有关虚拟功能和多重继承的对象布局.
我在没有涉及多重继承的情况下如何实现它的上下文中解释了它(即编译器如何生成虚拟表,在每个对象中插入指向虚拟表的秘密指针等等).
在我看来,我的解释中缺少一些东西.
所以这里有问题(见下面的例子)

  1. C类对象的确切内存布局是什么?
  2. C类的虚拟表条目
  3. A,B和C类对象的大小(由sizeof返回)(8,8,16 ??)
  4. 如果使用虚拟继承怎么办?当然,大小和虚拟表条目应该受到影响吗?

示例代码:

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;  
};   
Run Code Online (Sandbox Code Playgroud)

谢谢!

Tob*_*ias 13

内存布局和vtable布局取决于您的编译器.以我的gcc为例,它们看起来像这样:

sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20
Run Code Online (Sandbox Code Playgroud)

请注意,sizeof(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
Run Code Online (Sandbox Code Playgroud)

布局更改为

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以获取此信息.


ste*_*anv 5

对于多重继承,可以期望的一件事是,在转换为(通常不是第一个)子类时,指针可以更改。在调试和回答面试问题时,您应该注意的一些事项。


cur*_*guy 5

首先,多态类至少有一个虚函数,因此它有一个 vptr:

struct A {
    virtual void foo();
};
Run Code Online (Sandbox Code Playgroud)

被编译为:

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;
}
Run Code Online (Sandbox Code Playgroud)

备注:C++ 可以编译为另一种高级语言,如 C(如 cfront 所做的),甚至编译为 C++ 子集(此处为不带 的 C++ virtual)。我输入__了编译器生成的名称。

请注意,这是一个不支持 RTTI 的简单模型;真正的编译器会在 vtable 中添加数据来支持typeid.

现在,一个简单的派生类:

struct Der : A {
    override void foo();
    virtual void bar();
};
Run Code Online (Sandbox Code Playgroud)

非虚(*)基类子对象是像成员子对象一样的子对象,但成员子对象是完整的对象,即。它们的真实(动态)类型是它们声明的类型,基类子对象不完整,并且它们的真实类型在构造过程中发生变化。

(*) 虚拟基有很大不同,就像虚拟成员函数与非虚拟成员不同

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
};
Run Code Online (Sandbox Code Playgroud)

这里的“第一个位置”意味着该成员必须是第一个(其他成员可以重新排序):它们位于偏移量零处,因此我们可以使用reinterpret_cast指针,类型是兼容的;在非零偏移处,我们必须使用算术进行指针调整char*

就生成的代码而言,缺乏调整似乎不是什么大问题(只是一些添加立即 asm 指令),但它的意义远不止于此,它意味着此类指针可以被视为具有不同的类型:类型的对象A__vtable*可以包含指向Der__vtable并被视为 aDer__vtable*或 a的指针A__vtable*A__vtable同一个指针对象既可以用作处理类型对象的 in 函数的指针,也可以用作处理类型对象的 in 函数的A指针。Der__vtableDer

// 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);
}
Run Code Online (Sandbox Code Playgroud)

您会看到,当我们为 vptr 分配新值时,由 vptr 定义的动态类型会在构造过程中发生变化(在这种特殊情况下,对基类构造函数的调用没有任何用处,可以进行优化,但它不是) t 是非平凡构造函数的情况)。

具有多重继承:

struct C : A, B {};
Run Code Online (Sandbox Code Playgroud)

一个C实例将包含 aA和 a B,如下所示:

struct C {
    A base__A; // primary base
    B base__B;
};
Run Code Online (Sandbox Code Playgroud)

请注意,这些基类子对象中只有一个可以拥有位于偏移量零处的特权;这在很多方面都很重要:

  • 指向其他基类的指针转换(向上转换)将需要调整;相反,向上转型需要相反的调整;

  • 这意味着当使用基类指针进行虚拟调用时,this派生类重写器中的条目具有正确的值。

所以下面的代码:

void B::printaddr() {
    printf ("%p", this);
}

void C::printaddr () { // overrides B::printaddr()
    printf ("%p", this);
}
Run Code Online (Sandbox Code Playgroud)

可以编译为

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));
}
Run Code Online (Sandbox Code Playgroud)

我们看到C__B__printaddr声明的类型和语义与 兼容,因此我们可以在 ; 的 vtable 中B__printaddr使用;不兼容,但可用于涉及对象或从 派生的类的调用。&C__B__printaddrBC__printaddrCC

非虚拟成员函数就像可以访问内部内容的自由函数。虚拟成员函数是“灵活性点”,可以通过重写来定制。虚拟成员函数声明在类的定义中起着特殊的作用:与其他成员一样,它们是与外部世界契约的一部分,但同时它们也是与派生类契约的一部分。

非虚拟基类就像一个成员对象,我们可以通过重写来细化行为(我们也可以访问受保护的成员)。A对于外部世界, in的继承Der意味着指针将存在隐式派生到基类转换,aA&可以绑定到Der左值等。对于进一步的派生类(派生自Der),这也意味着 的虚函数A在 中继承Der: 中的虚函数A可以在进一步的派生类中重写。

当一个类进一步派生时,比如Der2从 派生Der,类型Der2*为 to的指针的隐式转换A*将在语义上执行:首先,验证 to 的转换(使用通常的 public/protected 方法检查对fromDer*的继承关系的访问控制) /private/friend 规则),然后对的访问控制。非虚拟继承关系不能在派生类中细化或重写。Der2DerDerA

非虚成员函数可以直接调用,而虚成员必须通过vtable间接调用(除非编译器恰好知道真实对象类型),因此该virtual关键字为成员函数访问添加了间接访问。就像函数成员一样,virtual关键字添加了对基对象访问的间接访问;就像函数一样,虚拟基类在继承中添加了灵活性。

当进行非虚拟、重复、多重继承时:

struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };
Run Code Online (Sandbox Code Playgroud)

(和) 中只有两个子Top::i对象,与成员对象一样:BottomLeft::iRight::i

struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }
Run Code Online (Sandbox Code Playgroud)

没有人对有两个int子成员(l.t.ir.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)
Run Code Online (Sandbox Code Playgroud)

这意味着有两个不同的(不相关的)虚拟函数,称为foo,具有不同的 vtable 条目(因为它们具有相同的签名,所以它们可以有一个共同的重写器)。

非虚拟基类的语义源于以下事实:基本的非虚拟继承是一种排他关系:Left 和 Top 之间建立的继承关系不能通过进一步的派生来修改,因此 和 之间也存在类似RightTop关系影响这个关系。特别是,这意味着可以在和 中Left::Top::foo()重写,但与 没有继承关系 ,无法设置此定制点。LeftBottomRightLeft::Top

虚拟基类有所不同:虚拟继承是可以在派生类中自定义的共享关系:

struct Top { int i; virtual void foo(); };
struct vLeft : virtual Top { }; 
struct vRight : virtual Top { };
struct vBottom : vLeft, vRight { }; 
Run Code Online (Sandbox Code Playgroud)

在这里,这只是一个基类 subobject 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);
}
Run Code Online (Sandbox Code Playgroud)

对于已知类型的对象,可以通过以下方式访问基类vLeft__complete

struct a_vLeft {
    vLeft m;
};

void f(a_vLeft &r) {
    Top &t = r.m; // upcast
    printf ("%p", &t);
}
Run Code Online (Sandbox Code Playgroud)

翻译为:

struct a_vLeft {
    vLeft__complete m;
};

void f(a_vLeft &r) {
    Top &t = r.m.Top__base;
    printf ("%p", &t);
}
Run Code Online (Sandbox Code Playgroud)

这里 的真实(动态)类型r.m是已知的,因此子对象的相对位置在编译时也是已知的。但在这儿:

void f(vLeft &r) {
    Top &t = r; // upcast
    printf ("%p", &t);
}
Run Code Online (Sandbox Code Playgroud)

的真实(动态)类型r未知,因此通过 vptr 进行访问:

void f(vLeft &r) {
    int off = r.__vptr->Top__offset;
    char *p = reinterpret_cast<char*> (&r) + off;
    printf ("%p", p);
}
Run Code Online (Sandbox Code Playgroud)

此函数可以接受具有不同布局的任何派生类:

// 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;
}; 
Run Code Online (Sandbox Code Playgroud)

请注意,vLeft基类位于 a 中的固定位置vBottom__subobject,因此vBottom__subobject.__ptr用作整个 的 vptr vBottom

语义:

继承关系为所有派生类所共享;这意味着 override 的权利是共享的,因此vRight可以 override 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
Run Code Online (Sandbox Code Playgroud)

这里我们看到一个冲突:vLeftvRight试图定义唯一 foo 虚拟函数的行为,但vBottom由于缺乏通用重写器,定义是错误的。

struct vBottom : vLeft, vRight  { 
    override void foo(); // reconcile vLeft and vRight 
                         // with a common overrider
};
Run Code Online (Sandbox Code Playgroud)

执行:

具有非虚拟基类的类的构造涉及到以与成员变量相同的顺序调用基类构造函数,每次输入构造函数时都会更改动态类型。在构造过程中,基类子对象实际上就像是完整的对象一样(对于不可能的完整抽象基类子对象也是如此:它们是具有未定义(纯)虚函数的对象)。虚函数和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 
};
Run Code Online (Sandbox Code Playgroud)

虚拟函数是 的函数vLeft(在构造期间,vBottom 对象的生存期尚未开始),而虚拟基位置是 a 的函数vBottom(如vBottom__complete翻译对象中所定义)。

语义:

在初始化过程中,显然我们必须小心,不要在初始化之前使用对象。因为 C++ 在对象完全初始化之前给我们一个名称,所以很容易做到这一点:

int foo (int *p) { return *pi; }
int i = foo(&i); 
Run Code Online (Sandbox Code Playgroud)

或者在构造函数中使用 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)) { }
};
Run Code Online (Sandbox Code Playgroud)

很明显,this必须仔细检查 ctor-init-list 中的任何使用。所有成员初始化后,this可以传递给其他函数并在某个集合中注册(直到销毁开始)。

不太明显的是,当构建涉及共享虚拟基的类时,子对象将停止构建:在构建 a 期间vBottom

  • 首先构建虚拟基地:Top构建时,它像普通主体一样构建(Top甚至不知道它是虚拟基地)

  • 然后按从左到右的顺序构造基类:vLeft子对象被构造并变得像正常一样起作用vLeft(但具有vBottom布局),因此Top基类子对象现在具有vLeft动态类型;

  • vRight对象构造开始,基类的动态类型变为vRight;但vRight不是源自vLeft,也不了解vLeft,因此vLeft基础现在已损坏;

  • 当构造函数的主体Bottom开始时,所有子对象的类型都已稳定并vLeft再次发挥作用。