在的虚拟功能和多重继承的情况下对象布局(Object layout in case of virtu

2019-06-18 19:04发布

我在大约与参与虚拟函数和多重继承对象布局的采访,最近问。
我在它的方式,而不涉及多重继承(即生成的虚拟表编译器,如何插入一个秘密指针到虚拟表中的每个对象等)来实现上下文解释它。
在我看来,有一些失踪我的解释。
因此,这里有问题(见下面的例子)

  1. 什么是C类的对象的确切内存布局
  2. 对于C类虚拟表中的项
  3. 类A的对象的尺寸(如通过的sizeof返回),B和C(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;  
};   

谢谢!

Answer 1:

内存布局和虚函数表布局取决于你的编译器。 用我的例如GCC,他们是这样的:

sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20

需要注意的是的sizeof(int)和所需的虚表指针也可以从编译器编译器和平台,以平台的改变空间。 之所以的sizeof(C)== 20,而不是图16是GCC给它的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来获取此信息。



Answer 2:

1件事期待与多重继承是铸造的(通常不是第一个)子类时鼠标指针会发生变化。 有些事情,你应该知道在调试和回答面试问题的。



Answer 3:

首先,多态类具有至少一个虚拟函数,所以它有一个的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*

缺乏调节的可能似乎不是一个大问题中的生成代码术语(只是一些添加立即汇编指令),但它意味着比这多,这意味着这样的指针可作为具有不同类型的被看作:类型的对象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(在调用基类的构造函数没有任何用处,可以优化掉这种特殊情况下,但它不是” T,带非平凡的构造的情况下)。

多重继承:

struct C : A, B {};

C实例将含有AB ,像:

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

非虚拟成员函数是这样的访问内部的东西自由功能。 虚拟成员函数是“灵活性点”,这可以通过重写来定制。 虚成员函数声明在类的定义发挥特殊的作用:同其他成员一样,他们与外部世界的合同的一部分,但同时它们与派生类合同的一部分。

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

当一个类被进一步衍生,说Der2源自Der ,隐式转换类型的指针Der2*A*在语义在步骤执行:首先,转换到Der*被验证(访问控制到的继承关系Der2Der检查与通常的公共/保护/私营/朋友规则),随后的访问控制DerA 。 非虚拟继承关系不能够被精制或派生类覆盖。

非虚拟成员函数可以直接调用和虚拟成员必须间接地通过虚表被调用(除非所述真实对象类型恰好由编译器是已知的),因此virtual关键字将添加的间接向成员功能的访问。 就像对于函数成员,所述virtual关键字将添加的间接到基础对象访问; 就像对于功能,虚拟基类在添加继承的柔性点。

在做非虚,重复,多重继承:

struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };

只有两个Top::i的子对象BottomLeft::iRight::i ),与成员的对象:

struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }

没有人感到惊讶的是有两个int子构件( ltirti )。

虚函数:

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 ,具有鲜明的虚表条目(既作为它们具有相同的签名,就可以有一个共同超控器)。

Left和Top之间建立的继承关系不能由进一步推导进行修改,以便之间存在相似的关系的事实:非虚基类的语义的事实,基本的,非虚拟,继承是排他关系如下RightTop可以在不影响这种关系。 特别是,它意味着Left::Top::foo()可以被覆盖LeftBottom ,但是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和V表(现有的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);
}

这里真正的(动态)型的rm是已知的,所以在编译时已知的子对象的相对位置。 但在这里:

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 。 这将创建一个分担责任: vLeftvRight必须同意他们如何自定义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

这里我们看到了一个矛盾: vLeftvRight寻求定义只FOO虚函数的行为,并vBottom定义是错误的,因为缺乏一个共同的超控器的。

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

执行:

类与非虚基类的非虚基类的构造需要调用的顺序相同的基类的构造函数为已完成的成员变量,改变我们每进入一个构造函数时的动态类型。 在施工期间,基类子对象确实表现得好像它们是完整的对象(这是不可能完成的抽象基类的子对象甚至是真实的:它们与不确定的(纯)虚函数的对象)。 虚函数和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); 

