前言

很早以前就听人推荐了《深入理解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++底层进行了哪些优化措施呢?下面就一探究竟吧!

简单对象模型

顾名思义,简单对象模型相当简单。在这个模型里面,一个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++对象模型,可以推算出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!

C++对象模型的那些事儿之一:对象模型(上)的更多相关文章

  1. C++对象模型的那些事儿之五:NRV优化和初始化列表

    前言 在C++对象模型的那些事儿之四:拷贝构造函数中提到如果将一个对象作为函数参数或者返回值的时候,会调用拷贝构造函数,编译器是如何处理这些步骤,又会对其做哪些优化呢?本篇博客就为他家介绍一个编译器的 ...

  2. C++对象模型的那些事儿之四:拷贝构造函数

    前言 对于一个没有实例化的空类,编译器不会给它默认生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数.这类函数包括一下几个: 构造函数 拷贝构造函数 析构函数 赋值运算符 在上一篇博文C ...

  3. C++对象模型的那些事儿之三:默认构造函数

    前言 继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数.对于C++的初学者来说,有如下两个误解: 任何class如果没有定义default constructor ...

  4. C++对象模型的那些事儿之二:对象模型(下)

    前言 上一篇博客C++对象模型的那些事儿之一为大家讲解了C++对象模型的一些基本知识,可是C++的继承,多态这些特性如何体现在对象模型上呢?单继承.多重继承和虚继承后内存布局上又有哪些变化呢?多态真正 ...

  5. C++对象模型的那些事儿之六:成员函数调用方式

    前言 C++的成员函数分为静态函数.非静态函数和虚函数三种,在本系列文章中,多处提到static和non-static不影响对象占用的内存,而虚函数需要引入虚指针,所以需要调整对象的内存布局.既然已经 ...

  6. C++对象模型那点事儿(布局篇)

    1 前言 在C++中类的数据成员有两种:static和nonstatic.类的函数成员由三种:static,nonstatic和virtual. 上篇我们尽量说一些宏观上的东西,数据成员与函数成员在类 ...

  7. 《深度探索c++对象模型》chapter1关于对象对象模型

    在c++中,有2种class data member:static和nostatic,以及3钟class member function:static,nostatic和virtual.已知下面这个c ...

  8. C++对象模型5--多继承下的对象模型

    C++对象模型中加入多继承 从单继承可以知道,派生类中只是扩充了基类的虚函数表.如果是多继承的话,又是如何扩充的? 1)        每个基类都有自己的虚表. 2)        子类的成员函数被放 ...

  9. DOM LEVEL 1 中的那些事儿[总结篇-上]

    DOM是前端编程中一个非常重要的部分,我们在动态修改页面的样式.内容.添加页面动画以及为页面元素绑定事件时,本质都是在操作DOM.DOM并不是JS语言的一个部分,我们通过JAVA.PHP等语言抓取网页 ...

随机推荐

  1. poj 2318 叉积+二分

    TOYS Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 13262   Accepted: 6412 Description ...

  2. Linux查看日志方法总结(1)

    注:日志文件为:test.log 1.tail -f test.log 查看当前打印的日志(平时就知道这方法!打印出的长度有限制.) 以下为网上搜集的: 2.先必须了解两个最基本的命令: tail  ...

  3. Java连接FTP成功,但是上传是失败,报错:Connected time out

    Java代码在本机上传文件到FTP服务器的时候成功,但是部署到测试服务器的时候出现,连接FTP成功但是上传失败,并且报Connected time out错误: 测试服务器和FTP服务都在阿里云上:( ...

  4. python,for,while循环控制

    1.for循环 for循环 for i in range(0,5): for j in range(0,5): print('#'*5) 2.while 循环 import random #get n ...

  5. Qone 正式开源,使 javascript 支持 .NET LINQ

    Qone 下一代 Web 查询语言,使 javascript 支持 LINQ Github: https://github.com/dntzhang/qone 缘由 最近刚好修改了腾讯文档 Excel ...

  6. Padding Oracle攻击

    最近在复现LCTF2017的一道题目,里面有一个padding oracle攻击,也算是CBC翻转攻击,这个攻击主要针对CBC加密模式的 网上有关这个攻击的博客文章很多,但是其中有一些细节可能是个人的 ...

  7. 浅谈Java中的equals和==与hashCode

    转载:https://www.cnblogs.com/dolphin0520/p/3592500.html 参考:http://blog.csdn.net/yinzhijiezhan/article/ ...

  8. Java获取随机数的3种方法

    最小值---最大值(整数)的随机数     方法1 (数据类型)(最小值+Math.random()*(最大值-最小值+1)) 例: (int)(1+Math.random()*(10-1+1)) / ...

  9. Java并发中的CopyOnWrite容器

    Copy-On-Write简称COW,是一种用于程序设计中的优化策略.其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改, ...

  10. MySQL数据类型DECIMAL用法

    MySQL DECIMAL数据类型用于在数据库中存储精确的数值.我们经常将DECIMAL数据类型用于保留准确精确度的列,例如会计系统中的货币数据. 要定义数据类型为DECIMAL的列,请使用以下语法: ...