c++对象内存布局示例详解

目录
  • 前言
  • 继承对象的内存布局
  • 具有多重继承和虚拟功能的对象的内存布局
  • 总结

前言

了解你所使用的编程语言究竟是如何实现的,对于C++程序员可能特别有意义。首先,它可以去除我们对于所使用语言的神秘感,使我们不至于对于编译器干的活感到完全不可思议;尤其重要的是,它使我们在Debug和使用语言高级特性的时候,有更多的把握。当需要提高代码效率的时候,这些知识也能够很好地帮助我们。

简单非多态的内存布局

class X {
    int     x;
    float   xx;

public:
    X() {}
    ~X() {}

    void printInt() {}
    void printFloat() {}
};
      |                        |
      |------------------------| <------ X class object memory layout
      |        int X::x        |
      |------------------------|  stack segment
      |       float X::xx      |       |
      |------------------------|       |
      |                        |      \|/
      |                        |
      |                        |
------|------------------------|----------------
      |         X::X()         |
      |------------------------|       |
      |        X::~X()         |       |
      |------------------------|      \|/
      |      X::printInt()     |  text segment
      |------------------------|
      |     X::printFloat()    |
      |------------------------|
      |                        |

在本示例中

  • 只有数据成员存储在堆栈中,且其声明顺序或者存储顺序的行为与编译器强相关
  • 所有其他方法(构造函数,析构函数和编译器扩展代码)都进存储在文本段。然后,这些方法将被调用并隐式地在调用对象的第一个参数中传递该指针。

this指针是一个隐含于每一个成员函数中的特殊指针。它是一个指向正在被该成员函数操作的对象,也就是要操作该成员函数的对象。this作用域是在类内部,当对一个对象调用成员函数时,编译程序先将对象的地址赋给this指针,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参。被调用的成员函数函数体内所有对类成员的访问,都会被转化为“this->类成员”的方式。

针对第二点,我们类似于:

A x;
x.printInt();

其中,X::printInt()这个行为,在编译器中,将处理为

printInt(const X* this)

那么,x.printInt()调用处理将最终成为

printInt(&x);

同时具有虚函数和静态数据成员的内存布局

class X {
    int         x;
    float       xx;
    static int  count;

public:
    X() {}
    virtual ~X() {}

    virtual void printAll() {}
    void printInt() {}
    void printFloat() {}
    static void printCount() {}
};

其内存布局如下

      |                        |
      |------------------------| <------ X class object memory layout
      |        int X::x        |
stack |------------------------|
  |   |       float X::xx      |
  |   |------------------------|      |-------|--------------------------|
  |   |         X::_vptr       |------|       |       type_info X        |
 \|/  |------------------------|              |--------------------------|
      |           o            |              |    address of X::~X()    |
      |           o            |              |--------------------------|
      |           o            |              | address of X::printAll() |
      |                        |              |--------------------------|
      |                        |
------|------------------------|------------
      |  static int X::count   |      /|\
      |------------------------|       |
      |           o            |  data segment
      |           o            |       |
      |                        |      \|/
------|------------------------|------------
      |        X::X()          |
      |------------------------|       |
      |        X::~X()         |       |
      |------------------------|       |
      |      X::printAll()     |      \|/
      |------------------------|  text segment
      |      X::printInt()     |
      |------------------------|
      |     X::printFloat()    |
      |------------------------|
      | static X::printCount() |
      |------------------------|
      |                        |
  • 所有非静态数据成员都按照声明的顺序将空间放入堆栈中,与前面的示例顺序相同。
  • 静态数据成员将空间放入内存的数据段中。使用范围解析运算符(即::)进行的访问。但是在编译之后,就没有像作用域和名称空间那样的东西了。因为,它的名称只是由编译器执行,所以所有内容都由其绝对或相对地址引用。
  • 静态数据成员将空间放入内存的数据段中。使用范围解析运算符(即::)进行的访问。
  • 静态方法进入文本段,并通过作用域解析运算符进行调用。
  • 对于virtual关键字,编译器会自动将指向虚拟表的指针(vptr)插入对象内存表示中。通常,虚拟表是在数据段中为每个类静态创建的,但它也取决于编译器的实现。
  • 在虚拟表中,第一个条目指向type_info对象,该对象包含与当前基类和其他基类的DAG(有向无环图)相关的信息(如果从这些基类派生的信息)。

继承对象的内存布局