或与该指针在构造函数:

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在构造函数,初始化列表必须仔细检查。 所有成员的初始化后, this可以被传递到其他功能,并且在一些组注册(直到破坏开始)。

什么是不太明显的是,当一个类的建设涉及到共享虚拟基础,停止在构建子对象:一个施工时vBottom

  • 第一虚拟基础构造:当Top被构造,其构造像正常受试者( Top甚至不知道它是虚基)

  • 然后基类被构造中从左到右依次为: vLeft子对象被构造和变作为一个正常的功能vLeft (但具有vBottom布局),故Top基类子对象现在有一个vLeft动态类型;

  • 所述vRight子对象施工开始,并且动态类型的基类的变化vRight; 但vRight不是源自vLeft ,不知道什么vLeft ,所以vLeft基地现在分成;

  • 在体内,当Bottom的构造开始,该类型的所有子对象都稳定, vLeft是功能性的一次。



Answer 4:

我不知道该怎么回答可以作为一个完整的答案没有对齐或填充比特的提。

让我举对准的一点背景:

“A存储器地址的,被认为是对准的n字节时a是n个字节的倍数(其中,n是2的幂)。在这种情况下的一个字节是存储器存取的最小单位,即,每个存储器地址指定不同的字节。一个n字节对齐地址将具有的log 2(n)的二进制表达时至少-显著零。

对准的备用措词b比特指定AB / 8字节对齐地址(来自对准的64位是8个字节对齐)。

一个存储器存取被认为是当比对时被访问的数据是n个字节长和所述数据地址是n字节对齐。 当存储器存取未对齐,它被认为是不重合。 按照定义字节存储器访问始终是对齐的。

一种存储器指针指的是由n个字节长是说,如果它仅允许以包含n字节对齐地址对齐原始数据,否则它被说成是不对齐的。 其指的是数据集合体(数据结构或阵列)存储器指针当(且仅当)在聚集的每个图元数据是对齐的对齐。

注意,上面的定义假设每个原始数据是两个字节的功率长。 当不是这种情况下(与x86 80位浮点)的上下文影响,其中基准被认为是对准与否的条件。

数据结构可以存储在存储器中的堆栈称为界或与已知为无界动态大小堆的静态尺寸上的“ - 来自维基...

为了保持对准,编译器插入在结构/类对象的经编译的代码的填充比特。 “虽然编译器(或翻译)上对齐的边界通常分配各个数据项,数据结构常常具有不同的对齐要求的成员。为了保持适当对准的翻译通常插入附加无名数据成员,使得每个部件被适当地对准。另外数据结构作为一个整体可与最终无名构件填充。这允许结构能够被正确地对准的阵列的每个成员。.... ....

当一个结构件,然后用一个较大的对齐要求,或在结构的”端部的构件填充只能插入 - 维基

要获取有关GCC是怎么做的更多信息,请查看

http://www.delorie.com/gnu/docs/gcc/gccint_111.html

并搜索文本“基本对齐”

现在,让我们来解决这个问题:

使用示例类,我已经建立该程序用于在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都具有虚拟功能,它们将创建单独的虚函数表和vptr的将在它们的对象的开始,分别加入。

因此类A的对象将具有的vptr(指向A的VTABLE)和int。 指针将是8字节长,INT将是4字节长。 因此前编译的大小是12个字节。 但是,编译器会在INT一结束作为填充比特添加额外的4个字节。 因此编译之后,A的对象的大小将是12 + 4 = 16。

同样,对于B类的对象。

现在的C对象将有两个VPTRs(每个类A&B类)和3个整数(A,B,C)。 因此,大小应该是8(vptr的A)+ 4(INT A)+ 4(填充字节)+ 8(vptr的B)+ 4(INT B)+ 4(INT C)= 32个字节。 所以C的总大小为32个字节。



文章来源: Object layout in case of virtual functions and multiple inheritance