C++类对象模型之内存布局

(C/C++必学)深入剖析类和其内存结构 一、前言 作为一个不断进步的程序员,在我们会使用一项技能后,不能只知其然不知其所以然,针对非科班或非系统成才的野生程序员,更应该在熟练使用后进行深入剖析和理解。要秉承看懂,会用,理解,复现的步骤进行学习,在使用这项技能后别人问到为什么要这么使用或者面试问到深层次的问题时不至于无所应对。有种“我知道怎么用,但是我不知道为什么”的无力感。 二、什么是类 1、类的组成 一个类由类名,类成员函数,类成员变量以及其他(静态变量,友元等)组成,以下面的 Demo ... 阅读详情

1、C++类对象的内存布局 

    在C++的类对象中,有两种类的成员变量:static和非static,有三种成员函数:static、非static和virtual。那么,它们在C++的内存中是如何分布的呢?

      C++程序的内存格局通常分为四个区:全局数据区(data area),代码区(code area),栈区(stack area),堆区(heap area)(即自由存储区)。全局数据区存放全局变量,静态数据和常量。所有类成员函数和非成员函数代码存放在代码区;为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;余下的空间都被称为堆区。

     在类的定义时,

  • 类的成员函数被放在代码区。
  • 类的静态成员变量在全局数据区。
  • 非静态成员变量在类的实例内,实例在栈区或者堆区。
  • 虚函数指针、虚基类指针在类的实例内,实例在栈区或者堆区。

     类的实例如果是定义的类变量,则在栈内存区,如果是new出来的类指针,则在堆内存区,同时引用会保存在栈里。 

     为何这样设计?其实这是从c语言发展而来的。类的成员变量相当于c的结构体,类的成员函数类似于c的函数,类的静态变量类似于c的静态或全局变量,至于虚函数,函数体还是放在代码区,但虚函数的指针和成员变量一起放在数据区,这是因为虚函数的函数体有多个,不同的子类调用同一虚函数实则调用的不同函数体,因此需要在类的数据区保持真正的虚函数的指针。

     类的成员函数为什么不需要在类的数据区保持指针?因为类的成员函数是唯一的,在编译时,编译器会为每个类的成员函数改头换面,如函数名加上类名,参数加上this类指针。这样类的成员函数和c的普通函数就一样了。虚函数由于其多态的特殊性,无法这样处理,所以需要保持在类的数据区。

 

     下面就一个非常简单的类,通过逐渐向其中加入各种成员,来逐一分析上述两种成员变量及三种成员函数对类的对象的内存分布的影响。 

注:以下的代码的测试结果均是基于Ubuntu 14.04 64位系统下的G++ 4.8.2,若在其他的系统上或使用其他的编译器,可能会运行出不同的结果。 

2、含有非static成员变量及成员函数的类的对象的内存分布 


类Persion的定义如下: 
class Person
{undefined
    public:
        Person():mId(0), mAge(20){}
        void print()
        {undefined
            cout << "id: " << mId
                 << ", age: " << mAge << endl;
        }
    private:
        int mId;
        int mAge;
}; 

Person类包含两个非static的int型的成员变量,一个构造函数和一个非static成员函数。为弄清楚该类的对象的内存分布,对该类的对象进行一些操作如下: 
int main()
{undefined
    Person p1;
    cout << "sizeof(p1) == " << sizeof(p1) << endl;
    int *p = (int*)&p1;
    cout << "p.id == " << *p << ", address: "  << p << endl;
    ++p;
    cout << "p.age == " << *p << ", address: " << p << endl;
    cout << endl;
    
    Person p2;
    cout << "sizeof(p2) == " << sizeof(p1) << endl;
    p = (int*)&p2;
    cout << "p.id == " << *p << ", address: " << p << endl;
    ++p;
    cout << "p.age == " << *p << ", address: " << p << endl;
    return 0;
} 

其运行结果如下: 

      从上图可以看到类的对象的占用的内存均为8字节,使用普通的int*指针可以遍历输出对象内的非static成员变量的值,且两个对象中的相同的非static成员变量的地址各不相同。 

      据此,可以得出结论,在C++中,非static成员变量被放置于每一个类对象中,非static成员函数放在类的对象之外,且非static成员变量在内存中的存放顺序与其在类内的声明顺序一致。即person对象的内存分布如下图所示: 

3、含有static和非static成员变量和成员函数的类的对象的内存分布

