首先重新回顾一下关于类/对象大小的计算原则:

类大小计算遵循结构体对齐原则

第一个数据成员放在offset为0的位置

其它成员对齐至min(sizeof(member),#pragma pack(n)所指定的值)的整数倍。

整个结构体也要对齐,结构体总大小对齐至各个min中最大值的整数倍。

win32 可选的有1, 2, 4, 8, 16
linux 32 可选的有1, 2, 4

类的大小与数据成员有关与成员函数无关
类的大小与静态数据成员无关
虚继承对类的大小的影响
虚函数对类的大小的影响

下面通过实例来展示虚继承和虚函数对类大小造成的影响。

测试环境为:Win32 + Vs2008

一、只出现虚继承的情况

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
 
#include <iostream>
using namespace std;

class BB
{
public :
      int bb_ ;
};

class B1 : virtual public BB
{
public :
      int b1_ ;
};

class B2 : virtual public BB
{
public :
      int b2_ ;
};

class DD : public B1, public B2
{
public :
      int dd_ ;
};

int main (void)
{
      cout<<sizeof (BB)<< endl;
      cout<<sizeof (B1)<< endl;
      cout<<sizeof (DD)<< endl;

B1 b1 ;
      int** p ;

cout<<&b1 <<endl;
      cout<<&b1 .bb_<< endl;
      cout<<&b1 .b1_<< endl;

p = (int **)&b1;
      cout<<p [0][0]<<endl;
      cout<<p [0][1]<<endl;

DD dd ;
      cout<<&dd <<endl;
      cout<<&dd .bb_<< endl;
      cout<<&dd .b1_<< endl;
      cout<<&dd .b2_<< endl;
      cout<<&dd .dd_<< endl;
      p = (int **)&dd;
      cout<<p [0][0]<<endl;
      cout<<p [0][1]<<endl;
      cout<<endl ;
      cout<<p [2][0]<<endl;
      cout<<p [2][1]<<endl;

BB* pp ;

pp = &dd ;
      dd.bb_ = 10; //对象的内存模型在编译时就已经确定了,否则无法定义类的对象,因为要开辟内存
      int base = pp-> bb_;     // 通过间接访问 (其实pp 已经偏移了20 ),这需要运行时的支持
      cout<<"dd.bb_=" <<base<< endl;

return 0;
}

从输出的地址和虚基类表成员数据可以画出对象内存模型图:

virtual base table

本类地址与虚基类表指针地址的差

虚基类地址与虚基类表指针地址的差

virtual base table pointer(vbptr)

从程序可以看出pp是BB* 指针,通过打印pp 的值与&dd 比较可知,

cout<<(void*)&dd<<endl;
cout<<(void*)pp<<endl;

pp实际上已经偏移了20个字节,如何实现的呢?先找到首个vbptr,找到虚基类BB地址与虚基类表指针地址的差,也即是20,接着pp偏移20个字节指向了dd对象中的BB部分,然后就访问到了bb_,这是在运行时才做的转换。记住:C++标准规定对对象取地址将始终为对应类型的首地址。

二、只出现虚函数的情况

(一):一般继承

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
 
#include <iostream>
using namespace std;

class Base
{
public :
    virtual void Fun1()
    {
        cout << "Base::Fun1 ..." << endl;
    }

virtual void Fun2()
    {
        cout << "Base::Fun2 ..." << endl;
    }
    int data1_ ;
};

class Derived : public Base
{
public :
    void Fun2 ()
    {
        cout << "Derived::Fun2 ..." << endl;
    }
    virtual void Fun3()
    {
        cout << "Derived::Fun3 ..." << endl;
    }
    int data2_ ;
};

typedef void (* FUNC)(void );

int main (void)
{
    cout << sizeof (Base) << endl;
    cout << sizeof (Derived) << endl;
    Base b ;
    int **p = (int **)& b;
    FUNC fun = (FUNC) p[0][0];
    fun();
    fun = (FUNC )p[0][1];
    fun();
    cout << endl ;

Derived d ;
    p = (int **)&d;
    fun = (FUNC )p[0][0];
    fun();
    fun = (FUNC )p[0][1];
    fun();
    fun = (FUNC )p[0][2];
    fun();

return 0;
}

从输出的函数体可以画出对象内存模型图:

vtbl:虚函数表(存放虚函数的函数指针)

vptr:虚函数表指针

从输出可以看出,Derived类继承了Base::Fun1,而覆盖了Fun2,此外还有自己的Fun3。注意,因为Fun3是虚函数,才会出现在虚函数表,如果是一般函数是不会的,因为不用通过vptr间接访问。

(二)、钻石继承

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
 
#include <iostream>
using namespace std;

class BB
{
public:
    virtual void vpbb()
    {
        cout << "BB:vpbb().." << endl;
    }
    int bb_;
};
class B1 : public BB
{
public:
    virtual void vpb1()
    {
        cout << "B1:vpb1().." << endl;
    }
    int b1_;
};
class B2 : public BB
{
public:
    virtual void vpb2()
    {
        cout << "B2:vpb2().." << endl;
    }
    int b2_;
};
class DD : public B1, public B2
{
public:
    virtual void vpdd()
    {
        cout << "DD:vpdd().." << endl;
    }
    int dd_;
};

typedef void (* FUNC)(void );

int main()
{
    cout << sizeof(BB) << endl;
    cout << sizeof(B1) << endl;
    cout << sizeof(DD) << endl;
    cout << endl;

DD dd ;
    cout << &dd << endl;
    cout << &dd.B1::bb_ << endl;
    cout << &dd.B2::bb_ << endl;
    cout << &dd .b1_ << endl;
    cout << &dd .b2_ << endl;
    cout << &dd .dd_ << endl;
    cout << endl;

B1 b ;
    int **p = (int **)& b;
    FUNC fun = (FUNC) p[0][0];
    fun();
    fun = (FUNC )p[0][1];
    fun();
    cout << endl ;

p = (int **)&dd
    fun = (FUNC)p[0][0];
    fun();
    fun = (FUNC)p[0][1];
    fun();
    fun = (FUNC)p[0][2];
    fun();

fun = (FUNC)p[3][0];
    fun();
    fun = (FUNC)p[3][1];
    fun();

cout << endl;

return 0;
}

从成员输出的地址和通过虚函数表指针访问到的函数可以画出模型:

DD::vfdd 的位置跟继承的顺序有关,如果DD先继承的是B2, 那么它将跟在B2::vfb2 的下面。

如果派生类是从多个基类继承或者有多个继承分支(从所有根类开始算起),而其中若干个继承分支上出现了多态类,则派生类将从这些分支中的每个分支上继承一个vptr,编译器也将为它生成多个vtable,有几个vptr就生成几个vtable(每个vptr分别指向其中一个),分别与它的多态基类对应。

三、虚继承与虚函数同时出现的情况:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
 
#include <iostream>
using namespace std;

class BB
{
public :
      virtual void vfbb()
     {
           cout<<"BB::vfbb" <<endl;
     }
      virtual void vfbb2()
     {
           cout<<"BB::vfbb2" <<endl;
     }
      int bb_ ;
};

class B1 : virtual public BB
{
public :
      virtual void vfb1()
     {
           cout<<"B1::vfb1" <<endl;
     }
      int b1_ ;
};

class B2 : virtual public BB
{
public :
      virtual void vfb2()
     {
           cout<<"B2::vfb2" <<endl;
     }
      int b2_ ;
};

class DD : public B1, public B2
{
public :
      virtual void vfdd()
     {
           cout<<"DD::vfdd" <<endl;
     }
      int dd_ ;
};

typedef void (* FUNC)(void);

int main (void)
{
      cout<<sizeof (BB)<< endl;
      cout<<sizeof (B1)<< endl;
      cout<<sizeof (DD)<< endl;

BB bb ;
      int** p ;
      p = (int **)&bb;
      FUNC fun ;
      fun = (FUNC )p[0][0];
      fun();
      fun = (FUNC )p[0][1];
      fun();
      cout<<endl ;

B1 b1 ;
     
      p = (int **)&b1;
      fun = (FUNC )p[0][0];
      fun();
      fun = (FUNC )p[3][0];
      fun();
      fun = (FUNC )p[3][1];
      fun();

cout<<p [1][0]<<endl;
      cout<<p [1][1]<<endl;
      cout<<endl ;

DD dd ;
      p = (int **)&dd;
      fun = (FUNC )p[0][0];
      fun();
      fun = (FUNC )p[0][1]; // DD::vfdd 挂在 B1::vfb1的下面
      fun();
      fun = (FUNC )p[3][0];
      fun();
      fun = (FUNC )p[7][0];
      fun();
      fun = (FUNC )p[7][1];
      fun();
     
      cout<<p [1][0]<<endl;
      cout<<p [1][1]<<endl;
      cout<<p [4][0]<<endl;
      cout<<p [4][1]<<endl;

return 0;
}

从输出的虚基类表成员数据和虚函数体可以画出对象内存模型图:

上图中vfdd 出现的位置跟继承的顺序有关,如果DD先继承的是B2,那么它将跟在vfb2 的下面。

注意:如果没有虚继承,则虚函数表会合并,一个类只会存在一个虚函数表和一个虚函数表指针(同个类的对象共享),当然也不会有虚基类表和虚基类表指针的存在。
但如果是钻石继承,那么是会存在两份虚函数表和两份虚函数表指针的。

参考:

《深入探索C++对象模型》

C++ primer 第四版
Effective C++ 3rd
C++编程规范

对C++对象内存模型造成的影响(类/对象的大小)的更多相关文章

  1. 从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响

    首先重新回顾一下关于类/对象大小的计算原则: 类大小计算遵循结构体对齐原则 第一个数据成员放在offset为0的位置 其它成员对齐至min(sizeof(member),#pragma pack(n) ...

  2. C++/C#中堆栈、对象内存模型、深浅拷贝、Array.Clone方法

    转载自:http://blog.csdn.net/jarvischu/article/details/6425534 目录 1.      C++/C#中对象内存模型................. ...

  3. C#的对象内存模型

    转载自:http://www.cnblogs.com/alana/archive/2012/07/05/2577893.html C#的对象内存模型: 一.栈内存和堆内存1.栈内存 由编译器自动分配和 ...

  4. (转)c#对象内存模型

    对象内存模型 C#的对象内存模型写这篇博客的主要目的是为了加深自己的理解,如有不对的地方,请各位见谅. C#的对象内存模型: 一.栈内存和堆内存1.栈内存 由编译器自动分配和释放,主要用来保存一些局部 ...

  5. Swift 对象内存模型探究(一)

    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/zIkB9KnAt1YPWGOOwyqY3Q 作者:王 ...

  6. Objective-C类成员变量深度剖析--oc对象内存模型

    目录 Non Fragile ivars 为什么Non Fragile ivars很关键 如何寻址类成员变量 真正的“如何寻址类成员变量” Non Fragile ivars布局调整 为什么Objec ...

  7. C++对象内存模型2 (虚函数,虚指针,虚函数表)

    从例子入手,考察如下带有虚函数的类的对象内存模型: class A { public: virtual void vfunc1(); virtual void vfunc2(); void func1 ...

  8. C++对象内存模型1(堆栈模型)

    对象内存模型 一. 栈(Stack) VS. 堆(heap) 栈 由系统自动管理,以执行函数为单位 空间大小编译时确定(参数+局部变量) 函数执行时,系统自动分配一个stack 函数执行结束时,系统立 ...

  9. C++对象内存模型1(堆栈模型)(转)

    对象内存模型 一. 栈(Stack) VS. 堆(heap) 栈 由系统自动管理,以执行函数为单位 空间大小编译时确定(参数+局部变量) 函数执行时,系统自动分配一个stack 函数执行结束时,系统立 ...

随机推荐

  1. C#利用NPOI在同一个Excel文件中创建多个sheet

    借用NPOI来实现,要在同一Excel文件中创建多个sheet,只需要在同一个workbook中创建多个sheet即可.要注意的是,sheet的名字一定不能重复.下面是实现的代码: private v ...

  2. 如何解决…has been modified since the precompiled header… was built的问题

    如何解决…has been modified since the precompiled header… was built 的问题 xcode5.1在程序中报错: File '/Applicatio ...

  3. java Integer包装类装箱的一个细节

    原文:https://www.cnblogs.com/JackPn/p/9392145.html java有八个基本数据类型,每个都有对应的一个包装类,比如int对应的Integer.从jdk1.5开 ...

  4. 使用PowerDesigner建立数据库模型【转】

    1. 打开PowerDesigner,点击File->New 2. 选择Conceptual Data Model,并修改Model name. 3.  在Palette工具栏中点击Entity ...

  5. 挑战黑客极限:Pwn2Own 2015成史上“最难”黑客大赛

    Pwn2Own是全球最著名.奖金最丰厚的黑客大赛,由美国五角大楼入侵防护系统供应商TippingPoint赞助.近日Pwn2Own 2015公布全新的比赛规则,本届赛事难度超高.史无前例,包括VUPE ...

  6. 网络驱动移植之解析Linux网络驱动的基本框架

    内核源码:linux-2.6.38.8.tar.bz2 概括而言,编写Linux网络驱动其实只要完成两件事即可,一是分配并初始化网络设备,二是注册网络设备. 1.分配并初始化网络设备 动态分配网络设备 ...

  7. CUDA,cudnn一些常见版本问题

    - 最好的方法是官网说明: https://tensorflow.google.cn/install/source_windows Version Python version Compiler Bu ...

  8. Mysql优化与使用集锦

    MyISAM的读性能是比Innodb强 MyISAM的索引和数据是分开的,并且索引是有压缩的 Innodb是索引和数据是紧密捆绑的,没有使用压缩从而会造成Innodb比MyISAM体积庞大不小 MyI ...

  9. cannot resolve symbol AppCompatActivity 心得

    新建Active 默认用的AppCompatActivity竟然报错cannot resolve symbol AppCompatActivity,网上找了半天,最后在朋友的帮助下解决,记录下 一.导 ...

  10. windows curl ssl版本号编译

    编译curl-ssl版本号碰到非常多坑,这里记录一下.亲手測试,注意版本号号!! ! !. 1.下载  curl-7.43.0  libssh2-1.3.0  openssl-1.0.0s   Act ...