C++对象内存模型详解(基于GDB)

因为挺多内容的来自别人博客的学习,所以把相关博客的链接拉上:
虚函数解析:http://blog.csdn.net/haoel/article/details/1948051
C++ 对象的内存布局(上):http://blog.csdn.net/haoel/article/details/3081328
C++对象的内存布局(下):http://blog.csdn.net/haoel/article/details/3081385
在这篇博客里面,我们将致力于利用GDB工具来分析以下几种情况的对象内存模型:
1.对象的虚函数表是怎么回事?
2.对象的虚函数是怎么分布的?
3.函数继承的虚函数表是怎么样的?
4.子类复写了父类的函数的结果虚函数表会怎样构成?
5.有成员变量的情况应该怎么处理?

在开始之前,我想先说几个基本概念,这几个基本概念可以帮助我们快速理解对象的内存是如何分配的?
1.C++的函数对象构造过程是从基类开始构造,然后在构造子类。就像搭房子一样,先盖基底然后再盖上层建筑。那么,析构的顺序也是相同的,先调用子类的析构函数,然后再调用基类的析构函数,就跟拆房子先从上层开始拆。
2.我们为何要使用虚函数、虚类,虚继承的目的。从目的出发,我们使用虚函数的最大目的是为了多态,多态的目的就是基类给你指定函数的接口形式,然后子类再去overwrite,这就必然会涉及到函数的覆盖。覆盖的目的就是换一个新的函数地址,这就需要一个虚函数表的数据结构。那么,反过来说。如果不是虚函数,那么我们子类的目的就是可以使用基类的函数,这就不需要虚函数表这个数据结构,而是正常的在this指针里面指向的空间并且调用。
3.顺序问题:虚基类(虚继承)放在this指针指向的空间的最后部分。所以,我们想要访问共有的基类元素就可以经过访问虚基类指针访问虚基类表中的偏移量,通过偏移量来获得我们的基类共有元素。
4.本类的虚函数存储在第一个虚函数表的最后一个。
5.虚函数表的目的是为了重载,之前一直犯了个错误。认为对象的函数都需要一个函数指针来指向。但是实际上,类似C语言的函数声明一样,非虚函数都已经通过函数声明声明好了。

好了,有了这些元素,我们就可以进行分析了:
首先,我们先来分析最简单的例子,不考虑虚继承,但是有非虚函数:

class Vertex3d
{
public:
    void Foo(){printf("Vertex3d::Foo() called !\n");}
};


class MY:public Vertex3d
{
public:
    void print(){printf("Hello,world!!");}
};

int main()
{
    printf("%lu\n",sizeof(MY));
    return 0;
}

结果是1。
只有一个字节,可见对象里面的函数并不会占据空间。
其次,我们想再难一点,这个时候,我们讨论一下当虚函数加入其中的情况:

#include<iostream>
using namespace std;
class Base {
     public:
            virtual void f() { cout << "Base::f" << endl; }
            virtual void g() { cout << "Base::g" << endl; }
            virtual void h() { cout << "Base::h" << endl; }
};


class MY:public Base
{
public:
    void test(){}
};



int main()
{
    printf("%lu\n",sizeof(MY));
    MY my;
    printf("%lu\n",sizeof(my));
    return 0;
}

考虑,这样一个含有虚函数的继承关系的类,我们现在在看看这个MY的size是多少:

8
8

结果是8个字节,我们没有生成任何新的数据类型,这个8个字节明显是一个指针类型(64位操作系统下的指针类型是64位),我们再用我们的gdb认证一下我们的观点:
这里写图片描述
可见我们的8个字节明显是一个_vptr$Base的指针,指向我们的虚函数表,虚函数表中的函数分别是我们基类的函数:Base::f()、Base::g()、Base::h()
这里借用一个图来加深理解:
这里写图片描述
来自:http://blog.csdn.net/haoel/article/details/1948051
三. 我们设计到了虚函数,却没有涉及到我们的重载。我们知道我们虚函数的目的就是为了多态,我们再改复杂一点,子类加入对基类的虚函数的Overwrite,然后子类和父类都加入自己的变量,子类也有自己的虚函数:

#include<iostream>
using namespace std;
class Base {
     public:
            virtual void f() { cout << "Base::f" << endl; }
            virtual void g() { cout << "Base::g" << endl; }
            virtual void h() { cout << "Base::h" << endl; }
    int ba;
};
class MY:public Base
{
public:
    void f() {cout<<"MY::f"<<endl;}
    virtual void k() {cout<<"MY::k"<<endl;}
    int bm;
};
int main()
{
    printf("%lu\n",sizeof(MY));
    MY my;
    printf("%lu\n",sizeof(my));
    return 0;
}

一样,看一下我们的size大小:

16
16

我们猜测是一个虚函数表,两个int变量。
一样的,我们通过gdb加以验证:
这里写图片描述
依旧可以看到我们的虚基类表指针和两个变量ba,bm,看来我们的16个字节是由虚基类指针和两个变量,然后根据我们的原则1,3,5顺序是由基类的元素先开始,然后再到子类。
再来看看我们虚基类表,我们子类在后面调用MY::f()覆盖了基类,这就实现虚函数的多态,之前我们如果将子类对象转化为我们的基类,我们所使用的就是基类的成员函数。
根据原则5可知,我们子类自己的函数MY::k()位于第一个虚函数表的最后。
这里还是上一个图,加深理解(别人的):
这里写图片描述