向Person类中加入一个static成员变量和一个static成员函数,如下:
class Person
{undefined
     public:
         Person():mId(0), mAge(20){ ++sCount; }
         ~Person(){ --sCount; }
         void print()
         {undefined
             cout << "id: " << mId
                  << ", age: " << mAge << endl;
         }
         static int personCount()
         {undefined
             return sCount;
         }
     private:
         static int sCount;
         int mId;
         int mAge;
}; 

      测试代码不变,与第1节中的代码相同。其运行结果不变,与第1节中的运行结果相同。 据此,可以得出:static成员变量存放在类的对象之外,static成员函数也放在类的对象之外。

其内存分布如下图所示:


4、加入virtual成员函数的类的对象的内存分布

在Person类中加入一个virtual函数,并把前面的print函数修改为虚函数,如下: 
class Person
{undefined
    public:
        Person():mId(0), mAge(20){ ++sCount; }
        static int personCount()
        {undefined
            return sCount;
        }
 
        virtual void print()
        {undefined
            cout << "id: " << mId
                 << ", age: " << mAge << endl;
        }
        virtual void job()
        {undefined
            cout << "Person" << endl;
        }
        virtual ~Person()
        {undefined
            --sCount;
            cout << "~Person" << endl;
        }
 
    protected:
        static int sCount;
        int mId;
        int mAge;
};

为了查看类的对象的内存分布,对类的对象执行如下的操作代码,如下: 
int main()
{undefined
    Person person;
    cout << sizeof(person) << endl;
    int *p = (int*)&person;
    for (int i = 0; i < sizeof(person) / sizeof(int); ++i, ++p)
    {undefined
        cout << *p << endl;
    }
    return 0;
} 

其运行结果如下: 


从上图可以看出,加virtual成员函数后,类的对象的大小为16字节,增加了8。通过int*指针遍历该对象的内存,可以看到,最后两行显示的是成员数据的值。

      C++中的虚函数是通过虚函数表(vtbl)来实现,每一个类为每一个virtual函数产生一个指针,放在表格中,这个表格就是虚函数表。每一个类对象会被安插一个指针(vptr),指向该类的虚函数表。vptr的设定和重置都由每一个类的构造函数、析构函数和复制赋值运算符自动完成。

      由于本人的系统是64位的系统,一个指针的大小为8字节,所以可以推出,在本人的环境中,类的对象的安插的vptr放在该对象所占内存的最前面。其内存分布图如下:


注:虚函数的顺序是按虚函数定义顺序定义的,但是它还包含其他的一些字段,本人还未明白它是什么,在下一节会详细说明虚函数表的内容。


5、虚函数表(vtbl)的内容及函数指针存放顺序


      在第3节中,我们可以知道了指向虚函数表的指针(vptr)在类中的位置了,而函数表中的数据都是函数指针,于是便可利用这点来遍历虚函数表,并测试出虚函数表中的内容。

测试代码如下:
typedef void (*FuncPtr)();
int main()
{undefined
    Person person;
    int **vtbl = (int**)*(int**)&person;
    for (int i = 0; i < 3 && *vtbl != NULL; ++i)
    {undefined
        FuncPtr func = (FuncPtr)*vtbl;
        func();
        ++vtbl;
    }
 
    while (*vtbl)
    {undefined
        cout << "*vtbl == " << *vtbl << endl;
        ++vtbl;
    }
    return 0;
}

代码解释:
由于虚函数表位于对象的首位置上,且虚函数表保存的是函数的指针,若把虚函数表当作一个数组,则要指向该数组需要一个双指针。我们可以通过如下方式获取Person类的对象的地址,并转化成int**指针:
Person person;
int **p = (int**)&person;

再通过如下的表达式,获取虚函数表的地址:
 
int **vtbl = (int**)*p;

然后,通过如下语句获得虚函数表中函数的地址,并调用函数。
FuncPtr func = (FuncPtr)*vtbl;
func();

最后,通过++vtbl可以得到函数表中下一项地址,从而遍历整个虚函数表。

其运行结果如下图所示:


从上图可以看出,遍历虚函数表,并根据虚函数表中的函数地址调用函数,它先调用print函数,再调用job函数,最后调用析构函数。函数的调用顺序与Person类中的虚函数的定义顺序一致,其内存分布与第3节中的对象内存分布图相一致。从代码和运行结果,可以看出,虚函数表以NULL标志表的结束。但是虚函数表中还含有其他的数据,本人还没有清楚其作用。

6、继承对于类的对象的内存分布的影响


