C++多态虚函数实现原理,对象和虚函数表的内存布局

基本概念

我们知道C++动态多态是用虚函数实现的,而虚函数的实现方式虽说C++标准没有要求,但是基本都是用虚函数表实现的(编译器决定)。所以我们有必要了解一下虚函数表的实现原理。

用virtual关键字声明的成员函数是虚函数。

具有虚函数的类及其派生的类会在编译时创建虚函数表,简称虚表(vtbl),虚表是虚函数指针的数组。

具有虚函数的类对象有一个虚表指针(vfptr),是编译器生成的指针,在对象构造时初始化。虚表指针vfptr指向虚表的第一个虚函数指针(即vfptr的值是虚表第一个虚函数指针的地址)。

打印对象布局或虚表布局

VS2017

在C/C++ -> 命令行里添加:/d1 reportSingleClassLayoutXXX(XXX是类名),重新编译会在输出窗口的生成里看到对象布局和虚表布局。

查看所有类布局

/d1 reportAllClassLayout 

 查看具体类布局

/d1 reportSingleClassLayoutXXX

gcc

查看虚表布局

g++ -fdump-class-hierarchy  Base.cpp

// >g++(8.3.1)
g++ -fdump-lang-class Base.cpp

然后会生成文件Base.cpp.002t.class,里面有虚表布局

gdb打印对象布局或虚表 

开启打印虚表
set print vtbl on

打印对象的虚表
i vtbl OBJECT

根据虚表打印对象的派生类
set p object on 

对象和虚表的内存布局

为了方便同时在windows和linux测试,统一使用x64编译器,数据类型使用intptr_t(与指针大小相同的整型)。

接下来介绍无继承有虚函数,单继承,多重继承,菱形继承,虚继承下的内存布局。

以下测试会在VS2017或gcc5.4.0进行。

无继承有虚函数

对象布局:第一个成员是虚表指针。

虚表布局:虚函数指针顺序即虚函数声明顺序。

class NoInhert_A {
public:
    NoInhert_A() { cout << "NoInhert_A::NoInhert_A()" << endl; }
    virtual ~NoInhert_A() { cout << "NoInhert_A::~NoInhert_A()" << endl; }
    virtual void f() { cout << "NoInhert_A::f()" << endl; }
    virtual void g() { cout << "NoInhert_A::g()" << endl; }
    intptr_t a = 1;
};

VS2017 

可以看到类NoInhert_A大小为16字节,虚表指针占8字节,变量a占8字节。虚表指针vfptr指向虚表的第一个虚函数指针,也就是析构函数。通过vfptr的偏移可以访问不同的虚函数指针。

gcc5.4.0

no_inhert.cpp.002t.class文件输出:

gdb输出:

可以看到对象布局和VS一样,都是16字节。

虚表指针vfptr指向虚表的第一个虚函数指针,接着是两个虚析构函数和两个虚函数。这是VS和gcc编译器的不同之处,gcc有两个析构函数指针,一个用在栈析构,一个用在堆析构。

虚表第一项是offset_to_top,表示该类虚表指针距离对象顶部地址的偏移量,这里是0,因为对象内存的第一项就是虚表指针,只有存在多重继承才不为0。

虚表的第二项是type_info,即RTTI指针,指向运行时类型信息,用于运行时类型识别,用于typeid和dynamic_cast。

整体内存布局如下:

单继承

单继承和多级继承比较简单,

对象布局:先是父类的成员,然后是子类的成员。

虚表布局:当子类重写虚函数时,父类的虚函数指针替换为子类的虚函数指针。

class SingleInhert_A {
public:
    SingleInhert_A() { cout << "SingleInhert_A::SingleInhert_A()" << endl; }
    virtual ~SingleInhert_A() { cout << "SingleInhert_A::~SingleInhert_A()" << endl; }
    virtual void f() { cout << "SingleInhert_A::f()" << endl; }
    virtual void g() { cout << "SingleInhert_A::g()" << endl; }
    intptr_t a = 1;
};

class SingleInhert_B : public SingleInhert_A {
public:
    SingleInhert_B() { cout << "SingleInhert_B::SingleInhert_B()" << endl; }
    virtual ~SingleInhert_B() { cout << "SingleInhert_B::~SingleInhert_B()" << endl; }
    virtual void f() override { cout << "SingleInhert_B::f()" << endl; }
    virtual void h() { cout << "SingleInhert_B::h()" << endl; }
    intptr_t b = 2;
};

