C++对象模型的那些事儿之一:对象模型(上)

来源:互联网 发布:淘宝买家恶意填错地址 编辑:程序博客网 时间:2024/06/05 20:34

前言

很早以前就听人推荐了《深入理解C++对象模型》这本书,从年初买来到现在也只是偶尔翻了翻,总觉得晦涩难懂,放在实验室上吃灰吃了好久。近期由于找工作对C++的知识做了一个全面系统的学习,基础相对扎实了不少,于是,又重新拿起这本书,突然觉得里面的知识也不那么难懂,而且越看越有意思,不愧是C++高阶教程啊!耐着性子,抓着头皮花了两个多月,总算对其中的知识有了一些理解,部分章节反反复复的看,每次都有新的收获。所谓好记性不如烂笔头,本系列博文就对我所学到的知识和我所遇到的困惑做一个整理。

引例

我以一个简单的例子来开始本篇博文,这个例子也会贯穿整篇博文,让大家一步一步对C++对象模型有一个全面的了解。

假设此时需要设计一个Animal类,包含动物名,体重和一些常见行为,设计如下:

class Animal{    Animal(){}    ~Animal(){}    char name[10];//动物名字    int weight;//体重    virtual void eat(){};//动物都需要吃,所以将eat设为虚函数,方便后面继承    virtual void sleep(){};//同上}

设计者很关注的一个问题就是,封装的布局成本,也就是这个类会占有多大的空间。于是,我很自然的运行了如下程序。

Animal animal;cout<<sizeof(animal)<<endl;//输出24(注:本测试机为ubuntu15.10,64位操作系统)

那么,为什么会输出24呢?下面就一一为大家分析和讲解。

常见对象模型

C++的成员

在C++中,主要有两类成员,分别是数据成员和成员函数。

数据成员有静态和非静态之分;成员函数有静态,非静态和虚函数之分。

C++成员

那么,这些成员在内存中时怎么布局的呢?为了考虑布局成本,C++底层进行了哪些优化措施呢?下面就一探究竟吧!

简单对象模型

顾名思义,简单对象模型相当简单。在这个模型里面,一个object是一系列的slots,每一个slot指向一个members,members按其声明顺序,各被指定一个slot。每一个data member和member function都有自己的slot。

这么设计的原因可能时为了尽量降低C++编译器的设计复杂度而开发出来的,但是在空间和执行器的效率就大打折扣了!在这个对象模型中,members本身不放在object中,只有”指向member的指针“采访在object中,避免不同类型拥有不同存储空间而招致的差异,而且也有利于计算每个class的内存占用大小。

简单对象模型

表格驱动对象模型

本节开始就讲到C++的成员包括了数据成员和成员函数,表格驱动模型就是以此来划分,在这个模型中,object内含指向两个表格的指针,Members funtion table是一系列的slots,每一个slots指向一个成员函数;Data member table则直接持有data本身。

简单对象模型

C++对象模型

在简单对象模型中提到了”指向成员的指针“的观念,在表格驱动对象模型中提到了member function table的观念,上述两个模型都没有用到实际的C++编译器中,但是这两个观念却被用到了C++对象模型中。

在此模型中,对于data members处理如下:

  • nonstatic data members:被配置于class object中
  • static data members:存放在class object之外

对function members处理如下:

  • static和nonstatic function members:存放在class object之外
  • virtual function members: 首先对class里的每个虚函数产生一个指针,放在一个virtual table(vtbl)中,然后在class object里面安置一个指针,指向上述的virtual table,这个指针称为vptr。vptr的设定和重置都有每个类的构造函数,析构函数和拷贝构造函数自动完成。

C++对象模型

内存布局

下面我们来看看引例中留下的问题。依据上图给出的C++对象模型,可以推算出animal类所占用的内存

  • 指向虚表的指针vptr占用8个字节
  • 非静态数据成员name占用10个字节
  • 非静态数据成员weight占用4个字节

这样,算出的结果是22个字节,为什么正确结果是24个字节呢?

于是又引出了一个问题,C++ class object需要多少内存才能表现出来呢?

