通过sizeof计算,深入了解C++的实现机制

从一个简单的例子开始

首先从一个简单的类开始,例如有如下类:

class A
{
private:
  static int c_nA; //32bite
  const int m_nA;
  char m_cB;
  char * m_pC;
public:
  A(){}
  ~A(){}
  virtual void FunA(){printf("This is A.FunA\n");}
  void FunB(){printf("This is A.FunB\n");}
  virtual void FunC(){printf("This is A.FunC\n");}
  static void FunD(){printf("This is A.FunD\n");}
};

在32位的CPU上运行,求sizeof(A)的值。如果是64位的CPU,sizeof(A)又是多少?

那么我们要求这个类的内存占用,首先要了解这个类的实例拥有哪些元素,以及这些元素如何组织。我们可以这里着手进行分析。

实例内容

为方便说明,这里将a定义为A的实例化,即:

A a;

那么,a的内存空间中有哪些元素?

  • 静态变量 c_nA 是类变量,在类被实例化时不需要为该变量分配新的内存空间,因此在实例a中不需要为该变量开辟新的空间。
  • const int m_nA是静态变量,但是也需要为该变量开辟缓存空间。
  • char m_cB 和 char * m_pC是常规成员变量,实例化时也需要为这些常规变量开辟存储空间。
  • FunA和FunC是虚函数,可能需要在内存中放置一些虚函数的标记,因此需要开辟一部分内存空间,称为vtable。

所以,实例a的内存空间中的变量如下:

const int m_nA;
char m_cB;
char * m_pC;
vtable

下面分别讲解各个部分的内存占用计算。

成员变量

成员变量需要占用一定的内存空间,各种变量类型需要占用的空间如下表:

类型占用空间
char / unsigned char1Byte
int / unsigned int2Byte / 4Byte (视主机特性而定)
long / unsigned long4Byte(windows) / 8Byte(unix/linux)
short / unsigned short2Byte
long long / unsigned long long8Byte
float / unsigned float4Byte
double / unsigned double8Byte
bool1Byte
void * / char * 等指针类型4Byte(32it下) / 8Byte(64Bit下)
函数指针4Byte(32Bit下) / 8Byte(64Bit下)

通过上表基本可以计算出以上所有类型所需的内存空间(宿主机为32Bit),如下:

const int m_nA;  //4Byte
char m_cB;       //1Byte
char * m_pC;     //4Byte

因此至少需要9Byte的空间来存放该A的实例化对象。

虚函数表

C++在编译时,会把类中的虚函数放到一张表中,称为虚函数表,即vtable。实例化时,会将虚函数表的地址放到类中,因此在实例中还需要一个指针类型存放vtable的值,即需要占用4个字节。如下:

const int m_nA;  //4Byte
char m_cB;       //1Byte
char * m_pC;     //4Byte
vtable           //4Byte

因此,总计需要占用13Byte的空间。

但是这个结果跟我们使用sizeof运算符计算出来的结果有所出入,是因为还需要考虑到内存对齐的问题。

内存对齐

在计算机中,还需要考虑到内存对齐带来的空内存占用问题。

在32位计算机中,内存以4字节对齐,即不满4字节的按4字节算;在64位计算机中,不满8字节的按8字节算。当然还需要考虑实际类变量占用的内存空间,具体规则如下:

  • 当单个变量占用空间不满4字节时,按单个变量所占空间最大值计算对齐。例如有如下类:

    class B
    {
      char m_cA;
      char m_cB;
      short m_nC;
      char m_cD;
    };
    

    由于有3个char变量,一个short变量,单个变量占用空间最大的是short变量,为2Byte,不满4字节,因此该类按2Byte对齐,应占用6Byte空间,内存分布如下:

    地址内容(2Byte)
    0x00m_cA,m_cB
    0x02m_nC
    0x04m_cD
  • 如果单个变量占用长度超过4字节(32位计算机下),则按4字节对齐。例如:

    class C
    {
      long long m_llA;
      char m_cB;
      short m_nC;
      char m_cD;
    };
    

    由于类中有一个 long long类型,占用8字节,超过4字节,所以系统应按4字节对齐,占用空间为12Byte,内存分布如下:

    地址内容(4Byte)
    0x00m_llA
    0x04m_llA
    0x08m_cB
    0x0Cm_nC,m_cD