本文并不打算详细地介绍继承对对象的内存分布的影响,也不介绍虚函数的实现机制。这里主要给出一个经过本人测试的大概的对象内存模型,由于代码较多,不一一贴出。假设所有的类都有非static的成员变量和成员函数、static的成员变量及成员函数和virtual函数。
1)单继承(只有一个父类)
类的继承关系为:class Derived : public Base

Derived类的对象的内存布局为:虚函数表指针、Base类的非static成员变量、Derived类的非static成员变量。

2)多重继承(多个父类)
类的继承关系如下:class Derived : public Base1, public Base2

Derived类的对象的内存布局为:基类Base1子对象和基类Base2子对象及Derived类的非static成员变量组成。基类子对象包括其虚函数表指针和其非static的成员变量。

3)重复继承(继承的多个父类中其父类有相同的超类)
类的继承关系如下:
class Base1 : public Base
class Base2:  public Base
class Derived : public Base1, public Base2

Derived类的对象的内存布局与多继承相似,但是可以看到基类Base的子对象在Derived类的对象的内存中存在一份拷贝。这样直接使用Derived中基类Base的相关成员时,就会引发歧义,可使用多重虚拟继承消除之。

4)多重虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)
类的继承关系如下:
class Base1 : virtual public Base
class Base2:  virtual public Base
class Derived : public Base1, public Base2

Derived类的对象的内存布局与重复继承的类的对象的内存分布类似,但是基类Base的子对象没有拷贝一份,在对象的内存中仅存在在一个Base类的子对象。但是它的非static成员变量放置在对象的末尾处。
 

C/C++内存布局 读书笔记:游戏引擎架构第二版 目的:了解存储空间如何分配,多种C/C++变量类型如何运作,需要认识的C/C++的内存布局。 一 可执行映像 当生成C/C++ 程序时,链接器创建可执行文件,这种流行的可执行文件格式 称为可执行与可链接格式。一些平台的可执行文件格式.elf 作为扩展名,windows上的可执行文件格式使用.exe作为扩展名。 无论是哪种文件格式,可执行文件总是包含程序的部分映像,程序执行时这部分映像会放在内存中。之所以被称为部分映像是因为:由于程序除了把可执行映像置于内存中,一般也会分配额外的 阅读详情

相关推荐

C++对象的内存模型

C++对象的内存模型

weixin_42831406的博客 767

【深度C++】之“sizeof运算符”

sizeof运算符作用于表达式或一个类型的名字,得到它们所占的字节数。它与基本内置类型、自定义数据类型和复合类型作用时,结果完全不一样,你都了解吗?

不能自拔 2789

C++对象内存布局

介绍C++对象在内存中是怎样分布的,有助于深层学习C++。

C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现

前言C++对象的内存布局 只有数据成员的对象没有虚函数的对象拥有仅一个虚函数的对象拥有多个虚函数的对象单继承且本身不存在虚函数的继承类的内存布局本身不存在虚函数(不严谨)但存在基类虚函数覆盖的单继承类的内存布局定义了基类没有的虚函数的单继承的类对象布局多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局如果第1个直接基类没有虚函数(表)What if 两个基类都没有虚函数表如果有三个

大自然的小女孩 1039

【影像组学入门百问】1#---#3

理论上讲三大主流平台Windows, Linux和MacOS均可进行影像组学研究。根据笔者实际经验,如果内存不足可能导致软件罢工,其余显示、GPU等均非必要。显示:最小分辨率为1024×768(推荐为1280×1024或更好)。Slicer中的许多计算都是多线程的,并且将受益于多核,多CPU配置。计算机需要足够的内存和图形功能将原始数据和处理结果保存在其内存中。如涉及深度学习等方法的研究需根据实际需求配置电脑,无法一概而论。接口设备:推荐使用带滚轮的三键鼠标。内存:超过4GB(推荐8G以上)。

weixin_38594676的博客 1483

ANSYS_APDL实战入门——从零开始掌握模态分析

本文为ANSYS APDL模态分析实战入门指南,通过一个悬臂梁案例,手把手教你从零开始掌握APDL命令流。内容涵盖从模型创建、材料定义、网格划分到边界条件设置、求解及后处理结果查看的全流程,帮助工程师快速上手结构动力学分析,避免共振风险。

egg99的博客 1128

C++对象模型5——类对象的内存布局

