只说C++对象模型在内存中如何分配这是不现实的,所以这里选择VS 2013作为调试环境具体探讨object在内存中分配情况.目录给出了具体要探讨的所有模型,正文分标题依次讨论.水平有限,如有错误之处请多包涵如若能及时反馈于我请接受我的谢意.

目录

  1. 简单对象模型
  2. 单继承对象模型
  3. 多继承对象模型
  4. 菱形多继承对象模型
  5. 虚单继承对象模型
  6. 虚多继承对象模型
  7. 菱形虚多继承对象模型

简单对象模型

首先给出具体的模型和类的代码,然后我们会验证模型是否正确:) 

class base {
public:
base() :baseData(5) {}
virtual ~base() {}
int baseFunc() { std::cout << "base::baseFunc"; return 0; }
static int sBaseFunc() { std::cout << "base::sBaseFunc"; return 0; }
virtual int vfunc() { std::cout << "base::vfunc()"; return 0; }
private:
int baseData;
static int sBaseData;
};

这个简单的类完备包含了静态类成员函数,类成员函数,类数据成员,静态类数据成员,虚函数.我们可以注意到base类中所有成员函数(指非静态成员函数, 下文同),静态成员函数和静态数据成员都存在于对象内存之外,也就是定义一个对象不会有额外的开销来保存这些内容,这也符合我们的常识.所以这个简单的对象在内存中主要表现为存储非静态数据成员和虚函数.更具体而言一个对象会保存非静态数据成员和一个指向虚函数表的指针(vfptr,如果有虚函数的话),我们常说C++的编译器会偷偷做很多事情这里便是一个例子,这里的vfptr会被编译器在合适的地方安插进代码,这个合适的地方通常就是default
constructor(如果没有显式声明default constructor编译器会合成一个(当然这也得视情况而定),不过这个不再讨论范围内); 下面是详细的验证,

int main() {
base b;
//在vs的编译器实现中虚函数表指针放在对象抵首位,所以&b也就相当于取vfptr地址;
int * vfptr = (int *)(&b);
//给virtual int vfunc()别名一个函数指针简化代码
using vfuncType = int(*)();
//指向虚函数表第一项的指针
int *vtablePtr = (int*)(*vfptr);
//vtablePtr+1表示获取虚函数vfunc的地址,因为直接的vtablePtr是指向析构函数
vfuncType vfunc= vfuncType(*(vtablePtr + 1));
//调用虚函数,输出base::vfunc()
(*vfunc)(); //输出5
int *dataPtr = (int *)(&b) + 1;
std::cout << *dataPtr;
system("pause");
}

值得注意的是vtablePtr-1就是指向type_info,而type_info主要用以支持RTTI,与主题相差较远这里不做赘述.

单继承对象模型

只要明白了简单对象模型接来的单/多继承也就变得很简单了.

class derived : public base {
public:
derived() :derivedData(10) {}
virtual int vfunc() { std::cout << "derived::vfunc()"; return 0; }
virtual void newVF() { std::cout << "derived::newVF();"; }
private:
int derivedData;
};

可以看到这里x派生类中新增加的newVF虚函数被置于虚函数表最下方,然后如果派生类重写了虚函数就用派生类重写的版本替代基类的版本,其他顺序不变.同样给出验证:

int main() {
derived d;
int * vptf = (int *)(&d);
int * vfptr = (int *)(&d);
using vfuncType = int(*)();
using newVFType = void(*)();
int *vtablePtr = (int*)(*vfptr);
vfuncType vfunc = vfuncType(*(vtablePtr + 1));
newVFType nvfunc = newVFType(*(vtablePtr + 2));
(*vfunc)(); //derived::vfunc()
(*nvfunc)();//derived::newVF() int *baseDataPtr = (int *)(&d) + 1;
int *derivedDataPtr = (int *)(&d) + 2;
std::cout << *baseDataPtr<<*derivedDataPtr;
system("pause");
}

