码迷,mamicode.com
首页 > 编程语言 > 详细

C++对象的内存模型(笔记)

时间:2017-10-07 17:45:34      阅读:232      评论:0      收藏:0      [点我收藏+]

标签:不同的   偏移量   观察   模型   子类   ati   2.3   point   多继承   

 

 

 

关于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;
};

  观察内存布局:

  技术分享

  技术分享

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

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

 

C++对象的内存模型(笔记)

标签:不同的   偏移量   观察   模型   子类   ati   2.3   point   多继承   

原文地址:http://www.cnblogs.com/linfan255/p/7634866.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!