c++对象内存布局

测试环境Linux ubuntu 14.04 gcc 4.8.2

  • 单继承
class Base {
public:
    int base;
};
class Drived : public Base {
public: 
    int drived;
};
class Test : public Drived {
public:
    int test;
};

Test tobj;
tobj.base = 1;
tobj.drived = 2;
tobj.test = 3;

用gdb查看内存布局

说明父类的data member在子类对象开始的位置依次放入

  • 单继承+多态(虚函数)
class Base {
public:
    int base;
    virtual void Func1() {std::cout << "Base func1" << std::endl;};
    virtual void Func2() {std::cout << "Base func2" << std::endl;};
};

class Drived : public Base {
public: 
    int drived;
    virtual void Func2() {std::cout << "Drived func2" << std::endl;};
};

class Test : public Drived {
public:
    int test;
};

这里查看虚函数稍微麻烦一点,首先c++ standard并没有说明多态具体的实现方式,假设gcc是用虚函数表的方式,并且把指向虚函数表的指针放在了对象最开始的位置,那么虚表地址应该是*(int*)&tobj,这里我的机器的指针长度和int相等所以我转成了int*来获得虚表指针然后对其取地址,用gdb查看是

那么如果虚表按照虚函数声明顺序来排列则第一个应该是Base::Func1(),第二个是Drived::Func2()用gdb验证之

果然! 说明猜想成立,可以看到Drived类override了第二个虚函数,并且可以知道虚表中每个slot都是8bytes,我的测试机器的alignment是4,暂时不知道为啥非要一个slot占用8bytes。其实也可以在源码中打印出这些虚函数的地址来验证这一点,并调用之,只需要

typedef void(*func_pointer)(void);
func_pointer fp = (func_pointer)*((int*)*(int*)&tobj+0); // Func1()
fp();

所以其实多态就很好理解了,runtime才知道父类指针引用是哪个子类对象,才能那个子类对象的虚表,进而找到被编译器name mangling后的虚函数的地址并调用它。

  • 多继承
class Base {
public:
    int base;
};
class Drived {
public: 
    int drived;
};
class Test : public Drived, public Base{
public:
    int test;
};

用gdb查看内存布局

父类data member在子类对象开始的位置按照从左到右的继承顺序依次放入

  • 菱形虚继承
class A {
public:
    int a;
};

class X : virtual public A {
public:
    int x;
};

class Y : virtual public A {
public:
    int y;
};

class Z : public X, public Y {
public:
    int z;
};

略感复杂,首先测试了所有类的大小发现A X Y Z大小依次是4,16,16,40。然后我只知道类Z对象的内存布局从开始到最后是X,Y,Z自己的data member,最后是A的data member。其中X和Y的开始是一个指针,指向虚表,据各种资料和lippman在他书里都说这里虚表里存的是offset但是你妹的我验证了X的虚表里存的确实是offset,也就是Y的大小+padding(或者没有)+Z自己的data member的大小,但是我看Y的虚表里存的都是零啊啊啊啊啊 不解!

  • 多继承+多态

太复杂,好吧我直接贴结论吧
多重继承的情况,按照父类出现的顺序从左到右依次为每一个父类建一个虚函数表。子类的虚函数放到第一个虚函数表的后面。如果子类复写了其中某个父类的虚函数,就在子类中对应其父类的虚函数表中替换这个slot,并不会真正的覆盖掉父类的虚函数实现,所以其实通过BaseCalss::vFunc()还是可以调用父类的虚函数,但是这个调用和虚表无关就是直接函数调用而已。虚函数表的结束标志由平台和编译器决定。

小结

大多数编译器是利用虚函数表来实现多态的,其实虚函数表就是一个数组,该数组的元素就是函数指针,夹杂一些特殊标记,比如结束标记之类的。再深入估计只能具体去看某个编译器的源码了。