一、类对象的内存布局 1.1、单一继承的类对象布局 示例1 class base { public: int m_fai; int m_faj; }; class derive : public base { public: int m_i; int m_j; }; int main() { base b; derive d; cout<<showbase; cout<<hex<<&b.b_i<<endl; cout&l

Master Cui的博客 527

FreeRTOS面试题汇总

如果一个低优先级的任务持有一个信号量,高优先级的任务想要用这个信号量,造成的死锁怎么解决。12.FreeRTOS的多任务之间如何去进行通信的,什么方法,它实现原理是什么。任务间数据传输一共有三种形式,用的最多的是消息队列,其次是全局变量和信号量。简历上写了了解RTOS,问了rtos的问题,用过哪些函数。讲解FreeRtos原理,调度机制,底层原理,代码调用。FreeRTOS,你的三个任务控制,请问你是怎么实现的。freertos中EventBits_t是干啥的?freertos使任务切换的方式有哪些?

weixin_45636780的博客 1万+

C++对象模型和布局(三种经典类对象内存布局)

侯捷翻译的《深度探索C++对象模型》一书中,对C++对象模型进行了三种典型模型划分,分别为简单对象模型(A Simple Object Model)、表格驱动对象模型(A Table-drive Object Model)、C++对象模型(The C++ Object、 Model)。本文以及之后的博客主要总结的都是最后一个C++对象模型。在C++中,成员分为成员变量和成员函数,其中成员变量分别静态成员变量(class static data members)、非静态成员变量(class nonstati

净无邪博客 1309

基于STM32的智能水温控制系统设计

智能水温控制系统广泛应用于家庭、工业、农业等多个场景,尤其在热水器、恒温泳池和地暖系统中,通过实时监测和调节水温,提高设备的效率和用户的舒适度。本文基于STM32微控制器设计了一个智能水温控制系统,能够实时监测水温、用户设置温度,并根据需要控制加热器或冷却装置,确保水温维持在设定范围内。系统能够根据用户设定自动控制加热和冷却设备,确保水温维持在舒适的范围内,并通过OLED屏实时显示水温信息。在生成的项目基础上,编写水温监测、目标温度设定、加热器和冷却装置控制、OLED数据显示以及超温报警的代码。

2401_88410555的博客 2472

HALCON实战:用两个工业相机实现无缝大视野拼接(附完整代码与避坑指南)

本文详细介绍了使用HALCON软件实现双工业相机图像无缝拼接的完整解决方案,涵盖硬件配置、相机标定、坐标系转换和图像拼接等关键步骤。通过高精度标定和矩阵变换技术,可构建大视野检测系统,适用于液晶面板、PCB等工业检测场景,显著提升检测效率和精度。

weixin_29050513的博客 188

VS查看C++类对象模型内存布局方法

1. 打开vs项目属性>配置属性>c/c++>命令行 2. 在其它选项中添加如下命令 /d1 reportAllClassLayout 是查看所有类的布局 /d1 reportSingleClassLayoutXX 其中"XX"是填入你想查看的类的名字 3. 配置之后,保存。然后重新编译该项目,在[输出]中可看到下图 ...

qq981091829的博客 1859

拆解一个800G光模块:手把手带你认识里面的MT插芯和光纤阵列(FA)

本文详细拆解了800G光模块中的关键组件MT插芯和光纤阵列(FA),揭示了并行光学技术如何实现高速数据传输。通过显微镜下的精密观察,展示了MT插芯的高精度构造和光纤阵列的连接奥秘,同时解析了高精度研磨工艺对光信号传输的重要性。这些微型组件是高速收发模块实现800Gbps带宽的核心技术。

weixin_30839881的博客 1539

C++单继承类对象内存布局实战讲解和分析

本文主要讲解C++对象模型中的单继承类对象模型,分为有虚函数表和无虚函数表两种类型。 # 一、无虚函数和虚继承类(无虚函数表) ## 1.1类Point1D、Point2D、Point3D的UML结构图 # 2.5总结 - 类对象只有非静态成员才占对象模型的内存; - 类对象的静态变量和静态函数都不占用对象模型的内存,存放在静态储存区; - 类对象的普通成员函数也不占用对象模型的内存,存放在普通数据区 - 基类或者当前类存在虚函数时,编译器自动给类对象内存生成一个虚指针,该虚指针指向当前类的虚函数表

净无邪博客 1744

sentinel——SLC数据下载、DEM数据下载拼接、精密轨道数据下载方法

Sentinel——SLC数据下载、DEM数据下载、精密轨道数据下载方法 SLC数据下载 网址:https://search.asf.alaska.edu DEM数据下载 1 手动下载 http://srtm.csi.cgiar.org/srtmdata/ 3.在SARscape中使用下载好的DEM 将下载好的DEM放在软件默认下载DEM路径下(默认路径是C:\Users<用户名&g...

u011322358的博客 2万+

制作 img系统镜像的详细方法

      I.MX6开发板目前支持从TF卡和eMMC启动,启动时,由于各个不同的启动文件对路径有不同的要求,例如uboot文件要求位于启动设备的某个块,dtb文件要求在FAT分区上,文件系统要求在Linux分区上,基于此,我们目前采用将映像制作成img文件的方式,这样可以更简单完成对启动卡的制作。 关于如何制作img,方法如下:方法一  Windows下面利用Win32工具1.   准备我司提供...

armkits的博客 4万+

c++_learning-对象模型探索

深入介绍了c++类对象的内存布局,以及虚函数的引入对内存布局分布的影响。c++因虚函数的引入,产生的动态多态的性质分析。函数模板、类模板的分析。

qq_44599368的博客 1626

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

带虚继承的类对象模型

vbptr是虚基类表,里面存放的是虚基类的成员变量相对于类对象的偏移量。2、将虚基类整体移到最下面,在原来虚基类的地方加上vbptr。虚继承的优先级比普通继承的优先级高。1、先按照常规布局分布内存。

doubleintfloat的博客 513

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

C++类对象内存模型与成员函数调用分析(上)

C++类对象内存模型是一个比较抓狂的问题,主要是C++特性太多了,所以必须建立一个清晰的分析层次。一般而言,讲到C++对象,都比较容易反应到以下这个图表:     这篇文章,就以这个表格作为分析和行文的策略的纵向指导;横向上,兼以考虑无继承、单继承、多重继承及虚拟继承四方面情况,这样一来,思维层次应该算是比较清晰了。 1、C++类数据成员的内存模型 1.1 无继承情况 实验最能说明

klarclm的专栏 831

对象内存模型-初级(转)

C++最初只是一个带类的C,后来给类加了继承功能,有了继承,自然就发展出多态的概念。那么当定义一个C++类对象时,它的内存模型是怎样的呢?了解它的内存模型应该会让我们在编程时心中更有数。下面就依C++的自然发展顺序来简单探讨一下其类对象的内存模型。1. 无继承,无多态     这是最简单的情况,在C里有结构体,只是一个多数据类型的集合,C++的类最初做的就是将数据和其操作封装在一起,如下类和结构体

闻道有先后,术业有专攻,如是而已。 958

C++类对象内存模型与成员函数调用分析(下)

C++类对象内存模型与成员函数调用分析(下) (文章转载于  http://blog.youkuaiyun.com/fairyroad/article/details/6376646) 2.4.2 多重继承下的虚拟函数 多重继承下的虚拟函数主要有一下几个麻烦: 几个父

PrintLife的专栏 510

C++虚函数对象模型剖析

测试环境:VS2013 C++中的类对象模型有简单的,也有复杂的。今天尝试着剖析一下,以加深对继承,虚函数等的理解。 从最简单的继承开始 class Base { public: int b; }; class Derive : public Base { public: int d; }; int main() { cout << sizeof(Derive) <<

Fireplusplus的博客 656

c++对象模型探索 视频教程

c++对象模型探索 视频教程 1-1 c++对象模型探索课程详细介绍.mp4 2-1 类对象所占用的空间.mp4 2-10 成员初始化列表说.mp4 2-2 对象结构的发展和演化.mp4 2-3 this指针调整.mp4 2-4 分析obj目标文件,构造函数语义.mp4 2-5 构造函数语义续.mp4 2-6 拷贝构造函数语义.mp4 2-7 程序转化语义.mp4 2-8 程序的优化.mp4 2-9 程序优化续、拷贝构造续,深浅拷贝.mp4 3-1 虚函数表指针位置分析.mp4 3-2

weixin_46353280的博客 551

C++对象内存布局探究

C++对象内存布局探究。

xuyouqiang1987的博客 1112

一个 C++ 类对象有多大?内存布局是怎样的?(上部)

从「一个C++类对象有多大」开始.........

程序员小坤的博客 1192

C++ 内存模型

C++ 内存模型

zaizai1007的博客 1078
上一篇: C++四种类型转换运算符:static_cast、dynamic_cast、const_cast和reinterpret_cast
下一篇: C++如何获取虚函数表(vtbl)的内容及虚成员函数指针存放原理
杂质想当杂志
博客等级 码龄10年 13粉丝 32原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值