综合以上两点可以发现,合理的变量声明顺序可以降低内存消耗。例如:

class D
{
  int m_nA;
  char m_cB;
  int m_nC;
  short m_nD;
  char m_cE;
};

如果使用sizeof计算以上类D的存储空间,可以发现,占用了16字节,但是实际变量只需要12字节就可以放得下。因此其中有4字节是被浪费掉的。内存分布如下:

地址内容(4Byte)备注
0x00m_nA
0x04m_cB浪费3Byte
0x08m_nC
0x0Cm_nD,m_cE浪费1Byte

如果调整上述变量的声明顺序如下,则可以降低内存消耗:

class D
{
  int m_nA;
  int m_nC;
  short m_nD;
  char m_cE;
  char m_cB;
};

此时,内存分布如下:

地址内容(4Byte)
0x00m_nA
0x04m_nC
0x08m_nD,m_cE,m_cB

通过调整顺序可以避免不必要的内存浪费,从而降低内存消耗。上述例子通过调整顺序,可以将原本需要消耗16Byte的内存降低为12Byte。

说完内存对齐,再回头看看class A。在内存中的变量分布应该如下:

地址内容(4Byte)备注
0x00m_nA
0x04m_cB浪费3Byte
0x08m_pC
0x0Cvtable

从上表可以看出,实际内存占用应为16Byte,其中有3Byte的空间被浪费了。

成员函数/类变量

成员函数和类变量一样,是类的公共对象,不占用实例的内存空间。

说点复杂的——继承

单继承

例如有如下代码:

class A
{
private:
  int m_nA;
  char m_cB;
public:
  A(){}
  ~A(){}
  virtual void FunA(){printf("this is A.FunA\r\n");}
  virtual void FunB(){printf("this is A.FunB\n");}
};

class B:public A
{
private:
  int m_nBA;
  char m_cBB;
public:
  B(){}
  ~B(){}
  virtual void FunA(){printf("this is B.FunA\r\n");}
  virtual void FunC(){printf("this is B.FunC\n");}
};

void main()
{
  B b;
  A* pA = &b;
  printf("sizeof(B) = %d\n",sizeof(B));
}
  • 求sizeof(B)的值;
  • 父类A在B中的内存如何分布;
  • pA和b的地址有什么关系,如何实现用父类指针访问子类实例;
  • A和B中的虚函数表如何处理?

首先,通过以上介绍我们已经可以计算出A所占的空间,为12字节,如下:

地址内容(4Byte)
0x00m_nA
0x04m_cB
0x08vtable

那么B是A的派生类,切含有一个重写的虚函数和一个原生的虚函数,这时候编译器如何处理?显而易见的,B需要为变量m_nC和m_nD分配内存空间,所以B至少需要20字节的内存空间,那么内存空间如何排列?

上述代码在构造函数中增加局部变量的初始值,用于标记。在windows下使用x86编译之后,运行调试模式,可以看到b的内存内容如下:在这里插入图片描述
从上图中可以发现:

  • pA指向对象b的起始地址(废话,程序中就是这么赋值的)
  • b中有且只有一个vtable指针,即A中的__vfptr,指向0x00e49bf4
  • 在vtable之后,放的是A类中的m_nA、m_cB的值,随后是B类中的m_nBA、m_cBB的值

因此可以看出在虚函数表上,派生类只对父类的虚表做扩展,所以从始至终都只有一张虚表,虚表指针占4个字节,B类的实例总计占用20字节的空间

在好奇心的驱使下,我们在找到__vfptr的地址,看看存了什么,截图如下:在这里插入图片描述

可以看到存储了三个指针,地址分别为:

  • 0x00e413ca
  • 0x00e41393
  • 0x00e41479

前两个指针可以结合上图得出:是虚函数FunA与FunB的地址,因此可以推断出0x00e41479是FunC的地址。但是这部分不参与sizeof的计算。(那你说个球球)

