前言

很早以前就听人推荐了《深入理解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++成员

那么,这些成员在内存中时怎么布局的呢?为了考虑布局成本,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++对象模型

内存布局

下面我们来看看引例中留下的问题。依据上图给出的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的地址:0x7ffe378125e0
name[0]的地址为:0x7ffe378125e8
name[1]的地址为:0x7ffe378125e9
name[2]的地址为:0x7ffe378125ea
name[3]的地址为:0x7ffe378125eb
name[4]的地址为:0x7ffe378125ec
name[5]的地址为:0x7ffe378125ed
name[6]的地址为:0x7ffe378125ee
name[7]的地址为:0x7ffe378125ef
name[8]的地址为:0x7ffe378125f0
name[9]的地址为:0x7ffe378125f1
weight的地址为:0x7ffe378125f4
虚表的地址:0x400d58
Please let me eat
Please 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!