多继承对象模型

模型如下:

为了方便我们这里适当简化class

class base1 {
public:
base1() :base1Data(5) {}
virtual int vfunc() { std::cout << "base::vfunc()"; return 0; }
private:
int base1Data;
}; class base2 {
public:
base2() :base2Data(5) {}
virtual int vfunc() { std::cout << "base::vfunc()"; return 0; }
private:
int base2Data;
}; class derived : public base1, public base2 {
public:
derived() :derivedData(10) {}
virtual int vfunc() { std::cout << "derived::vfunc()"; return 0; }
virtual void newVF() { std::cout << "derived::newVF();"; }
private:
int derivedData;
};

图片基本上已经把我想说的说完了, 这里只需要注意一下

  • 派生类中新定义的虚函数会置于第一个虚函数表最下面而不是两个虚函数表都放置.
  • 基类数据成员会放置在指向该基类的虚函数表的指针下面,如果有必要之类还会进行内存alignment.

验证如下

int main() {
derived d;
//上帝原谅我这里用下划线
int *base1_vfptr = (int*)(&d);
int *base1_dataPtr = (int*)(&d) + 1;
int *base2_vfptr = (int*)(&d) + 2;
int *base2_dataPtr = (int*)(&d) + 3;
int *derived_dataPtr = (int*)(&d) + 4; int (*derived_vfunc)() = (int(*)())(*(int*)(*base1_vfptr));
void(*derived_newVF)() = (void(*)())(*((int*)(*base1_vfptr)+1));
int(*derived_vfunc1)() = (int(*)())(*(int*)(*base2_vfptr));
derived_vfunc();
derived_newVF();
derived_vfunc1();
std::cout << *base1_dataPtr << *base2_dataPtr << *derived_dataPtr;
system("pause");
}

菱形多继承对象模型

class root {
public:
root() :rootData(1) {}
virtual int vfunc() { std::cout << "root::vfunc()"; return 0; }
virtual void print() { std::cout << "root::print()"; }
private:
int rootData;
}; class base1 :public root {
public:
base1() :base1Data(2) {}
virtual int vfunc() { std::cout << "base::vfunc()"; return 0; }
private:
int base1Data;
}; class base2 :public root {
public:
base2() :base2Data(3) {}
virtual int vfunc() { std::cout << "base::vfunc()"; return 0; }
private:
int base2Data;
}; class derived : public base1, public base2 {
public:
derived() :derivedData(4) {}
virtual int vfunc() { std::cout << "derived::vfunc()"; return 0; }
virtual void newVF() { std::cout << "derived::newVF();"; }
private:
int derivedData;
};