综上,我们可以回答上述的问题了:

  • 求sizeof(B)的值;答:20Byte

  • 父类A在B中的内存如何分布;答:分布如下:

    地址内容(4Byte)
    0x00__vfptr,即vtable
    0x04m_nA
    0x08m_cB
    0x0Cm_nBA
    0x10m_cBB

    父类A只能访问__vfptr和m_nA、m_cB,先分配A的存储空间再分配B的存储空间。

  • pA和b的地址有什么关系,如何实现用父类指针访问子类实例;答:pA=&b,分配内存时,先分配父类A的空间,再给子类B分配空间,指针pA的可访问区域小于B,因此可以用A的指针访问B的实例。

  • A和B中的虚函数表如何处理?答:A在实例化后会生成一张虚表和虚表指针,派生类B会将虚表扩展,放入自己的虚函数指针。

多继承

例如有如下代码:

class A
{
private:
  int m_nA;
public:
  A(){}
  ~A(){}
  virtual void FunA(){printf("this is A.FunA\r\n");}
};

class B
{
private:
  int m_nB;
public:
  B(){}
  ~B(){}
  virtual void FunB(){printf("this is B.FunB\n");}
};

class C:public A,public B
{
private:
	int m_nC;
public:
	C(){}
  ~C(){}
  virtual void FunC(){printf("this is C.FunC\n");};
};

void main()
{
  C c;
  A* pA = &c;
  B* pB = &c;
  printf("sizeof(B) = %d\n",sizeof(B));
  printf("A:%p\nB:%p\nC:%p\n",pA,pB,&c);
}
  • 求sizeof(C )的值;
  • 父类A、B在C中的内存如何分布;
  • c的地址和pA、pB有什么关系,如何实现用父类指针访问子类实例;
  • A和B中的虚函数表分别如何处理?

要解决如上问题,关键点有两个:

  1. pA、pB地址如如何分布
  2. A和B中的虚函数表如何处理

通过以上知识,我们可以做出基本判断:

  1. C中至少有一个虚函数表指针,占用4个字节
  2. C中有三个成员变量,占用12字节

因此至少需要16Byte的内存空间。

再将代码放到windows下编译调试,可以看到如下内容:
在这里插入图片描述

从上图可以看出:

  • c的地址等于pA,但是pB的值不等于c的地址!!!(程序中可不是这样的)
  • 有两张虚表,A和B中各一张

关键的两个问题有答案了,我们就可以回答上述问题:

  • 求sizeof(C )的值;答:20Byte(3个成员变量占用12字节,2个虚表指针占用8字节)
  • 父类A、B在C中的内存如何分布;答:先开辟内存存放A的成员变量和虚表,B的空间跟在A后面
  • c的地址和pA、pB有什么关系,如何实现用父类指针访问子类实例;答:c的地址等于pA,但是不等于pB,与pB的关系为:
    pB = ((char*)pA) + sizeof(A)
  • A和B中的虚函数表分别如何处理?答:分别定义

那么最后一个问题,C中的虚函数放在哪张虚表中?通过分别查找两张虚表在内存中的内容,如下:

父类A的虚表:
/Users/hwk/Library/Application Support/typora-user-images/截屏2020-12-08 下午3.57.08.png

前两个指针为:

  • 0x003712da
  • 0x00371488

父类B中的虚表:
在这里插入图片描述
显然,FunC应该是放在A的虚表中,地址为:0x00371488。理由是0x73696874与其他地址偏差较大,因此不可能是FunC的地址,排除掉。

其他

针对如下结构体,计算长度:

struct st1
{
	char a;
	int b;
	int c[0];
};

其中有两个注意点:

  1. char a和int b之间,由于内存对其的原因,char a后面有3个byte的空闲空间,因此a和b共需要占用8个字节的空间
  2. int c[0]这里不占用任何空间,但是程序中可以访问到成员c,其地址是(&b + 1)(这里+1偏移四个字节)

因此,该结构体的sizeof计算结果因为8字节。

孔子云:“温故而知新,可以为师矣”。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值