关于C++对象的内存模型,由于各家编译器不一样导致的结果也不尽相同,所以以下测试都是基于VS 2017为准。其指针大小为4个字节,为了避免对齐带来的干扰,所有成员变量都为int类型。

1、非继承下的对象模型

  首先是最为简单情况下的C++对象的内存模型,即不考虑任何继承等情况。测试代码如下:

class Point2d {
public:
    Point2d(int x_, int y_) : x(x_), y(y_) {}
    virtual ~Point2d() {}

    virtual void draw() {
        std::cout << "Point2d::draw()" << std::endl;
    }
    virtual void draw2d() {
        std::cout << "Point2d::draw2d()" << std::endl;
    }

private:
    int x, y;
public:
    static int var;
};

 

  利用VS查看Point2d的对象的内存布局可以得到下图:    

  

  由此可见,在VS中,非继承的模型下,将虚指针放在了第一个元素。其余元素放在之后。

2、考虑一般继承情况

  如果考虑一般继承情况(相对于虚继承)的话,则需要分为三种,单继承和多继承,以及棱形继承。

2.1:单继承

  在上一步的代码基础上加上如下测试代码:

class Point3d : public Point2d {
public:
    Point3d(int x_, int y_, int z_) : Point2d(x_, y_), z(z_) {}
    virtual ~Point3d() {}

    virtual void draw() {
        std::cout << "Point3d::draw()" << std::endl;
    }
    virtual void draw3d() {
        std::cout << "Point3d::draw3d()" << std::endl;
    }

private:
    int z;
};

  查看内存布局:

  

  对于对象本身的内存布局来说,先是基类部分,然后才是本类的部分。其中虚指针还是在第一个位置。

  虚函数表部分发生了较大变化

  • 子类如果重写了父类的虚函数,则虚表中会只保存子类的版本。--draw() & dtor()
  • 子类如果新添加了自己的虚函数,则在上面的虚表的基础上会在后面加上一个slot来保存。--draw3d()
  • 如果子类没有重写父类的虚函数,则原有的虚函数在表中保留。--draw2d()

2.2:多继承

  测试代码如下:

class Base1
{
public:
    Base1(int x_) : x(x_) {}
    virtual ~Base1() {}    

    virtual void base1_func() { std::cout << "Base1::base1_func" << std::endl; } 
    virtual void func() { std::cout << "func" << std::endl; }    

private:
    int x;
};

class Base2
{
public:
    Base2(int y_) : y(y_) {};
    virtual ~Base2() {};

    virtual void base2_func() { std::cout << "Base1::base2_func" << std::endl; }
    virtual void func() { std::cout << "func" << std::endl; }

private:
    int y;
};

class Derived : public Base1, public Base2 {
public:
    Derived(int x_, int y_, int z_) : Base1(x_), Base2(y_), z(z_) {}
    virtual ~Derived() {}

    virtual void derived_func() { std::cout << "Derived::derived_func" << std::endl; }
    virtual void base1_func() { std::cout << "Derived::base1_func" << std::endl; }
    virtual void base2_func() { std::cout << "Derived::base2_func" << std::endl; }

private:
    int z;
};

  查看内存布局如下图所示:

  

  从对象本身的内存布局来看,其中按照基类的声明顺序,依次包含了两个基类的部分。每个基类部分都有一个虚指针指向各自虚表。也就是说,在这种情况下,一个对象可能含有多个虚指针指向不同的虚表。通常来讲,排在对象内存最前面的基类部分所包含的虚指针指向的是“虚函数主表”。所以有如下几个规律:

  • 本类自身所添加的新虚函数的地址会添加在“虚函数主表”的后面。--derived_func()
  • 如果重写了基类的虚函数,则主虚函数表中只会保存重写后的版本,其余虚函数表会通过thunk机制跳转到主虚函数表中去。
  • 如果基类的虚函数没有重写,则会原样保留下来。--Base2::func()等
  • 注意到第二个虚函数表最前面有一个thunk,这是为了调用如下两句语句 Base2* pb = new Derived(); delete pb; 能够正确调用到Derived::dtor。其实本质上是一段Assembly代码。

2.3:棱形继承

  测试函数如下:

class Base
{
public:
    Base() {}
    virtual ~Base() {}

