很早以前就听人推荐了《深入理解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)<//输出24(注:本测试机为ubuntu15.10,64位操作系统)
那么,为什么会输出24呢?下面就一一为大家分析和讲解。
在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本身。
在简单对象模型中提到了”指向成员的指针“的观念,在表格驱动对象模型中提到了member function table的观念,上述两个模型都没有用到实际的C++编译器中,但是这两个观念却被用到了C++对象模型中。
在此模型中,对于data members处理如下:
对function members处理如下:
下面我们来看看引例中留下的问题。依据上图给出的C++对象模型,可以推算出animal类所占用的内存
这样,算出的结果是22个字节,为什么正确结果是24个字节呢?
于是又引出了一个问题,C++ class object需要多少内存才能表现出来呢?
对比一下animal的各个成员的内存消耗,可以看出,忽略了内存对齐而带来的内存消耗。由于是64位操作系统,所以以8字节对齐,于是可以很容易的算出最后整个animal类占用的内存为24个字节。
讲到这里,似乎还是不能理解C++对象底层的布局。这一切都是以概念为主,没有深究到底层。
于是,我写了如下的测试代码,让我们一起去探究一下整个C++对象的底层布局。
#include
#include
#include
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"<void Animal::sleep(){//sleep函数的实现
cout<<"Please let me sleep"<int main(){
Animal animal;
strcpy(animal.name,"hello");
animal.weight = 10;
cout<<"虚指针vptr的地址:"<<&animal<//虚指针vptr的地址
for (int i = 0; i < 10; ++i)
{
cout<<"name["<"]的地址为:"<<(long long *)&(animal.name[i])<//name每个参数的地址
}
cout<<"weight的地址为:"<<&(animal.weight)<//weight的地址
cout<<"虚表的地址:"<<(long long *)(*((long long*)&animal))<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位寻址,所以硬件也没必要支持那么多位的地址。(也有可能时我的机子太老了,囧!)
上述问题不影响我们分析结果,从输出的地址可以看出
为了验证虚表一定存在在对象布局的最前面,我首先利用(long long *)(*((long long*)&animal))
强制内存转换取出了虚表的地址,然后定义一个函数指针typedef void(*Fun)(void)
,指向虚表的第一位,再调用pfun()
来验证输出Please let me eat
,结果也如预料的一样。
接下来,又以同样的方式验证了sleep()
函数,同样输出Please let me sleep
,结果符合预期!
本篇博客简单得带大家了解了一下C++的内存布局,以一个小的例子来剖析和验证了此模型的正确性。
下篇博客将带大家继续深入剖析C++的内存布局,主要讲解引入继承关系后的C++内存布局,以及C++多态的底层实现原理,敬请期待!
由于本人也是初学,在写作过程中,难免有错误的地方,读者如果发现,请在下面留言指出。
最后,如有疑惑或需要讨论的地方,可以联系我,联系方式见我的个人博客about页面,地址:About Me。
另外,本人的第一本gitbook书已整理完,关于leetcode刷题题解的,点此进入One day One Leetcode
欢迎持续关注!Thx!