class X {
    int     x;
    string str;

public:
    X() {}
    virtual ~X() {}

    virtual void printAll() {}
};

class Y : public X {
    int     y;

public:
    Y() {}
    ~Y() {}

    void printAll() {}
};

其内存布局信息如下

      |                              |
      |------------------------------| <------ Y class object memory layout
      |          int X::x            |
stack |------------------------------|
  |   |              int string::len |
  |   |string X::str ----------------|
  |   |            char* string::str |
 \|/  |------------------------------|      |-------|--------------------------|
      |           X::_vptr           |------|       |       type_info Y        |
      |------------------------------|              |--------------------------|
      |          int Y::y            |              |    address of Y::~Y()    |
      |------------------------------|              |--------------------------|
      |               o              |              | address of Y::printAll() |
      |               o              |              |--------------------------|
      |               o              |
------|------------------------------|--------
      |           X::X()             |
      |------------------------------|       |
      |           X::~X()            |       |
      |------------------------------|       |
      |         X::printAll()        |      \|/
      |------------------------------|  text segment
      |           Y::Y()             |
      |------------------------------|
      |           Y::~Y()            |
      |------------------------------|
      |         Y::printAll()        |
      |------------------------------|
      |      string::string()        |
      |------------------------------|
      |      string::~string()       |
      |------------------------------|
      |      string::length()        |
      |------------------------------|
      |               o              |
      |               o              |
      |               o              |
      |                              |
  • 在继承模型中,基类和数据成员类是派生类的子对象。
  • 编译器会在类的构造函数中生成具有所有重写的虚拟功能和为_vptr分配虚拟表的代码的虚拟表。

具有多重继承和虚拟功能的对象的内存布局

class X {
public:
    int     x;

    virtual ~X() {}
    virtual void printX() {}
};

class Y {
public:
    int     y;

    virtual ~Y() {}
    virtual void printY() {}
};

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

    ~Z() {}
    void printX() {}
    void printY() {}
    void printZ() {}
};

内存布局如下

      |                              |
      |------------------------------| <------ Z class object memory layout
stack |          int X::x            |
  |   |------------------------------|                  |--------------------------|
  |   |          X:: _vptr           |----------------->|       type_info Z        |
  |   |------------------------------|                  |--------------------------|
 \|/  |          int Y::y            |                  |    address of Z::~Z()    |
      |------------------------------|                  |--------------------------|
      |          Y:: _vptr           |------|           |   address of Z::printX() |
      |------------------------------|      |           |--------------------------|
      |          int Z::z            |      |           |--------GUARD_AREA--------|
      |------------------------------|      |           |--------------------------|
      |              o               |      |---------->|       type_info Z        |
      |              o               |                  |--------------------------|
      |              o               |                  |    address of Z::~Z()    |
      |                              |                  |--------------------------|
------|------------------------------|---------         |   address of Z::printY() |
      |           X::~X()            |       |          |--------------------------|
      |------------------------------|       |
      |          X::printX()         |       |
      |------------------------------|       |
      |           Y::~Y()            |      \|/
      |------------------------------|  text segment
      |          Y::printY()         |
      |------------------------------|
      |           Z::~Z()            |
      |------------------------------|
      |          Z::printX()         |
      |------------------------------|
      |          Z::printY()         |
      |------------------------------|
      |          Z::printZ()         |
      |------------------------------|
      |               o              |
      |               o              |
      |                              |               

在多继承层次结构中,创建的虚拟表指针(vptr)的确切数目将为N-1,其中N代表类的数目。

如果尝试使用任何基类指针调用Z类的方法,则它将使用相应的虚拟表进行调用。如下例子所示:

Y *y_ptr = new Z;
y_ptr->printY(); // OK
y_ptr->printZ(); // Not OK, as virtual table of class Y doesn't have address of printZ() method

在上面的代码中,y_ptr将指向完整Z对象内类Y的子对象。

结果,调用任何方法,例如使用y_ptr-> printY()。 使用y_ptr的解析方式如下:

 ( *y_ptr->_vtbl[ 2 ] )( y_ptr )

虚继承内存布局

class X { int x; };
class Y : public virtual X { int y; };
class Z : public virtual X { int z; };
class A : public Y, public Z { int a; };

其布局如下:

                  |                |
 Y class  ------> |----------------| <------ A class object memory layout
