【C++ Primer | 15】C++类内存分布
C++类内存分布
书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承、虚函数存在的情况下。
下面可以定义一个类,像下面这样:
class Base
{
int a;
int b;
public:
void CommonFunction();
};
然后编译一下,可以看到输出框里面有这样的排布:

这里不想花精力在内存对齐因素上,所以成员变量都设为int型。
从这里可以看到普通类的排布方式,成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。
2. 再看下继承,往后面添加如下代码:
class Base
{
int a;
int b;
public:
void CommonFunction();
}; class DerivedClass: public Base
{
int c;
public:
void DerivedCommonFunction();
};
编译,然后看到如下的内存分布(父类的内存分布不变,这里只讨论子类成员变量的内存分布):

可以看到子类继承了父类的成员变量,在内存排布上,先是排布了父类的成员变量,接着排布子类的成员变量,同样,成员函数不占字节。
下面给基类加上虚函数,暂时注释掉DerivedClass,看一下这时的内存排布:
class Base
{
int a;
int b;
public:
void CommonFunction();
void virtual VirtualFunction();
};

这个内存结构图分成了两个部分,上面是内存分布,下面是虚表,我们逐个看。VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;下面生成了虚表,紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布,下面列出了虚函数,左侧的0是这个虚函数的序号,这里只有一个虚函数,所以只有一项,如果有多个虚函数,会有序号为1,为2的虚函数列出来。
编译器是在构造函数出创建一个对象时创建这个虚表指针以及虚表的。
那么编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。
所以,如果是调用Base *p = new Derived();生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived*到Base*的转换并没有改变虚表指针,所以这时候p->VirtualFunction,实际上是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类的VirtualFunction,所以调用的是子类的虚函数,这就是多态了。
3. 下面加上子类,并在子类中添加虚函数,像下面这样:
class Base
{
int a;
int b;
public:
void CommonFunction();
void virtual VirtualFunction();
}; class DerivedClass: public Base
{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};
可以看到子类内存的排布如下:

上半部是内存分布,可以看到,虚表指针被继承了,且仍位于内存排布的起始处,下面是父类的成员变量a和b,最后是子类的成员变量c,注意虚表指针只有一个,子类并没有再生成虚表指针了;下半部的虚表情况与父类是一样的。
4. 我们把子类换个代码,像这样:
class Base
{
int a;
int b;
public:
void CommonFunction();
void virtual VirtualFunction();
}; class DerivedClass1 : public Base
{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction2();
};
注意到这时我们并没有覆写父类的虚方法,而是重声明了一个新的子类虚方法,内存分布如下:

还是只有一个虚表指针,但是下方虚表的内容变化了,虚表的0号是父类的VirtualFunction,而1号放的是子类的VirtualFunction2。也就是说,如果定义了DerivedClass的对象,那么在构造时,虚表指针就会指向这个虚表,以后如果调用的是VirtualFunction,那么会从父类中寻找对应的虚函数,如果调用的是VirtualFunction2,那么会从子类中寻找对应的虚函数。
5. 我们再改造一下子类,像这样:
class Base
{
int a;
int b;
public:
void CommonFunction();
void virtual VirtualFunction();
}; class DerivedClass1 : public Base
{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
void virtual VirtualFunction2();
};
我们既覆写父类的虚函数,也有新添的虚函数,那么可以料想的到,是下面的这种内存分布:

多继承子类的内存布局
多继承子类的内存布局
下面讨论多继承派生类,代码如下:
class A
{
public:
int dataA;
}; class B : public A
{
public:
int dataB;
}; class C : public A
{
public:
int dataC;
}; class D : public B, public C
{
public:
int dataD;
};
内存布局如下:

为了跟后文加以比较,我们再来看看B和C的内存布局:

下面来讨论多重继承,代码如下:
class Base
{
int a;
int b;
public:
void CommonFunction();
void virtual VirtualFunction();
}; class DerivedClass1: public Base
{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
}; class DerivedClass2 : public Base
{
int d;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
}; class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
{
int e;
public:
void DerivedDerivedCommonFunction();
void virtual VirtualFunction();
};
内存分布从父类到子类,依次如下:

Base中有一个虚表指针,地址偏移为0

DerivedClass1继承了Base,内存排布是先父类后子类。

下面我们重点看看这个类DerivedDerivedClass,由外向内看,它并列地排布着继承而来的两个父类DerivedClass1与DerivedClass2,还有自身的成员变量e。DerivedClass1包含了它的成员变量c,以及Base,Base有一个0地址偏移的虚表指针,然后是成员变量a和b;DerivedClass2的内存排布类似于DerivedClass1,注意到DerivedClass2里面竟然也有一份Base。

里有两份虚表了,分别针对DerivedClass1与DerivedClass2,在&DerivedDericedClass_meta下方的数字是首地址偏移量,靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}在DerivedDerivedClass的内存偏移。
虚继承内存布局
class A
{
public:
int dataA;
}; class B : virtual public A
{
public:
int dataB;
}; class C : virtual public A
{
public:
int dataC;
}; class D : public B, public C
{
public:
int dataD;
};
我们看看B、C、D类的内存布局情况:
class A
{
public:
int dataA;
}; class B : virtual public A
{
public:
int dataB;
}; class C : virtual public A
{
public:
int dataC;
}; class D : public B, public C
{
public:
int dataD;
};
B、C的内存布局:

D的内存布局:

我们可以看到,菱形继承体系中的子类在内存布局上和普通多继承体系中的子类类有很大的不一样。对于类B和C,sizeof的值变成了12,除了包含类A的成员变量dataA外还多了一个指针vbptr,类D除了继承B、C各自的成员变量dataB、dataA和自己的成员变量外,还有两个分别属于B、C的指针。
那么类D对象的内存布局就变成如下的样子:
- vbptr:继承自父类B中的指针
- int dataB:继承自父类B的成员变量
- vbptr:继承自父类C的指针
- int dataC:继承自父类C的成员变量
- int dataD:D自己的成员变量
- int A:继承自父类A的成员变量
显然,虚继承之所以能够实现在多重派生子类中只保存一份共有基类的拷贝,关键在于vbptr指针。那vbptr到底指的是什么?又是如何实现虚继承的呢?其实上面的类D内存布局图中已经给出答案:

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中,第一项记录了vbptr与本类的偏移地址;第二项是vbptr到共有基类元素之间的偏移量。在这个例子中,类B中的vbptr指向了虚表D::$vbtable@B@,虚表表明公共基类A的成员变量dataA距离类B开始处的位移为20,这样就找到了成员变量dataA,而虚继承也不用像普通多继承那样维持着公共基类的两份同样的拷贝,节省了存储空间。
参考资料
【C++ Primer | 15】C++类内存分布的更多相关文章
- 【转】C++类内存分布
C++类内存分布 https://www.cnblogs.com/jerry19880126/p/3616999.html 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看 ...
- 【转】C++类-内存分布
C++类内存分布 - 转载自Jerry19880126 - 博客园 的文章 在上面这篇文章的基础上做了些整理. 主要讨论了C++类对象的内存分布结构. 来看看编译器是怎么处理类成员内存分布的,特别是在 ...
- 记录:C++类内存分布(虚继承与虚函数)
工具:VS2013 先说一下VS环境下查看类内存分布的方法: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存 ...
- C++类内存分布
http://www.cnblogs.com/jerry19880126/p/3616999.html#undefined 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看 ...
- 转载:C++类内存分布
本文转自:http://www.cnblogs.com/jerry19880126/p/3616999.html,原文写的非常好,从中学到了虚继承的概念,也学会了用VS查看内存分布. 说下C++内存分 ...
- c++类内存分布解析
首先使用Visual Studio工具查看类的内存分布,如下: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内 ...
- 使用Visual Studio查看C++类内存分布
书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承.虚函数存在的情况下. 工欲善其事,必先利其器,我们先用好Visual Stu ...
- C++浅析——继承类内存分布和虚析构函数
继承类研究 1. Code 1.1 Cbase, CTEST为基类,CTest2为其继承类,并重新申明了基类中的同名变量 class CBase { public: int Data; CBase() ...
- C++ 类的内存分布
C++类内存分布 转自:http://www.cnblogs.com/jerry19880126/p/3616999.html 先写下总结,通过总结下面的例子,你就会明白总结了. 下面总结一下: ...
随机推荐
- 在tomcat集群环境下redis实现分布式锁
上篇介绍了redis在集群环境下如何解决session共享的问题.今天来讲一下如何解决分布式锁的问题 什么是分布式锁? 分布式锁就是在多个服务器中,都来争夺某一资源.这时候我们肯定需要一把锁是不是 , ...
- 史上最全 40 道 Dubbo 面试题及答案
https://blog.csdn.net/zl1zl2zl3/article/details/83721147
- Multi-Nim游戏结论不变证明
Nim取石子游戏结论: 若n堆石子的异或和为0,则先手必胜:否则,先手必败 加入新规则: 每次取完石子后,可以将取的那一堆的石子 分为多堆,也可以不分 结论: 同Nim取石子游戏结论 证明: 如果异或 ...
- ThinkPHP 3.2公共类库、应用类库ThinkPHP/Library讲解
一.ThinkPHP的类库主要包括公共类库和应用类库,都是基于命名空间进行定义和扩展的.只要按照规范定义,都可以实现自动加载. 公共类库 公共类库通常是指ThinkPHP/Library ...
- .Net进阶系列(14)-异步多线程(async和await)(被替换)
1. 方法名前只有async,但是方法中Task实例前没有await关键字,该方法和普通方法没有什么区别,但是会报一个警告. #region 情况一 /// <summary> /// ...
- BZOJ3527 [Zjoi2014]力 【fft】
题目 给出n个数qi,给出Fj的定义如下: 令Ei=Fi/qi,求Ei. 输入格式 第一行一个整数n. 接下来n行每行输入一个数,第i行表示qi. 输出格式 n行,第i行输出Ei.与标准答案误差不超过 ...
- [C++]数组指针与指针数组
//声明: 1.&----取地址运算符 eg: int m = 1; int *p = &m;//(*p) == m的地址 == &m; 2.*----间接访问运算符 eg: ...
- 理解django框架中的MTV与MVC模式
1.Models:一个抽象层,用来构建和操作你的web应用中的数据,模型是你的数据的唯一的.权威的信息源.它包含你所储存数据的必要字段和行为.通常,每个模型对应数据库中唯一的一张表. from dja ...
- python 新式类的 __getattribute__
这个方法定义在object中,所以所有的新式类都继承有该方法,所有的新式类的实例在获取属性value的时候都会调用该方法,为了验证这一结论,我们重写一下该方法: class C(object): a ...
- saltstack系列~第一篇
一 简介:从今天开始学习saltstack 二 salt的认证系列操作 1 原理 saltstack通过/etc/salt/pki/目录下面的配置文件的密钥进行通信,master端接受minion端后 ...