    virtual void overwrite_func() {
        std::cout << "Base::overwrite_func" << std::endl;
    }
    virtual void Base_func() {
        std::cout << "Base::base_func" << std::endl;
    }

private:
    int x;
};

class Base1: public Base
{
public:
    Base1(){}
    virtual ~Base1() {}    

    virtual void overwrite_func() {
        std::cout << "Base1::overwrite_func" << std::endl;
    }
    virtual void base1_func() {
        std::cout << "Base1::base1_func" << std::endl;
    }
};

class Base2 : public Base
{
public:
    Base2() {}
    virtual ~Base2() {}

    virtual void overwrite_func() {
        std::cout << "Base2::overwrite_func" << std::endl;
    }
    virtual void base2_func() {
        std::cout << "Base2::base2_func" << std::endl;
    }
};

class Derived : public Base1, public Base2 {
public:
    Derived() {}
    virtual ~Derived() {}

    virtual void overwrite_func() { std::cout << "Derived::overwrite()" << std::endl; }
    virtual void derived_func() { std::cout << "Derived::derived_func" << std::endl; }

private:
    int z;
};

  查看内存布局如下:

  

  可见内存布局与多继承并无明显差别,可以先按照单继承规则来安排Base1类和Base2类的布局,之后再按照多继承规则安排Derived类对象内存的布局。需要注意的是最终Derived对象的内存模型中会包含两个Base祖父基类的部分。在这种情况下,如果想要使用祖父基类中的成员x就必须这么写:derived_obj.Base1::x,或者derived_obj.Base2::x。否则会引发歧义。

3、考虑虚拟继承

3.1:单继承

  测试代码如下:

class Base1
{
public:
    Base1(){}
    virtual ~Base1() {}    

    virtual void overwrite_func() {
        std::cout << "Base1::overwrite_func" << std::endl;
    }
    virtual void base1_func() {
        std::cout << "Base1::base1_func" << std::endl;
    }

private:
    int x;
};

class Derived : virtual public Base1 {
public:
    Derived() {}
    virtual ~Derived() {}

    virtual void overwrite_func() { std::cout << "Derived::overwrite()" << std::endl; }
    virtual void derived_func() { std::cout << "Derived::derived_func" << std::endl; }

private:
    int z;
};

  观察内存布局:

  

  可以看出此时对象的内存布局大概分为三个部分,由上到下分别为:

  • 本类部分,包含一个虚指针和紧接后面的虚基类指针和成员变量,虚指针指向的虚函数表只存放本类中新添加的虚函数地址。虚基类指针指向的是虚基类表,表中第一项存放的是虚基类指针和对象起始地址的偏移量,接下来各项分别是从左到右各个基类部分相对于对象起始地址的偏移量。
  • 4个字节的空白部分,用于分割第一部分和第三部分。
  • 第三部分是基类部分,首先是虚指针,这个虚指针指向的虚表存放着继承下来和改写过后的虚函数地址。之后是基类的成员变量。

  所以由上看出,此时子类有着两张表,子类内存部分虚指针指向的那张表存放新添加的虚函数地址,基类部分虚指针指向的那张表存放的是继承下来和改写过后的虚函数的地址。

3.2:多继承

  测试代码如下:

class Base1
{
public:
    Base1(){}
    virtual ~Base1() {}    

    virtual void overwrite_func() {
        std::cout << "Base1::overwrite_func" << std::endl;
    }
    virtual void base1_func() {
        std::cout << "Base1::base1_func" << std::endl;
    }

private:
    int x;
};

class Base2 
{
public:
    Base2() {}
    virtual ~Base2() {}

    virtual void overwrite_func() {
        std::cout << "Base2::overwrite_func" << std::endl;
    }
    virtual void base2_func() {
        std::cout << "Base2::base2_func" << std::endl;
    }

private:
    int y;
};

class Derived : virtual public Base1, virtual public Base2 {
public:
    Derived() {}
    virtual ~Derived() {}