sub-object        |   Y::y         |
                  |----------------|             |------------------|
                  |   Y::_vptr_Y   |------|      |    offset of X   | // offset(20) starts from Y
 Z class  ------> |----------------|      |----> |------------------|
sub-object        |   Z::z         |             |       .....      |
                  |----------------|             |------------------|
                  |   Z::_vptr_Z   |------|
                  |----------------|      |
 A sub-object --> |   A::a         |      |      |------------------|
                  |----------------|      |      |    offset of X   | // offset(12) starts from Z
 X class -------> |   X::x         |      |----> |------------------|
 shared           |----------------|             |       .....      |
 sub-object       |                |             |------------------|
  • 具有一个或多个虚拟基类的派生类的内存表示形式分为两个区域:不变区域和共享区域。
  • 不变区域内的数据与对象的起始位置保持固定的偏移量,而与后续派生无关。
  • 共享区域包含虚拟基类,并且随后续派生和派生顺序而波动。

总结

了解内存布局,对我们的项目开发会提供很大的便利,比如对coredump的调试

到此这篇关于c++对象内存布局的文章就介绍到这了,更多相关c++对象内存布局内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 关于C++对象继承中的内存布局示例详解

    前言 本文给大家介绍的是关于C++对象继承的内存布局的相关内容,分享出来供大家参考学习,在开始之前说明下,关于单继承和多继承的简单概念可参考此文章 以下编译环境均为:WIN32+VS2015 虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的.简称为V-Table.在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承.覆盖的问题,保证其容真实反应实际的函数. 首先先通过一个例子来引入虚函数表,假如现在有三

  • 详谈C++中虚基类在派生类中的内存布局

    今天重温C++的知识,当看到虚基类这点的时候,那时候也没有太过追究,就是知道虚基类是消除了类继承之间的二义性问题而已,可是很是好奇,它是怎么消除的,内存布局是怎么分配的呢?于是就深入研究了一下,具体的原理如下所示: 在C++中,obj是一个类的对象,p是指向obj的指针,该类里面有个数据成员mem,请问obj.mem和p->mem在实现和效率上有什么不同. 答案是:只有一种情况下才有重大差异,该情况必须满足以下3个条件: (1).obj 是一个虚拟继承的派生类的对象 (2).mem是从虚拟基类派

  • 深入解析C++ Data Member内存布局

    如果一个类只定义了类名,没定义任何方法和字段,如class A{};那么class A的每个实例占用1个字节的内存,编译器会会在这个其实例中安插一个char,以保证每个A实例在内存中有唯一的地址,如A a,b;&a!=&b.如果一个直接或是间接的继承(不是虚继承)了多个类,如果这个类及其父类像A一样没有方法没有字段,那么这个类的每个实例的大小都是1字节,如果有虚继承,那就不是1字节了,每虚继承一个类,这个类的实例就会多一个指向被虚继承父类的指针.还有一点值得说明的就是像A这样的类,编译器不

  • 浅谈C++中派生类对象的内存布局

    主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性. 派生类可以看作是完整的基类的Object再加上派生类自己的Object.如果基类中没有虚成员函数,那么派生类与具有相同功能的非派生类将不带来任何性能上的差异.另外,一定要保证基类的完整性.实际内存布局由编译器自己决定,VS里,把虚指针放在最前边,接着是基类的Object,最后是派生类自己的object.举个栗子: class A { int b; char c; }

  • c++对象内存布局示例详解

    目录 前言 继承对象的内存布局 具有多重继承和虚拟功能的对象的内存布局 总结 前言 了解你所使用的编程语言究竟是如何实现的,对于C++程序员可能特别有意义.首先,它可以去除我们对于所使用语言的神秘感,使我们不至于对于编译器干的活感到完全不可思议:尤其重要的是,它使我们在Debug和使用语言高级特性的时候,有更多的把握.当需要提高代码效率的时候,这些知识也能够很好地帮助我们. 简单非多态的内存布局 class X { int x; float xx; public: X() {} ~X() {}

  • Python深度学习实战PyQt5窗口切换的堆叠布局示例详解

    目录 1. 堆叠布局简介 1. 1什么是堆叠布局(Stacked Layout) 1.2 堆叠布局的实现方法 2. 创建多窗口切换的堆叠布局 3. 堆叠布局的主程序设计 3.1 QStackedWidget 类 3.2 建立信号/槽连接 3.3 页面控制程序 3.4 堆叠布局中的控件操作 软件项目中经常需要多种不同的图形界面,以适应不同的任务场景.选项卡控件(QTackedWidget)通过标签选择打开对应的对话框页面,不需要另外编程.堆叠窗口控件(QStackedWidget)在主程序中通过编

  • iOS小技能之字典转模及对象相等性示例详解

    目录 前言 I 字典转模型 1.1 字典转模型的实现步骤 1.2 字典转模型的过程 II 对象的相等性 & 本体性 2.1 相等性检查 2.2 Foundation 框架中,自己实现的相等性检查 2.3 字符串驻留 III 代码重构(前提是已经实现了基本功能) see also 前言 字典转模型 /** 通常实现字典实例化模型,都实现了以下模型的实例化方法*/ //使用字典实例化模型 - (instancetype) initWithDictionary :(NSDictionary *) ap

  • flutter中使用流式布局示例详解

    目录 简介 Flow和FlowDelegate Flow的应用 总结 简介 我们在开发web应用的时候,有时候为了适应浏览器大小的调整,需要动态对页面的组件进行位置的调整.这时候就会用到flow layout,也就是流式布局. 同样的,在flutter中也有流式布局,这个流式布局的名字叫做Flow.事实上,在flutter中,Flow通常是和FlowDelegate一起使用的,FlowDelegate用来设置Flow子组件的大小和位置,通过使用FlowDelegate.paintChildre可

  • SpringMVC域对象共享数据示例详解

    目录 SpringMVC域对象共享数据 一.域对象 1. 域对象的作用 2. 域对象生命周期 3. 使用原则 二.向域对象共享数据 1. 向 request 域对象共享数据 2. 向 session 域共享数据 3. 向 application 域共享数据 SpringMVC域对象共享数据 一.域对象 1. 域对象的作用 就是在一定范围内可以共享数据,通常有 3 种: request: 一次请求,多个资源共享数据 session: 默认一次会话,多个请求,多个资源共享数据 servletCont

  • vue3响应式Object代理对象的读取示例详解

    目录 正文 读取属性 xx in obj for ... in 正文 从这一章开始,作者将更新深入的讲解响应式,尤其是vue3响应式的具体的实现.其实在前面一章,如果你仔细阅读,你是可以实现一个简单的响应式函数的,类似于@vue/reactive,当然那只是个demo,是个玩具,我能不能在生产环境上去使用的,它差了太多功能和边界条件. 现在,我们才是真正的深入@vue/reactive. 在vue中,obj.a是一个读取操作,但是仔细想来,读取这个操作很宽泛. obj.a // 访问一个属性 '

  • Redis如何存储对象与集合示例详解

    前言 大家都知道在项目中,缓存以及mq消息队列可以说是不可或缺的2个重要技术.前者主要是为了减轻数据库压力,大幅度提升性能.后者主要是为了提高用户的体验度,我理解的是再后端做的一个ajax请求(异步),并且像ribbmitmq等消息队列有重试机制等功能. 这里主要讲redis如何把对象,集合存入,并且取出.下面话不多说了,来一起看看详细的介绍吧. 1.在启动类上加入如下代码 private Jedis jedis;private JedisPoolConfig config;private Je

  • JavaScript中的垃圾回收与内存泄漏示例详解

    前言 程序的运行需要内存.只要程序提出要求,操作系统或者运行时就必须供给内存.所谓的内存泄漏简单来说是不再用到的内存,没有及时释放.为了更好避免内存泄漏,我们先介绍Javascript垃圾回收机制. 在C与C++等语言中,开发人员可以直接控制内存的申请和回收.但是在Java.C#.JavaScript语言中,变量的内存空间的申请和释放都由程序自己处理,开发人员不需要关心.也就是说Javascript具有自动垃圾回收机制(Garbage Collecation). 一.垃圾回收的必要性 下面这段话

  • 关于C/C++内存管理示例详解

    1.内存分配方式 在C++中,内存分成五个区,分别是堆.栈.自由存储区.静态存储区和常量存储区. 1) 栈 执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置处理器指令集中,效率很高,但分配的内存容量有限. 2) 堆 由new分配的内存块,释放由程序员控制.如果程序员没有释放,那么就在程序结束的时候,被操作系统回收. 3) 自由存储区 由malloc等分配的内存块,用free结束自己的生命. 4) 静态存储区 全局变量和静态变量被分配到

随机推荐