  • 其nonstatic data members的总和大小
  • 加上任何由于alignment的需求而填补上去的空间
  • 加上为了支持virtual而由内部产生的任何额外负担

对比一下animal的各个成员的内存消耗,可以看出,忽略了内存对齐而带来的内存消耗。由于是64位操作系统,所以以8字节对齐,于是可以很容易的算出最后整个animal类占用的内存为24个字节。

测试小结

讲到这里,似乎还是不能理解C++对象底层的布局。这一切都是以概念为主,没有深究到底层。

于是,我写了如下的测试代码,让我们一起去探究一下整个C++对象的底层布局。

#include <stdio.h>#include <iostream>#include <string.h>using namespace std;typedef void(*Fun)(void);class Animal{public:    char name[10];//动物名字    int weight;//体重    virtual void eat();    virtual void sleep();};void Animal::eat(){//eat函数的实现    cout<<"Please let me eat"<<endl;}void Animal::sleep(){//sleep函数的实现    cout<<"Please let me sleep"<<endl;}int main(){    Animal animal;    strcpy(animal.name,"hello");    animal.weight = 10;    cout<<"虚指针vptr的地址:"<<&animal<<endl;//虚指针vptr的地址    for (int i = 0; i < 10; ++i)    {        cout<<"name["<<i<<"]的地址为:"<<(long long *)&(animal.name[i])<<endl;//name每个参数的地址    }    cout<<"weight的地址为:"<<&(animal.weight)<<endl;//weight的地址    cout<<"虚表的地址:"<<(long long *)(*((long long*)&animal))<<endl;    Fun pfun1 = NULL;    Fun pfun2 = NULL;    pfun1 = (Fun)*((long long*)*(long long*)(&animal));//通过强制转换,验证虚函数的地址是否正确    pfun1();    pfun2 = (Fun)*((long long*)*(long long*)(&animal)+1);//通过强制转换,验证虚函数的地址是否正确    pfun2();    return 0;}

由于我的测试机为64位操作系统,所以指针类型必须强制转换为long long*,各位如果是32位或者VS上32位程序的,记得将此改为int*。

上述测试案例输出结果如下:

虚指针vptr的地址:0x7ffe378125e0name[0]的地址为:0x7ffe378125e8name[1]的地址为:0x7ffe378125e9name[2]的地址为:0x7ffe378125eaname[3]的地址为:0x7ffe378125ebname[4]的地址为:0x7ffe378125ecname[5]的地址为:0x7ffe378125edname[6]的地址为:0x7ffe378125eename[7]的地址为:0x7ffe378125efname[8]的地址为:0x7ffe378125f0name[9]的地址为:0x7ffe378125f1weight的地址为:0x7ffe378125f4虚表的地址:0x400d58Please let me eatPlease let me sleep

分析结果之前,先解释一下为什么64位操作系统的指针是48位,因为现在的硬件还用不到完整的64位寻址,所以硬件也没必要支持那么多位的地址。(也有可能时我的机子太老了,囧!)

上述问题不影响我们分析结果,从输出的地址可以看出

  • 虚指针在animal object内存布局的最前面,占用8个字节
  • 往下依次是name数组的十个元素,为了内存对齐,这里填补了2个字节的空隙
  • 最后就是weight占用的8个字节

为了验证虚表一定存在在对象布局的最前面,我首先利用(long long *)(*((long long*)&animal))强制内存转换取出了虚表的地址,然后定义一个函数指针typedef void(*Fun)(void),指向虚表的第一位,再调用pfun()来验证输出Please let me eat,结果也如预料的一样。

接下来,又以同样的方式验证了sleep()函数,同样输出Please let me sleep,结果符合预期!

结束语

本篇博客简单得带大家了解了一下C++的内存布局,以一个小的例子来剖析和验证了此模型的正确性。

下篇博客将带大家继续深入剖析C++的内存布局,主要讲解引入继承关系后的C++内存布局,以及C++多态的底层实现原理,敬请期待!

About Me

由于本人也是初学,在写作过程中,难免有错误的地方,读者如果发现,请在下面留言指出。

最后,如有疑惑或需要讨论的地方,可以联系我,联系方式见我的个人博客about页面,地址:About Me。

另外,本人的第一本gitbook书已整理完,关于leetcode刷题题解的,点此进入One day One Leetcode

欢迎持续关注!Thx!

2 0
原创粉丝点击