这里我们root作为基类,然后base1 base2从root上派生出来,最后derived从base1 base2中派生出来实现一个菱形继承.我们看到上图第一个应该关注的是有两个rootData和root::print(),这对于追求效率的C艹是无法容忍的,所以后面引出虚继承解决这个问题(以便引出另一些问题:( ).至于为什么有两个稍微想一下就能明白,在单继承下派生类内存模型会储存基类的数据成员和虚函数,所以这里base1和base2分别储存了rootData和print(),最后derived的多重继承把每个相对于它而言的基类一块一块的放入内存,之所以说是一块一块是因为内存不会把vfptr放一块然后数据成员放一块而是像之前提及的分块处理.

未完待续.

探索C++对象模型的更多相关文章

  1. 读书笔记《深度探索c++对象模型》 概述

    <深度探索c++对象模型>这本书是我工作一段时间后想更深入了解C++的底层实现知识,如内存布局.模型.内存大小.继承.虚函数表等而阅读的:此外在很多面试或者工作中,对底层的知识的足够了解也 ...

  2. 柔性数组-读《深度探索C++对象模型》有感 (转载)

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  3. 柔性数组-读《深度探索C++对象模型》有感

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  4. [读书系列] 深度探索C++对象模型 初读

    2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...

  5. 拾遗与填坑《深度探索C++对象模型》3.3节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  6. 拾遗与填坑《深度探索C++对象模型》3.2节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  7. 深度探索C++对象模型

    深度探索C++对象模型 什么是C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各个支持的底层实现机制. 抽象性与实际性之间找出平衡点, 需要知识, 经验以及许多思考. 导读 这本书是C+ ...

  8. 《深度探索C++对象模型》读书笔记(一)

    前言 今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++.操作系统.计算机网络.算法和数据结构等.C++就先从这本<深度探索C++对象模型>开始.不同于& ...

  9. C++的黑科技(深入探索C++对象模型)

    周二面了腾讯,之前只投了TST内推,貌似就是TST面试了 其中有一个问题,“如何产生一个不能被继承的类”,这道题我反反复复只想到,将父类的构造函数私有,让子类不能调用,最后归结出一个单例模式,但面试官 ...

  10. 深入探索C++对象模型(一)

    再读<深入探索C++对象模型>笔记. 关于对象 C++在加入封装后(只含有数据成员和普通成员函数)的布局成本增加了多少? 答案是并没有增加布局成本.就像C struct一样,memeber ...

随机推荐

  1. javascript执行机制

    文的目的就是要保证你彻底弄懂javascript的执行机制,如果读完本文还不懂,可以揍我. 不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的 ...

  2. 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分

    这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...

  3. 企业级缓存系统varnish应用

    场景 随着公司业务快速发展,公司的电子商务平台已经聚集了很多的忠实粉丝,公司也拿到了投资,这时老板想通过一场类似双十一的活动,进行一场大的促销,届时会有非常多的粉丝访问网站,你的总监与市场部门开完会后 ...

  4. Spring MVC 学习总结(九)——Spring MVC实现RESTful与JSON(Spring MVC为前端提供服务)

    很多时候前端都需要调用后台服务实现交互功能,常见的数据交换格式多是JSON或XML,这里主要讲解Spring MVC为前端提供JSON格式的数据并实现与前台交互.RESTful则是一种软件架构风格.设 ...

  5. mysql 索引类型

    根据类型分为普通索引2种类型,hash 和b-tree   最常用    hash是按一对一索引的.速度 最快但不支持范围 比如where name = 'dd' 最快.但是使用 date >3 ...

  6. 通过Nutch扩展点开发插件(添加自定义索引字段到solr)

    爬虫系统:通过Nutch扩展点开发插件(添加自定义索引字段到solr) 准备工作 爬虫环境 -- nutch2.3.1 + solr4.10.3 + hbase0.98 开发环境 -- Eclipse ...

  7. 《跟我学IDEA》一、下载安装idea,设置背景字体编码,配置JDK

    写在前面的话:作为一个在IT界摸爬滚打6年+的老程序员,我属于会的东西多而杂,但是没有任何一样精通的.曾经自己也认真过,蹉跎过,最近和别的同事朋友聊天时,突然发现自己得到的东西却很少很少,于是想认真的 ...

  8. C语言简单实现链栈基本几个功能(适合新手看,大神可指正)

            接着上一次的顺序栈,今天我记一下链栈,因为我也是刚学不久,有些地方也稍稍理解不了,所以,一起共勉.我会用我自己结合教材上画的图,争取跟代码一起结合,用文字和图最大化的解释代码,这样的话 ...

  9. Markdown语法入门

    本文内容参考与这里,本篇文档,用Markdown语法写成. 概述 宗旨 Markdown 的目标是实现「易读易写」. 可读性,无论如何,都是最重要的.一份使用 Markdown 格式撰写的文件应该可以 ...

  10. 【剑指Offer学习】【面试题21:包括min 函数的栈】

    题目: 定义栈的数据结构,请在该类型中实现一个可以得到栈的最小素的min 函数.在该栈中.调用min.push 及pop的时间复杂度都是0(1) 解题思路: 把每次的最小元素(之前的最小元素和新压入战 ...