四.我们继续深入,这次我们考虑多继承中的虚函数的情况:
还是先上我们的例子:

#include<iostream>
using namespace std;
class Base1 {
public:
        virtual void f() { cout << "Base1::f" << endl; }
        virtual void g() { cout << "Base1::g" << endl; }
        virtual void h() { cout << "Base1::h" << endl; }
        int num1;
};


class Base2 {
public:
        virtual void f() { cout << "Base2::f" << endl; }
        virtual void g() { cout << "Base2::g" << endl; }
        virtual void h() { cout << "Base2::h" << endl; }
        int num2;
};


class Base3 {
public:
        virtual void f() { cout << "Base3::f" << endl; }
        virtual void g() { cout << "Base3::g" << endl; }
        virtual void h() { cout << "Base3::h" << endl; }
        int num3;
};


class MY:public Base1,public Base2,public Base3
{
public:
    void f() {cout<<"MY::f"<<endl;}
    virtual void k() {cout<<"MY::k"<<endl;}
    int bm;
};
int main()
{
    printf("%lu\n",sizeof(MY));
    MY my;
    printf("%lu\n",sizeof(my));
    return 0;
}

看下当我们有三重继承的时候,内存占用的情况如何:

48
48

48按照我的初步猜想,3个8字节的指针为24个字节,4个int型的16个字节,一共40个字节,似乎还缺了点什么,我们再使用GDB调试看看:

这里写图片描述
似乎多了些什么我们不清楚的东西,并且我们进行判断数组的[2]或者[3]的时候,感觉中间多了一些我们不知道的东西,我决定把结果函数的相关内容打印出来看一下:
简单上一些打印代码(原理就是将this指针转化为二维数组,然后分别访问这个二维数组,将指针进行转化为函数指针):

typedef void(*Fun)(void);


int main()
{
            Fun pFun = NULL;


            Derive d;
            size_t** pVtab = (size_t**)&d;


            //Base1's vtable

            //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);
            pFun = (Fun)pVtab[0][0];
            pFun();
            //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);
            pFun = (Fun)pVtab[0][1];
            pFun();
            //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);
            pFun = (Fun)pVtab[0][2];
            pFun();
            //Derive's vtable
            //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);
            pFun = (Fun)pVtab[0][3];
            pFun();
            //The tail of the vtable
            pFun = (Fun)pVtab[0][4];
            cout<<pFun<<endl;
            //Base2's vtable
            //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
            pFun = (Fun)pVtab[1][0];
            pFun();
            //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
            pFun = (Fun)pVtab[1][1];
            pFun();
            pFun = (Fun)pVtab[1][2];
            pFun();
            //The tail of the vtable
            pFun = (Fun)pVtab[1][3];
            cout<<pFun<<endl;
            //Base3's vtable

            //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
            pFun = (Fun)pVtab[2][0];
            pFun();


            //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
            pFun = (Fun)pVtab[2][1];
            pFun();


            pFun = (Fun)pVtab[2][2];
            pFun();


            //The tail of the vtable
            pFun = (Fun)pVtab[2][3];
            cout<<pFun<<endl;

结果如下:

Y::f
Base1::g
Base1::h
MY::k
1
MY::f
Base2::g
Base2::h
1
MY::f
Base3::g
Base3::h
1
32
32

看来跟我们的基本假设相同,只是在GDB里面多了一些调试信息,可以看出的信息的如下:
1.多重继承的虚函数,有多个虚函数表,分别存储着不同类的虚函数信息。
2.本类中的相关虚函数会overwrite基类的虚函数。
3.本类中新的虚函数在在第一个虚函数表中,后面的虚函数表并没有。
4.在G++环境下,虚函数表的最后一个字节的信息为1.
再来一张别人的图啦:
这里写图片描述
这里写图片描述

五。最后,我们讨论以下虚继承的情况
这里参考了这个博客总结的相关信息:
http://blog.csdn.net/xiejingfa/article/details/48028491


class MY: virtual public Base1,virtual public Base2,public Base3
{
public:
    void f() {cout<<"MY::f"<<endl;}
    virtual void k() {cout<<"MY::k"<<endl;}
//    int bm;
};

我们把前两个类改成虚基类
先看大小

32
32

没变。
接着往GDB下面看
这里写图片描述
不知道大家有没看出,这个时候Base3(非虚继承)成了离MY最近的的基类,Base1,Base2(虚继承)的放在最后,离my最远,符合原则4.
那有怎么虚继承的公共元素呢?
大家看到MY+104,MY+168,这些104,168偏离量。对了,我们的虚继承之所以能在多重派生的结果上只有一个虚基类的拷贝,就是靠的这个偏移量。我们的虚基类永远在内存空间的最后。然后,我们前面的派生类的虚基类表里面存储着偏离量。通过,这些偏离量可以快速的找到我们的公共虚基类了。

猜你喜欢

转载自blog.csdn.net/github_33873969/article/details/79594290