在许多笔试面试中都会涉及到sizeof 运算符的求值问题。

这类问题主要分四类:

  1. 基本数据类型,如int,bool,fload,long,long,int * 等,这一类比较简单,但要注意x86和x64情况下的指针大小
  2. 枚举 enum。这个类型网络上有说是1-4个byte,根据最大值决定的;也有说是sizeof(int)。我这边个人使用 visual studio 2015 获得的结果是4个byte
  3. struct 和 union 组合类型。union 是取其中一个最大成员的size作为其size;struct 则要考虑对齐填充因素
  4. class 类型,class 就稍微复杂点,不仅仅要考虑对齐填充因素,还要考虑继承,虚继承,虚函数等因素。

下文主要讲述class 的内存布局,稍带介绍一下struct 的size。

struct 的内存布局:

struct 的内存对齐和填充概念学过C 的都应该知道一点。其实只要记住一个概念和三个原则就可以了:

一个概念:

  自然对齐:如果一个变量的内存地址正好位于它长度的整数倍,就被称做自然对齐。

  如果不自然对齐,会带来CPU存取数据时的性能损失。(PS:具体应该与CPU通过总线读写内存数据的细节相关,具体没有细究)

三个原则:

  1. struct 的起始地址需要能够被其成员中最宽的基本数据类型整除;
  2. struct 的size 也必须能够被其成员中最宽的基本数据类型整除;
  3. struct 中每个成员地址相对于struct 的起始地址的offset,必须是自然对齐的。

Class 的内存布局:

在学习C++ 的class 的内存布局前,先介绍下文会被用到的Visual studio 中的编译选项"/d1reportAllClassLayout" 和 "/d1reportSingleClassLayout[ClassName]"。

这两个编译选项分别会输出当前编译单元中所以class 的内存布局和指定class 的内存布局。对于学习class 的内存布局很方便。

关于一个class 的定义,在定义过程中涉及到的有:

  成员数据(静态,非静态)和成员函数(静态,非静态,virtual)。

所有的成员函数都不会占用对象的存储空间,无论是静态,非静态还是虚函数。

而对于成员数据来说,只有非静态的数据才会占用对象的存储空间。

这个很好理解,静态成员数据和成员函数是属于class 的,而非属于具体的对象,所以只要维护一份内存就可以了,无需每个对象都拷贝一份。

但是影响对象的大小的因素并不仅仅与看到的成员变量有关:

  非静态成员变量,虚函数表指针(_vftprt),虚基类表指针(_vbtptr),上文的内存对齐

  • 空类

class CEmpty{};

  对于空类,许多人想当然的认为大小应该是0。这是错误的,如果是正确的话,这个类可以被实例化成一个对象,且这个对象不占任何存储空间,且可以有很多不占任何空间的对象,而且这个不占空间的对象还可以有指针,这样就很奇怪了。

  所以正常编译器会给空类分配1个byte 的空间用于标示。

  sizeof(CEmpty) = 1

  • 普通类

class CBase {
public:
int m_ia;
static int s_ib;
private:
void f();
void g();
};

  其类的布局如下:

class CBase	size(4):
+---
0 | m_ia
+---

  只有m_ia 成员,size 为4个byte。因为静态数据成员和成员函数不占有对象空间。

  • 有虚函数的类

class CBase {
public:
int m_ia;
private:
void f();
void g();
virtual void h();
};

  其类的布局如下:

class CBase	size(8):
+---
0 | {vfptr}
4 | m_ia
+--- CBase::$vftable@:
| &CBase_meta
| 0
0 | &CBase::h

  可以看到该类的起始地址是放了一个"vfptr",这个指针用来指向该类的虚函数表。

  • 单一继承的类(无虚函数)

class CBase {
public:
int m_ia;
private:
void f();
void g();
}; class CChild :public CBase {
public:
int m_iChild;
};

  类的布局如下:

class CChild	size(8):
+---
| +--- (base class CBase)
0 | | m_ia
| +---
4 | m_iChild
+---

  即派生类中拷贝了一份基类中的成员数据,所以size 为8个byte。

  • 单一继承的类(含有虚函数)

class CBase {
public:
int m_ia;
public:
virtual ~CBase();
virtual void f();
virtual void g();
}; class CChild :public CBase {
public:
int m_iChild;
public:
virtual ~CChild();
virtual void g();
};

  其类的布局如下:

class CChild	size(12):
+---
| +--- (base class CBase)
0 | | {vfptr}
4 | | m_ia
| +---
8 | m_iChild
+--- CChild::$vftable@:
| &CChild_meta
| 0
0 | &CChild::{dtor}
1 | &CBase::f
2 | &CChild::g

  可以看到派生类中只有一个"vfptr",但是虚函数表中的函数却不同于基类中的函数,没有重写的虚函数沿用基类中的虚函数,而被重写的虚函数则更新为派生类中的虚函数。

  • 多重继承的类(基类都含有虚函数)

  

class CBase1 {
public:
int m_i1;
public:
virtual ~CBase1();
virtual void f1();
virtual void g1();
}; class CBase2 {
public:
int m_i2;
public:
virtual ~CBase2();
virtual void f2();
virtual void g2();
}; class CChild :public CBase1, public CBase2 {
public:
int m_iChild;
public:
virtual ~CChild();
virtual void f1();
virtual void g2();
};

  其类的布局如下:

class CChild	size(20):
+---
| +--- (base class CBase1)
0 | | {vfptr}
4 | | m_i1
| +---
| +--- (base class CBase2)
8 | | {vfptr}
12 | | m_i2
| +---
16 | m_iChild
+--- CChild::$vftable@CBase1@:
| &CChild_meta
| 0
0 | &CChild::{dtor}
1 | &CChild::f1
2 | &CBase1::g1 CChild::$vftable@CBase2@:
| -8
0 | &thunk: this-=8; goto CChild::{dtor}
1 | &CBase2::f2
2 | &CChild::g2

  CChild 分别从CBase1 和 CBase 中继承一个vfptr.

  • 菱形结构继承的类(非虚继承)

class CBase {
public:
int m_iBase;
public:
virtual ~CBase();
virtual void f0();
virtual void g0();
virtual void h0();
}; class CChild1:public CBase {
public:
int m_iChild1;
public:
virtual ~CChild1();
virtual void f0();
virtual void h1();
}; class CChild2:public CBase {
public:
int m_iChild2;
public:
~CChild2();
void g0();
void h1();
}; class CGrandChild :public CChild1, public CChild2 {
public:
int m_iGrandChild;
public:
virtual ~CGrandChild();
virtual void h0();
virtual void h1();
virtual void h2();
virtual void f0();
};

  其类的布局如下:

class CGrandChild	size(28):
+---
| +--- (base class CChild1)
| | +--- (base class CBase)
0 | | | {vfptr}
4 | | | m_iBase
| | +---
8 | | m_iChild1
| +---
| +--- (base class CChild2)
| | +--- (base class CBase)
12 | | | {vfptr}
16 | | | m_iBase
| | +---
20 | | m_iChild2
| +---
24 | m_iGrandChild
+--- CGrandChild::$vftable@CChild1@:
| &CGrandChild_meta
| 0
0 | &CGrandChild::{dtor}
1 | &CGrandChild::f0
2 | &CBase::g0
3 | &CGrandChild::h0
4 | &CGrandChild::h1
5 | &CGrandChild::h2 CGrandChild::$vftable@CChild2@:
| -12
0 | &thunk: this-=12; goto CGrandChild::{dtor}
1 | &thunk: this-=12; goto CGrandChild::f0
2 | &CChild2::g0
3 | &thunk: this-=12; goto CGrandChild::h0

  这种继承是有风险的,即通过CGrandChild 去访问m_iBase 时,容易造成二义性,需要使用"pGrandChild->CChild::m_iBase" 这种方法去访问。

  为了避免这种问题,C++ 中有一种机制是虚继承

  • 单一虚继承

class CBase {
public:
int m_iBase;
public:
virtual ~CBase();
virtual void f0();
virtual void g0();
virtual void h0();
}; class CChild1: virtual public CBase {
public:
int m_iChild1;
public:
virtual ~CChild1();
virtual void f0();
virtual void h1();
};

  其类的布局如下:

class CChild1	size(24):
+---
0 | {vfptr}
4 | {vbptr}
8 | m_iChild1
+---
12 | (vtordisp for vbase CBase)
+--- (virtual base CBase)
16 | {vfptr}
20 | m_iBase
+--- CChild1::$vftable@CChild1@:
| &CChild1_meta
| 0
0 | &CChild1::h1 CChild1::$vbtable@:
0 | -4
1 | 12 (CChild1d(CChild1+4)CBase) CChild1::$vftable@CBase@:
| -16
0 | &(vtordisp) CChild1::{dtor}
1 | &(vtordisp) CChild1::f0
2 | &CBase::g0
3 | &CBase::h0

  从布局中看,发现多了一个vbptr 指针,则是一个指向基类的虚基类指针;在派生类和虚基类之间又多了“vtordisp for vbase CBase”,vtordisp 并不是每个虚继承的派生类都会生成的,关于这部分可以参考MSDN 中 vtordisp;在vtordisp 后面则是虚基类的一个拷贝。

  • 多重继承的类(虚继承)

class CChild1 {
public:
int m_iChild1;
public:
virtual ~CChild1();
virtual void f0();
virtual void h1();
}; class CChild2 {
public:
int m_iChild2;
public:
~CChild2();
void g0();
void h1();
}; class CGrandChild :public CChild1, public CChild2 {
public:
int m_iGrandChild;
public:
virtual ~CGrandChild();
virtual void h0();
virtual void h1();
virtual void h2();
virtual void f0();
};

  virtual public Child1, public CChild2

    其类的布局如下:

class CGrandChild	size(28):
+---
0 | {vfptr}
| +--- (base class CChild2)
4 | | m_iChild2
| +---
8 | {vbptr}
12 | m_iGrandChild
+---
16 | (vtordisp for vbase CChild1)
+--- (virtual base CChild1)
20 | {vfptr}
24 | m_iChild1
+---

  

  public Child1, virtual public CChild2

    其类的布局如下:

class CGrandChild	size(20):
+---
| +--- (base class CChild1)
0 | | {vfptr}
4 | | m_iChild1
| +---
8 | {vbptr}
12 | m_iGrandChild
+---
+--- (virtual base CChild2)
16 | m_iChild2
+---

  virtual public Child1, virtual public CChild2

class CGrandChild	size(28):
+---
0 | {vfptr}
4 | {vbptr}
8 | m_iGrandChild
+---
12 | (vtordisp for vbase CChild1)
+--- (virtual base CChild1)
16 | {vfptr}
20 | m_iChild1
+---
+--- (virtual base CChild2)
24 | m_iChild2
+---

  通过上述虚继承的情况来看,可以看出有虚继承的派生类中,派生类和虚基类的数据是完全隔开的,先存放派生类自己的虚函数指针,虚基类指针和数据;然后有vtordisp 作为间隔;在存放虚基类的内容。

  • 菱形结构继承的类(虚继承)

class CBase {
public:
int m_iBase;
public:
virtual ~CBase();
virtual void f0();
virtual void g0();
virtual void h0();
}; class CChild1 : virtual public CBase {
public:
int m_iChild1;
public:
virtual ~CChild1();
virtual void f0();
virtual void h1();
}; class CChild2 : virtual public CBase{
public:
int m_iChild2;
public:
virtual ~CChild2();
virtual void g0();
virtual void h1();
}; class CGrandChild : public CChild1, public CChild2 {
public:
int m_iGrandChild;
public:
virtual ~CGrandChild();
virtual void h0();
virtual void h1();
virtual void h2();
virtual void f0();
};

  其类的布局如下:

class CGrandChild	size(40):
+---
| +--- (base class CChild1)
0 | | {vfptr}
4 | | {vbptr}
8 | | m_iChild1
| +---
| +--- (base class CChild2)
12 | | {vfptr}
16 | | {vbptr}
20 | | m_iChild2
| +---
24 | m_iGrandChild
+---
28 | (vtordisp for vbase CBase)
+--- (virtual base CBase)
32 | {vfptr}
36 | m_iBase
+---

  有了上文的基础,这个派生类的机构就不难理解了。

  