int main() {
    SingleInhert_B obj;
    return 0;
}

 VS2017

gcc5.4.0

single_inhert.cpp.002t.class文件输出:

gdb输出:

 整体内存布局如下:

可以看到SingleInhert_B的虚表中,f函数被覆盖,g函数继承下来,h是新的虚函数。

多重继承

// 16字节
class Base1 {
public:
    Base1() { cout << "Base1::Base1()" << endl; }
    virtual ~Base1() { cout << "Base1::~Base1()" << endl; }
    virtual void f() { cout << "Base1::f()" << endl; }
    virtual void g() { cout << "Base1::g()" << endl; }
    intptr_t a = 1;
};

// 16字节
class Base2 {
public:
    Base2() { cout << "Base2::Base2()" << endl; }
    virtual ~Base2() { cout << "Base2::~Base2()" << endl; }
    virtual void f() { cout << "Base2::f()" << endl; }
    virtual void g() { cout << "Base2::g()" << endl; }
    virtual void h() { cout << "Base2::h()" << endl; }
    intptr_t b = 2;
};

// 40字节
class Derived : public Base1, public Base2 {
public:
    Derived() { cout << "Derived::Derived()" << endl; }
    virtual ~Derived() { cout << "Derived::~Derived()" << endl; }
    void f() override { cout << "Derived::f()" << endl; }
    void h() override { cout << "Derived::h()" << endl; }
    virtual void k() { cout << "Derived::k()" << endl; }
    intptr_t c = 3;
};

int main() {
    Derived d;
    return 0;
}

VS2017

/d1 reportSingleClassLayoutDerived

可以看到对象布局:先是父类Base1的成员,然后是Base2的成员,最后是子类Derived的成员。

注意这里继承2个父类所以子类有2个虚表,Derived和Base1共享虚表,对象有2个虚表指针。

整体内存布局如下:

可以看到子类Derived的f()和析构函数覆盖了Base1,&Base1::g和&Base2::g直接继承下来,Derived新增的 &Derived::k追加到Base1虚表的末尾。Derived和Base1共享第一个虚表。 

多重继承需要解决什么问题?

多重继承需要this指针调整。

考虑下面的情况:

Base2* pb2 = new Derived;
delete pb2;

Derived对象赋值给Base2指针时,this指针需要向后调整sizeof(Base),这样才能调用Base2的成员。

delete pb2时this指针要向前调整sizeof(Base),这样才能调用Derived的析构函数。

VS2017下多重继承总结

  • 子类的虚函数如果存在覆盖(override),会修改父类的虚表(替换为子类的虚函数指针)
  • 子类和第一个父类共享虚表,子类独有的虚函数指针追加到第一个父类的虚表后面
  • 有n个父类,则子类共有n个虚表

gcc5.4.0

multiple_inhert.cpp.002t.class输出:

 gdb输出:

和VS虚表实现不同,gcc下:

两个虚表似乎合并了,第二个虚表是通过偏移量得到的。为了方便下面介绍时还是说成两个虚表。

Base2被覆盖的虚函数指针&Derived::h被放在了第一个虚表里。当通过Base1或Derived的指针指向Derived对象时,调用h()函数不需要调整this指针,而通过Base2指针指向Derived对象时反而要调整this指针。这一点和《深度探索C++对象模型》多重继承例子里的mumble函数刚好相反,我个人猜测是gcc实现变了。

注意第二个虚表的offset_to_top是-16,即虚表指针向后调整16是对象起始地址,即this-=16。

所以gcc下Base2的虚函数除非没有被覆盖,否则都会走this指针调整。

菱形继承

/**
 * @brief 16字节
 * vptr_A
 * a
*/
class A {
public:
    A() { cout << "A::A()" << endl; }
    virtual ~A() { cout << "A::~A()" << endl; }
    virtual void f() { cout << "A::f()" << endl; }
    intptr_t a = 1;
};

/**
 * @brief 24字节
 * vptr_B
 * a
 * b
*/
class B : public A {
public:
    B() { cout << "B::B()" << endl; }
    virtual ~B() { cout << "B::~B()" << endl; }
    virtual void g() { cout << "B::g()" << endl; }
    intptr_t b = 2;
};

