C++ 无虚函数、无虚基类的继承内存模型

《深度探索c++内存模型》读书笔记 (一) 第一章 关于对象前言加上封装后的布局成本c++对象模式加上继承C++中的多态对象所占用的内存大小指针的类型加上多态之后 前言 在C语言里面,数据和函数是分开的,它们之间的关联是通过参数和返回值来取得联系。如: typedef struct point3d { float x; float y; float z; } Point3d; void print_point3d(const Point3d *p) { printf("%g %g %g", p->x, p 阅读详情

C++类因为有继承的存在要比C时代的struct复杂得一些,特别是加上有虚函数的时候,以及多继承等这些特性更是令其内存布局变得面目全非。说实在的我也把握不了,我只是在一个实际的平台上进行了一些探索而已,并用此篇笔记将我的探索成果记录下来。

 

虽然说有些东西在C++标准里面没有规定如何做,不同的实现可能会有不同的作法,但是了解一个实际的系统是如何做的也会有益于我们更加深入的了解C++或者举一反三地理解其他的实现,而且如果我们了解了自己所用的系统上的具体实现的话,就可以对其为所欲为。

 

1.没有虚函数单继承内存布局


在只有单继承的情况下,类对象的内存布局也还算不复杂。在class B 继承了class A 的情况下,我们大致也可以猜测到B的对象的内存分布,应该是先储存类A里面的成员,然后再依次存放类B本身的成员。因为这样的布局在转换父类指针的时候不需要做任何处理即可兼容C,而且这种对象模型的实现也比较简单合理。但是具体是怎么放的呢?可以想到的大概有4种情况。像class B:A{…}这种单继承关系,默认的继承是private继承,访问修饰不影响内存布局,内存布局的4种可能是:

 

1.                   先将A作为一块整块的结构存放,然后再将B作为一块整块的结构存放,即有点像这样class temp{A x; B y;}这种等价布局;

 

2.                   先将A作为一块整块的结构存放,然后再在其后依次排放类B本身的各个成员变量,像class temp{A x; …. B的各个成员变量…}这种等价布局;

 

 

3.                   先将A的各个成员变量依次排放,然后将B作为一个整体结构存放,就像这样class temp{ A的各个成员….; B y}这种等价布局;

 

4.                   先排放A的各个成员变量再跟着排放B的各个成员变量,即像是这样class temp{A的各个成员…..; B的各个成员…..;};

 

这4种布局有什么不同么?当然有~因为有字节对齐的存在~!这几种布局的意义显然是不同的,当然,如果把字节对齐设为1字节对齐的话这几种布局模型表面上看起来就是一样的了。

 

如果熟悉结构的字节对齐的话,就可以很容易找到一些合适的类来一个个地推翻他们,如果不是那样的话,估计找一些合适的类的过程会比较困惑。当然也不要刻意去找一些很巧妙的类,能够证明对错其实就可以了。

 

#pragma pack(8)
class A1{public:
   double b1;
   char c1;
   A1():b1(0),c1(0xFF){}
};
 
classA2:A1{public:
   char a2;
   int a3;
   A2():a2(0xee),a3(0x22222222){}
};


 

像上面的两个类,按照字节对齐的规则可以推翻那4个可能的分布中至少两个内存布局情况。按照字节对齐规则,在上面的两个类中,布局类型1和2是等价的布局,而第3种可能和第4种可能都有其不同的布局情况。在有IDE的情况下我们可以很方便的写代码然后就直接抓数据,我在这里用的是VS2013。一同测试的代码还有下面的一些,和上面是一起的:

 

#include"stdafx.h"
#pragma pack(8)
class A1{
public:
      double b1;
      char c1;
      A1() :b1(0), c1(0xFF){}
};
 
class A2 :A1{
public:
      char a2;
      int a3;
      A2() :a2(0xee), a3(0x22222222){}
};
 
class A2_{
public:   //  A2不带A1的继承的情况的等价类
      char a2;
      int a3;
      A2_() :a2(0xee), a3(0x22222222){}
};
 
class temp1{
public:     // 与情况1等价布局的类
      A1 a1;
      A2_ a2;
};
 
class temp2{
public:     // 与情况2等价布局的类
      A1 a1;
      char a2;
      int a3;
      temp2() :a2(0xee), a3(0x22222222){}
};
 
class temp3{
public:     // 与情况3等价布局的类
      double b1;
      char c1;
      A2_ a2;
      temp3() :b1(0), c1(0xFF){}
};
 
class temp4{
public:     // 与情况4等价布局的类
      double b1;
      char c1;
      char a2;
      int a3;
      temp4() :b1(0), c1(0xFF), a2(0xee),a3(0x22222222){}
};
 
int res;    // 在main函数里打断点抓内存数据
int _tmain(int argc, _TCHAR* argv[])
{
      A2 x; res = sizeof(x);
      temp1 y; res = sizeof(y);
      temp2 z; res = sizeof(z);
      temp3 m; res = sizeof(m);
      temp4 n; res = sizeof(n);
 
      return 0;
} 


   以上代码在VS2013里面的布局抓包如下:

 

A2 继承自 A1,以下是A2 类在内存中的布局,我们看看

 

图1:A2在内存中的布局

 

接下来我们以4 种方式模拟 A2 的内存布局,看看哪种才是真相。

 

1.     情况1:先将A1(A2的基类)作为一块整块的结构存放,然后再将A2_ (A2 自己的成员变量)作为一块整块的结构存放,即有点像这样class temp1{A1 x;  A2_ y;}这种等价布局,我们以这种方式模拟 A2 的内存布局,发现其内存布局确定和图 1 (A2的内存布局)一样

 

图2

 

2.     情况2:先将A1 (A2的基类)作为一块整块的结构存放,然后再在其后依次排放类A2 自己的各个成员变量成员,像class temp2{A1 x; …. A2的各个成员变量…}这种等价布局,我们以这种方式模拟 A2 的内存布局,发现其内存布局确定和图 1 (A2的内存布局)一样

 

图3

 

3.     情况3:先将A1 (A2的基类)的各个成员变量依次排放,然后将A2_(A2 自己的成员变量)作为一个整体结构存放,就像这样class temp3{ A的各个成员….; B y}这种等价布局,我们以这种方式模拟 A2 的内存布局,发现和图 1 (A2的内存布局)不一样,说明情况3是错误的。

 

图4

 

4.     情况4:先排放A1 (A2的基类)的各个成员变量再跟着排放A2 自己的成员变量的各个成员变量,即像是这样class temp4{A1的各个成员…..; A2的各个成员…..;},我们以这种方式模拟 A2 的内存布局,发现和图 1 (A2的内存布局)不一样,说明情况4是错误的。 

图5

 

很明显,第3种和第4种情况的内存分布方式已经被推翻了,还有第1和第2种情况中的一种是正确的,要想进一步推翻其中一个得改一下我们的类里面的成员类型了。我修改的思路是这样的:让基类 class A1 的最宽的成员小于8字节,而且还得让sizeof(A1)不能为8的整数倍,然后让class A2的最宽成员变成8字节的double。

 

#include"stdafx.h"
#pragma pack(8)
 
class A1{
public:
      short b1;
      char c1;
      A1() :b1(0xaaaa), c1(0xFF){}
};
 
class A2 :A1{
public:
      char a2;
      double a3;
      A2() :a2(0xee), a3(0){}
};
 
class A2_{
public:   // A2不带A1的继承 的情况的等价类
      char a2;
      double a3;
      A2_() :a2(0xee), a3(0){}
};
 
class temp1{
public:     // 与情况1等价布局的类
      A1 a1;
      A2_ a2;
};
 
class temp2{
public:     // 与情况2等价布局的类
      A1 a1;
      char a2;
      double a3;
      temp2() :a2(0xee), a3(0){}
};
 
int res;    // 在main函数里打断点抓内存数据
 
int _tmain(int argc, _TCHAR* argv[])
{
      A2 x; res = sizeof(x);
      temp1 y; res = sizeof(y);
      temp2 z; res = sizeof(z);
 
 
      return 0;
}


测试结果如下:

A2 继承自 A1,以下是A2 类在内存中的布局,我们看看

 

图6:A2 的内存布局

 

接下来我们以2 种方式模拟 A2 的内存布局,看看哪种才是真相。

 

1.     情况1:先将A1(A2的基类)作为一块整块的结构存放,然后再将A2_ (A2 自己的成员变量)作为一块整块的结构存放,即有点像这样class temp1{A1 x;  A2_ y;}这种等价布局,我们以这种方式模拟 A2 的内存布局,发现temp1的大小为 24 个字节,这与 A2 的大小(见图6,16个字节)不符,所有情况1失败。


图7

 

2.      情况2:先将A1 (A2的基类) 作为一块整块的结构存放,然后再在其后依次排放类A2 自己的各个成员变量成员,像class temp2{A1 x; …. A2的各个成员变量…}这种等价布局,我们以这种方式模拟 A2 的内存布局,发现其内存布局确定和图6 (A2的内存布局)一样

 

 

现在我们可以得出结论了,单继承的类的内存布局是和第二种情况等价的:先将base作为一块整块的结构存放,然后再在其后依次排放类derived本身的各个成员变量,像class temp{base x; …. derived的各个成员变量…}这种等价布局。

 

其实这也是最合理的一种情况,因为C++标准规定需要保证基类子对象的完整性,所以基类就必须作为一个完整的结构进行存储。


知道了这些对象的内存布局的情况我们就可以对任意的单继承类进行把握其在内存里面的样子了,一直递归地推理下去就可以得到最终的内存布局。单继承还是比较简单的,还有一种比较特殊的情况就是继承有空类的时候,空类这个东西各个系统应该会有不同的实现,在Windows 上的实现我测试了一下,除了一些基本的东西比较好估计以外,其他的很多种情况貌似找不到很好的理由去解释它,就是比较难总结,所以,索性我就不说空类这种情况了,没什么意义。

1.没有虚函数多继承内存布局

既然单继承的情况已经明了了,多继承的情况也自然可以推理出来了,自然是按照继承的声明顺序,逐个类依次排放,最后到派生类本身的成员。比如类B继承了A1、A2、A3,即class B:A1,A2,A3{…}; 其内存布局就是等价于class temp{A1 a1; A2 a2; A3 a3; … }; 论证过程和单继承的时候差不多,也不难就是比较多东西而已,在这里我就不摆出来了,随便摆个例子吧。

#include"stdafx.h"
#pragma pack(8)
class A1{
public:
      int a1;
      A1() :a1(0xa1a1a1a1){}
};
class A2{
public:
      int a2;
      A2() :a2(0xa2a2a2a2){}
};
class A3{
public:
      int a3;
      A3() :a3(0xa3a3a3a3){}
};
class B : A1,A2, A3 {
public:
      int b;
      B() :b(0xbbbbbbbb){}
};
 
 
int _tmain(int argc, _TCHAR* argv[])
{
      B bb;
      return 0;
} 


 

C++虚函数内存布局深度解析:从继承到多继承与菱形继承 虚函数表是C++实现运行时多态的核心机制,它本质上是一个存储类虚函数地址的指针数组。其工作原理是:每个包含虚函数的类在编译期会生成一张虚函数表,类的对象在内存布局起始位置包含一个指向该表的指针(vptr)。当通过基类指针或引用调用虚函数时,程序会通过vptr找到正确的虚函数表,并根据函数在表中的偏移量定位到实际要执行的派生类函数地址,从而实现动态绑定。这项技术的核心价值在于提供了灵活的接口与实现分离能力,是构建可扩展框架和复杂对象层次结构的基石,广泛应用于GUI系统、游戏引擎、插件架构等需要运行时行为定制的 阅读详情

相关推荐

C++对象内存模型学习

本文原文出处为MSDN。如果你安装了MSDN,可以搜索到C++ Under the Hood。否则也可在网站上找到http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarvc/html/jangrayhood.asp 。 1 前言 本文着回答这样一些问题: 1* 类如何布局? 2* 成员变量如何访问? 3* 成员函数如何访问? 4* 所谓的“调整块”(adjuster thunk)是怎么回事? 5* 使用如下机制时,开销

bcbobo21cn的专栏 275

继承内存布局@c++对象模型

c++对象模型中,如果子类虚继承父类,内存布局 。

飞鸟 864

C++内存分布之继承和多继承(无虚函数)

之前在本地有写过一篇很长的关于C++内存分布的文档(里面有很多调试例子),想一次性移到博客当中。但是排版起来非常麻烦,索性现在分情况来写吧。一来排版比较容易,二来同学们如果看到了我的博客,也会觉得冗长。博客中如果有错误的地方,欢迎大家指正,我们共同进步。 现在来介绍一下我的环境: 1.系统:Ubuntu 12.04.2 LTS 2.编译器:gcc version 4.4....

armwind的专栏 2458

c++浅探类内存布局

总结 多继承和虚继承用了解太多,现实中怎么用到。 对继承 若父类和子类都没有虚函数,则子类对象内存布局没有虚表指针和虚表,只有成员变量的内存(字节需要对齐),且父类成员在内存低地址,子类额外的成员在内存高地址(因此,子类指针可以转换为父类指针,又可以再次转换为子类指针,因为子类对象内存中低地址为父类的成员变量) 若父类有虚函数,则子类对象内存布局起始地址部分为虚表指针,指向虚表,后面的结构同上。虚表里面,包含了父类和子类的虚函数,若子类覆盖了父类的虚函数,则子类的虚函数在子类的虚表里面。在构

lgfun的专栏 206

浅析C++当中的对象模型

我们来分析一下C++当中的对象模型。什么是虚函数表?C++的对象模型,我们首先需要分析的是虚函数表。 虚函数,这里我们就要牵扯到多态。简的说就是父类指针或者引用调用写的虚函数,当指向父类的时候,调用父类的虚函数,当指向子类的时候,这时候会调用子类的虚函数。首先,我们在这里需要了解一个概念,虚函数表。虚函数呢,就是通过虚函数表来实现的,在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承

宇哲 1058

C++】浅析C++中的对象模型

以下代码运行环境:windows8.1 32位 VS2015 (一)含有虚函数继承模型: 测试代码: //继承,无虚函数 class A { public: A(int a = 0, char c = 0) :_a(a) , _c(c) {} int GetA()

天涯海角 694

C++虚表详解

使用工具: 本文输出内存模型的工具为vs2017开发人员命令提示符,使用方法如图所示: 我们使用cl(是C1,是C一,是cl,LLLLL)命令cl的语法为: cl [filename].cpp /d1reportSingleClassLayout[className] 意思是查看[filename].cpp文件中名称以[className]开头的所有类的内存模型 如果文件在同一...

chczy1的博客 1万+

C++虚函数、虚继承内存模型

虚函数地址通过虚指针索引的虚函数表在运行时确定;虚表中仅储存了虚函数的地址,还储存了类型RTTI的地址、距实际类型首地址偏移量等信息;虚函数的调用可能涉及到this指针的变更,需要Thunk等方式实现;对于虚基类的派生类,虚基类的偏移量由实际类型决定,因此在运行时才可以确定虚基类的地址;在多态类型的构造和析构过程中,通过修改虚指针使其指向同的虚表,可以实现在同的阶段调用同的虚函数;对于虚继承的情况,由于同一类型虚表的虚表在同具体类型中可以同,在构造和析构时,需要通过VTT。...

杰瑞不打瞌睡的博客 622

C++继承、多继承虚基类 继承、多继承时对象内存分布

类对象内存模型 类是对属性和行为的封装,在类的对象中也有属性(成员变量)和行为(成员函数),反映到类对象内存模型中,也就是内存中应该有对象的成员变量和成员函数。在同一个类的所有对象的成员函数,并是每个对象有一份成员函数,而是共有一份相同的成员函数。例如下面一个类: [cpp] view plaincopyprint? class Base { //行为 public:

xiaoyegui的专栏 1281

C++之深入解析虚函数和虚继承内存模型及其原理

C++虚函数、虚继承内存模型是一个经典问题,其实现依赖于编译器,但其主要原理大体相同。本文以问题导向的方式,详细介绍 g++ 中虚函数和虚继承内存模型及其原理。

╰つ栺尖篴夢ゞ 1465

C++对象内存模型

文章目录C++对象内存模型继承情况虚继承情况存在虚函数情况虚函数继承情况 C++对象内存模型 ​ 一般来说,类成员变量存储顺序按照声明的顺序从低地址到高地址顺序存储。 ​ 对于类的静态成员变量,它独存储在程序的.data段中,与特定的对象相关联。 ​ 如果子类和父类的成员变量(或成员函数)同名,则父类的成员变量(或成员函数)会被隐藏(当然它可以通过类名受限的方式来访问),具体如下: cla...

Ivan_zcy的博客 294

大话C++之:对象内存模型

详解C++对象内存模型

kelvin_feng的专栏 986

vs2019 查看类的虚函数_vs查看虚函数表和类内存布局

继承虚基类继承:在继承定义中包含了virtual关键字的继承关系;虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:class CSubClass : public virtual CBase {}; 其中CBase称之为CSubClass的虚基类,而是说CBase就是个虚基类,因为CBase还可以是虚继承体系中的基类。vs中如何查看内存布局:. 打开“Visua...

weixin_39810901的博客 405

C++虚函数表到虚继承:图解多继承场景下的内存优化技巧

本文深入剖析了C++继承与虚继承场景下的对象内存布局,点讲解了菱形继承导致的数据冗余问题及其解决方案。通过图解和代码示例,详细阐述了虚函数表、虚基类表指针的工作原理,并分析了虚继承来的内存与性能开销,为开发者优化内存使用和设计高效类层次结构提供了实用技巧。

y7z8a的博客 178

C++继承的概念

C++中虚拟继承的概念 为了解决从同途径继承来的同名的数据成员在内存中有同的拷贝造成数据一致问题,将共同基类设置为虚基类。这时从同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样仅就解决了二义性问题,也节省了内存,避免了数据一致的问题。 class 派生类名:virtual 继承方式  基类名 virtual是关键字,声明该基类为派生类的虚

浮尘丶若梦 321

(总结)C++继承,虚基类虚函数与纯虚函数的概念和引用(待补充)

C++虚基类虚函数与纯虚函数的概念和引用     声明:       部分资料出自以下博文:点击打开链接和点击打开链接            一:什么是C++继承的多义性? 先看以下的代码: class A { public: int iValue; }; class B1:public A { public: void b1Printf()

轻雨漫步的专栏 4360

C++继承虚基类

C++中虚拟继承的概念 为了解决从同途径继承来的同名的数据成员在内存中有同的拷贝造成数据一致问题,将共同基类设置为虚基类。这时从同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样仅就解决了二义性问题,也节省了内存,避免了数据一致的问题。 class 派生类名:virtual 继承方式 基类名 virtual是关键字,声明该基类为派生类的虚基类

天空之尘的博客 2618
上一篇: C++ 虚基类表指针字节对齐模型
下一篇: C++ 虚继承的对象模型
chengonghao
博客等级 码龄12年 491粉丝 475原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值