C++ 中类的内存布局的更多相关文章

  1. Vc++内存布局

    Vc++内存布局 测试平台 Windows server 2012 R2 and visual studio 2013 professional. 本篇文章意在介绍vc++中类的内存布局方式,只是研究 ...

  2. VS2010下如何查看类的内存布局

    用VS2010查看类的内存布局,这里用两种方法 (1)MSVC有个隐藏的"/d1"开关,通过这个开关可以查看项目中类的内存布局情况. 修改项目属性,添加"/d1 repo ...

  3. 继承虚函数浅谈 c++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。

    本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...

  4. vs查看虚函数表和类内存布局

    虚继承和虚基类 虚继承:在继承定义中包含了virtual关键字的继承关系:     虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:class CSubClass : publ ...

  5. VS2013命令行界面查看虚函数的内存布局

    内存布局可能使用vs的界面调试看到的旺旺是一串数字,很不方便,但是vs的命令行界面可以很直观的显示出一个类中具体的内存布局. 打开命令行.界面如下所示: 测试代码如下所示: class Base1 { ...

  6. C++ | 虚函数表内存布局

    虚表指针 虚函数有个特点.存在虚函数的类会在类的数据成员中生成一个虚函数指针 vfptr,而vfptr 指向了一张表(简称,虚表).正是由于虚函数的这个特性,C++的多态才有了发生的可能. 其中虚函数 ...

  7. 图说C++对象模型:对象内存布局详解

    0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看. 本文的结论都在VS2013上得到验证.不同的编译器在内存布局的细节上可能有 ...

  8. C++ 系列:内存布局

    转载自http://www.cnblogs.com/skynet/archive/2011/03/07/1975479.html 为什么需要知道C/C++的内存布局和在哪可以可以找到想要的数据?知道内 ...

  9. C++类内存布局图(成员函数和成员变量分开讨论)

    一.成员函数 成员函数可以被看作是类作用域的全局函数,不在对象分配的空间里,只有虚函数才会在类对象里有一个指针,存放虚函数的地址等相关信息. 成员函数的地址,编译期就已确定,并静态绑定或动态的绑定在对 ...

随机推荐

  1. UML-UML工具与UML蓝图

    1.UML应用场景 1).UML作为草图 2).UML作为蓝图. UML生成java代码(前向工程) java代码生成UML(逆向工程) 2.如果绘制了UML草图,如何在编码后更新该图形? 逆向工程, ...

  2. Thread--线程工作万花筒

    线程工作内存图. 线程状态.

  3. cuda addressMode解析

    cudaAddressModeClamp:超出范围就用边界值代替,示意: AA | ABCDE | EE cudaAddressModeBorder:超出范围就用零代替,示意: 00 | ABCDE ...

  4. 关于 SQL Server 数据库批量备份与还原的一些 Tips

    一.前提 最近需要将服务器 A 上的数据库全部备份,并在服务器 B 上进行还原,30多个数据库一个一个地用鼠标点,先是 backup,之后时 restore……整个过程实在是太浪费时间了!于是直接写一 ...

  5. awk grep sed 的一些问题

    条件   匹配    打印含关键字的行 ps aux  | sort -k 4 -r | awk '$4 ~ /^[0-9]/ && $4>0 {print $4,$11}' z ...

  6. UML-类图-箭头

    概览 1.泛化 一般理解为 继承.实线+空心箭头 2.依赖 成员变量.局部变量.参数.虚线+箭头 public class Sale { public void updatePriceFor(Prod ...

  7. Deep learning with Python

    一.导论 1.1 人工智能.机器学习.深度学习 人工智能.机器学习 人工智能:1980年代达到高峰的是专家系统,符号AI是之前的,但不能解决模糊.复杂的问题. 机器学习是把数据.答案做输入,规则作输出 ...

  8. python3.6内置模块——random详解

    python内置模块random是用来生成随机数的,在许多场合都能应用到,算是比较常见的一种模块吧,下面详细介绍其具体用法. 基本用法 随机生成浮点数:有两种,一种没有参数,默认是0~1,另一种可以指 ...

  9. 实践一次有趣的sql优化

    课程表 #课程表 create table Course( c_id int PRIMARY KEY, name varchar(10) ) 增加 100 条数据 #增加课程表100条数据 DROP ...

  10. swift------导入OC三方类找不到头文件的解决方法

    1.首先新建个 swift工程 2.在swift 中新建 OC 类 比如新建 Request 类,会自动生成个.XXXX-Bridging-Header 类: 3.让后把 导入的第三方头文件导入进去. ...