    virtual void overwrite_func() { std::cout << "Derived::overwrite()" << std::endl; }
    virtual void derived_func() { std::cout << "Derived::derived_func" << std::endl; }

private:
    int z;
};

  观察内存布局:

  

  可见此时内存布局情况和单虚拟继承下的差不了多少

  • 首先是本类的部分,包含虚指针和虚基类指针以及成员变量,本类部分中的虚指针指向的虚函数表存放着新添加的虚函数的地址。
  • 之后是按照声明顺序的两个基类的部分,第一个基类中虚指针指向的虚函数表是主表(存放重写过后的虚函数的地址以及从第一个基类继承下来的虚函数的地址),其它的虚函数表中只存放从对应的基类继承下来的没有改写的虚函数的地址,如果要调用重写过后的虚函数的话会利用thunk机制跳转到主表中去。
  • 各个部分之间以4个字节的全为0的项分隔。

3.3:棱形继承

  测试代码如下:

  

class Base
{
public:
    Base() {}
    virtual ~Base() {}

    virtual void overwrite_func() {
        std::cout << "Base::overwrite_func" << std::endl;
    }
    virtual void Base_func() {
        std::cout << "Base::base_func" << std::endl;
    }

private:
    int x;
};

class Base1 : virtual public Base 
{
public:
    Base1(){}
    virtual ~Base1() {}    

    virtual void overwrite_func() {
        std::cout << "Base1::overwrite_func" << std::endl;
    }
    virtual void base1_func() {
        std::cout << "Base1::base1_func" << std::endl;
    }

private:
    int z;
};

class Base2 : virtual public Base
{
public:
    Base2() {}
    virtual ~Base2() {}

    virtual void overwrite_func() {
        std::cout << "Base2::overwrite_func" << std::endl;
    }
    virtual void base2_func() {
        std::cout << "Base2::base2_func" << std::endl;
    }

private:
    int y;
};

class Derived : public Base1, public Base2 {
public:
    Derived() {}
    virtual ~Derived() {}

    virtual void overwrite_func() { std::cout << "Derived::overwrite()" << std::endl; }
    virtual void derived_func() { std::cout << "Derived::derived_func" << std::endl; }

private:
    int a;
};

  观察内存布局:

  

  

  由上面的结果可以得出以下的一些信息:

  • 把基类部分放在最前面,最左边的基类最先安排。
  • 祖父类放在最后面,在祖父类部分前面的是本类的数据成员。
  • 基类部分都有各自的虚指针和虚基类指针,祖父类也有自己的虚指针。
  • 本类部分没有虚指针,而是占用了最左边的基类的虚指针所指向的虚函数表,并将之扩充以用来保存本类新添加的虚函数的地址。
  • 重写的虚函数的地址保存在祖父类部分的虚指针指向的虚函数表中。未重写的虚函数的地址各自保留在各自基类/祖父类对应的虚函数表中。

总结:

  从新添加的虚函数,重写的虚函数,继承下来的虚函数的存放位置方面,来总结下VS 2017下C++对象内存模型的规律:

  1. 不考虑继承情况下:
    1. 总是将虚指针安排在对象的起始地址处。
    2. 紧接着即为非静态成员变量。
  2. 考虑一般性的继承情况:
    1. 单继承:首先安排基类对象,紧接着安排本类的部分。并直接扩展基类部分的函数表。
    2. 多继承:按照继承时候声明的顺序依次声明的顺序安排基类部分。此时会有多个虚函数表,将重写后的虚函数地址以及新添加的虚函数地址都放在最左基类部分的虚表中。未重写的还是放在自己所属基类部分对应的虚表中。
    3. 菱形继承:可首先按照单继承规律和祖父类对象模型安排父类对象模型,再按照普通多继承安排本类对象模型。
  3. 考虑虚拟继承的情况:
    1. 虚拟单继承:先安排本类部分,本类部分有自己的虚函数表用来存放本类新添加的虚函数地址。重写/继承下来的虚函数地址保存再基类部分对应的虚函数表中。
    2. 虚拟多继承:本类部分会有自己的虚函数表来存储新添加的虚函数地址。之后最左基类对应的虚函数表存储重写的虚函数地址,继承下来的虚函数保留在各自对应基类的虚函数表中。
    3. 菱形虚拟继承:首先会安排基类部分,最左基类的虚函数表会保存新添加的虚函数的地址。接下来是本类的数据部分,最后才是超类部分,其中超类部分中对应的虚函数表中保存的是重写过后的虚函数的地址。继承下来的虚函数的地址还是保存在各个基类对应的虚函数表中。

 

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!