C++ 无虚函数的单继承内存模型

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

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

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

在只有单继承的情况下,类对象的内存布局也还算不复杂。在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 上的实现我测试了一下,除了一些基本的东西比较好估计以外,其他的很多种情况貌似找不到很好的理由去解释它,就是比较难总结,所以,索性我就不说空类这种情况了,没什么意义。

多继承的情况

既然单继承的情况已经明了了,多继承的情况也自然可以推理出来了,自然是按照继承的声明顺序,逐个类依次排放,最后到派生类本身的成员。比如类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;
} 

时间: 2024-10-09 18:22:57

C++ 无虚函数的单继承内存模型的相关文章

C++ 深入了解 函数, 虚函数, 单继承,多继承,指针,引用。

最近又开始写项目服务器部分了, 再次接触了C++ 有了一些更深入的体会.记录一下,以免忘记~  之前学习C++ 差不错都是靠死记, 记住C++的用法,C++的特性,然后去使用.没有从根本上理解,导致 几年不用C++,就已经完全忘记,然后又要花好长时间去记忆,使用.所以要真正做到学会C++,必须要从根本上了解,这样才不至于有会忘记, 而且使用起来会更的心应手. 1.明确了一些定义 关于指针, 刚开始学习指针的时候,总是一些模糊的印象,想不清楚具体是什么, 就知道死记用法.仅仅知道指针能指向一个对象

虚函数和虚拟继承的内存分布

一.虚函数 (1)C++中的虚函数的主要作用:实现了多态的机制. (2)多态:用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. (3)多态要基于函数重载,所以如果子类没有重载父类的虚函数那是一件毫无意义的事情. 二.虚函数表 1.虚函数表:虚函数表C+

关于虚函数,类的内存分布以及类的成员函数调用原理

1.类的内存分布 空类为了占位,空间占一个字节 成员函数,静态函数,静态变量并不占内存(不和类放在一起) 所有的虚函数也并不和类放在一起,而是将所有的虚函数构造成一个虚函数表,用一个指针指向这个虚函数表,类仅仅存储这个指针,一个指针在32位的机器上占四个字节 所有的非静态成员变量占内存 因此,类的内存分布=所有的非静态成员变量+虚指针(自创的名词:即指向虚函数表的指针) 2.虚函数的原理 一个非继承的类:一个虚指针(指向他的虚函数表). 一个单继承的类:一个虚指针(指向他的虚函数表,这个虚函数表

C++ 虚函数的内存分配

1.无继承的普通类: 在有虚函数的情况下类会为其增加一个隐藏的成员,虚函数表指针,指向一个虚函数表,虚函数表里面就是类的各个虚函数的地址了.那么,虚函数表指针是以什么模型加入到类里面的,虚函数表里面又是怎么安排的呢.简单来看下就可以知道了. #include"stdafx.h" #pragma pack(8) class A{ public: int a; double a2; A() :a(0xaaaaaaaa), a2(0){} virtual void funA2(){} vir

C++ 虚函数的对象模型

1.无继承的普通类: 在有虚函数的情况下类会为其增加一个隐藏的成员,虚函数表指针,指向一个虚函数表,虚函数表里面就是类的各个虚函数的地址了.那么,虚函数表指针是以什么模型加入到类里面的,虚函数表里面又是怎么安排的呢.简单来看下就可以知道了. #include"stdafx.h" #pragma pack(8) class A{ public: int a; double a2; A() :a(0xaaaaaaaa), a2(0){} virtual void funA2(){} vir

c++继承汇总(单继承、多继承、虚继承、菱形继承)

一.C++中的对象模型 1. 概念 语言中直接支持面向对象程序设计的部分: 对于各种支持的底层实现机制.(没看懂……) 2. 类中的成员分类 a) 成员函数 i. static function ii. non static function iii. virtual function b)  数据成员 i. static member data ii. non static member data 3. C++对象模型 a) 类对象内存布局中的包括 i. non static member d

虚继承之单继承的内存布局(VC在编译时会把vfptr放到类的头部,这和Delphi完全一致)

C++2.0以后全面支持虚函数与虚继承,这两个特性的引入为C++增强了不少功能,也引入了不少烦恼.虚函数与虚继承有哪些特性,今天就不记录了,如果能搞了解一下编译器是如何实现虚函数和虚继承,它们在类的内存空间中又是如何布局的,却可以对C++的了解深入不少.这段时间花了一些时间了解这些玩意,搞得偶都,不过总算有些收获,嘿嘿. 先看一段代码class A{      virtual aa(){};}; class B : public virtual  A{      char j[3];      

虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte

#include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout << "A:A" <<endl; } virtual void getb(){ cout << "A:B" <<endl; } }; class B :public A{ public: B(){} virtual void g

看到的关于虚函数继承及虚继承解释比较好的文章的复制

(来源于:http://blog.chinaunix.net/uid-25132162-id-1564955.html) 1.空类,空类单继承,空类多继承的sizeof #include <iostream> using namespace std; class Base1 { }; class Base2 { }; class Derived1:public Base1 { }; class Derived2:public Base1, public Base2 { }; int main(