/**
 * @brief 24字节
 * vptr_C
 * a
 * c
*/
class C : public A {
public:
    C() { cout << "C::C()" << endl; }
    virtual ~C() { cout << "C::~C()" << endl; }
    virtual void h() { cout << "C::h()" << endl; }
    intptr_t c = 3;
};

/**
 * @brief 56字节
 * --------------------
 * vptr_B
 * a
 * b
 * --------------------
 * vptr_C
 * a
 * c
 * --------------------
 * d
*/
class Tom : public B, public C {
public:
    Tom() { cout << "Tom::Tom()" << endl; }
    virtual ~Tom() { cout << "Tom::~Tom()" << endl; }
    void f() override { cout << "Tom::f()" << endl; }
    virtual void k() { cout << "Tom::k()" << endl; }
    intptr_t d = 4;
};

int main() {
    Tom t;
    t.a = 2; // Tom::a不明确
    return 0;
}

菱形继承存在问题

  1. 公共父类的数据存在两份拷贝,造成浪费。(公共基类会构造和析构两次)
  2. 存在二义性,不能直接访问公共父类的数据和函数,需要通过类名::访问

解决方法:使用虚继承

VS2017

/d1 reportSingleClassLayoutTom

对象布局:

虚表布局:

 从上面对象布局可以看到A的数据成员a有两份。因为二义性我们也不能直接通过t.a访问a。

gcc5.4.0

diamond_inhert.cpp.002t.class输出:

gdb输出: 

菱形继承gcc逻辑跟VS差不多。

菱形继承+虚继承

/**
 * @brief 16字节
*/
class VA {
public:
    VA() { cout << "VA::VA()" << endl; }
    virtual ~VA() { cout << "VA::~VA()" << endl; }
    virtual void f() { cout << "VA::f()" << endl; }
    intptr_t a = 1;
};

/**
 * @brief VS2017:40字节,gcc5.4.0:32字节
*/
class VB : virtual public VA {
public:
    VB() { cout << "VB::VB()" << endl; }
    virtual ~VB() { cout << "VB::~VB()" << endl; }
    //void f() override { cout << "VB::f()" << endl; }
    virtual void g() { cout << "VB::g()" << endl; }
    intptr_t b = 2;
};

/**
 * @brief VS2017:40字节,gcc5.4.0:32字节
*/
class VC : virtual public VA {
public:
    VC() { cout << "VC::VC()" << endl; }
    virtual ~VC() { cout << "VC::~VC()" << endl; }
    virtual void h() { cout << "VC::h()" << endl; }
    intptr_t c = 3;
};

/**
 * @brief VS2017:80字节,gcc5.4.0:56字节
 * --------------------
*/
class VTom : public VB, public VC {
public:
    VTom() { cout << "VTom::VTom()" << endl; }
    virtual ~VTom() { cout << "VTom::~VTom()" << endl; }
    void f() override { cout << "VTom::f()" << endl; }
    virtual void k() { cout << "VTom::k()" << endl; }
    intptr_t d = 4;
};

int main() {
    VTom v;
    cout << sizeof(VA) << endl;
    cout << sizeof(VB) << endl;
    cout << sizeof(VC) << endl;
    cout << sizeof(VTom) << endl;
    return 0;
}

虚继承可以解决菱形继承的问题,菱形继承改为虚继承后,A只有一份拷贝。

深度探索C++对象模型里建议不要在虚基类声明非静态数据成员。

公共基类VA是虚基类。

只有第一个直接基类会调用虚基类的构造函数。

VS2017

/d1 reportSingleClassLayoutVTom

对象布局:

两个直接基类多了vbptr,即虚基类指针。vbptr指向虚基类表vbtable。

接着是子类成员,然后是4字节的vtordisp,为了保持8字节对齐,前面保留4字节。

最后才是虚基类的成员。

虚表布局:

可以看到子类新增的虚函数指针&VTom::k在第一个直接基类VB里。

再来看虚基类表:

  1. 虚基类表的第一项是类首地址相对vbptr的偏移量,这里都是-8。
  2. 虚基类表的第二项是虚基类的虚表指针相对vbptr的偏移量,即64-32=32,64-8=56。

gcc5.4.0

virtual_inhert.cpp.002t.class输出:

Vtable for VTom
VTom::_ZTV4VTom: 21u entries
0     40u
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI4VTom)
24    (int (*)(...))VTom::~VTom
32    (int (*)(...))VTom::~VTom
40    (int (*)(...))VB::g
48    (int (*)(...))VTom::f
56    (int (*)(...))VTom::k
64    24u
72    (int (*)(...))-16
80    (int (*)(...))(& _ZTI4VTom)
88    (int (*)(...))VTom::_ZThn16_N4VTomD1Ev
96    (int (*)(...))VTom::_ZThn16_N4VTomD0Ev
104   (int (*)(...))VC::h
112   18446744073709551576u
120   18446744073709551576u
128   (int (*)(...))-40
136   (int (*)(...))(& _ZTI4VTom)
144   (int (*)(...))VTom::_ZTv0_n24_N4VTomD1Ev
152   (int (*)(...))VTom::_ZTv0_n24_N4VTomD0Ev
160   (int (*)(...))VTom::_ZTv0_n32_N4VTom1fEv

Construction vtable for VB (0x0x7f648bdb1888 instance) in VTom
VTom::_ZTC4VTom0_2VB: 13u entries
0     40u
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI2VB)
24    0u
32    0u
40    (int (*)(...))VB::g
48    0u
56    18446744073709551576u
64    (int (*)(...))-40
72    (int (*)(...))(& _ZTI2VB)
80    0u
88    0u
96    (int (*)(...))VA::f

Construction vtable for VC (0x0x7f648bdb1820 instance) in VTom
VTom::_ZTC4VTom16_2VC: 13u entries
0     24u
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI2VC)
24    0u
32    0u
40    (int (*)(...))VC::h
48    0u
56    18446744073709551592u
64    (int (*)(...))-24
72    (int (*)(...))(& _ZTI2VC)
80    0u
88    0u
96    (int (*)(...))VA::f

VTT for VTom
VTom::_ZTT4VTom: 7u entries
0     ((& VTom::_ZTV4VTom) + 24u)
8     ((& VTom::_ZTC4VTom0_2VB) + 24u)
16    ((& VTom::_ZTC4VTom0_2VB) + 80u)
24    ((& VTom::_ZTC4VTom16_2VC) + 24u)
32    ((& VTom::_ZTC4VTom16_2VC) + 80u)
40    ((& VTom::_ZTV4VTom) + 144u)
48    ((& VTom::_ZTV4VTom) + 88u)

Class VTom
   size=56 align=8
   base size=40 base align=8
VTom (0x0x7f648be1dd90) 0
    vptridx=0u vptr=((& VTom::_ZTV4VTom) + 24u)
  VB (0x0x7f648bdb1888) 0
      primary-for VTom (0x0x7f648be1dd90)
      subvttidx=8u
    VA (0x0x7f648bd785a0) 40 virtual
        vptridx=40u vbaseoffset=-24 vptr=((& VTom::_ZTV4VTom) + 144u)
  VC (0x0x7f648bdb1820) 16
      subvttidx=24u vptridx=48u vptr=((& VTom::_ZTV4VTom) + 88u)
    VA (0x0x7f648bd785a0) alternative-path

 gdb输出:

虽然gdb输出VA的成员在最前面,但实际上的对象布局是这样的:

整体内存布局:

virtual base offsets表示虚基类虚表指针VA::vfptr相对于直接基类首地址的偏移量,即40和24。

可以看到整体上虚表布局显示类VB部分,接着是类VC部分,最后是虚基类VA部分。

虚基类部分都是使用trunk技术,即调整this指针来访问虚函数。

虚表开头是第一个直接基类VB的虚函数指针&VB::g,然后是覆盖虚基类的&VTom::f和子类新增的&VTom::k。

整体上虚表是一个连续的数组,但是逻辑上我们可以认为这里有3个虚表。

gcc下没有vbptr。

参考链接

C++虚函数的实现基本原理 - JackTang's Blog

面试系列之C++的对象布局【建议收藏】 - SegmentFault 思否

图说C++对象模型:对象内存布局详解 - melonstreet - 博客园

猜你喜欢

转载自blog.csdn.net/ET_Endeavoring/article/details/124830482