从一个简单的例子开始
首先从一个简单的类开始,例如有如下类:
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 char | 1Byte |
int / unsigned int | 2Byte / 4Byte (视主机特性而定) |
long / unsigned long | 4Byte(windows) / 8Byte(unix/linux) |
short / unsigned short | 2Byte |
long long / unsigned long long | 8Byte |
float / unsigned float | 4Byte |
double / unsigned double | 8Byte |
bool | 1Byte |
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) 0x00 m_cA,m_cB 0x02 m_nC 0x04 m_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) 0x00 m_llA 0x04 m_llA 0x08 m_cB 0x0C m_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) | 备注 |
---|---|---|
0x00 | m_nA | |
0x04 | m_cB | 浪费3Byte |
0x08 | m_nC | |
0x0C | m_nD,m_cE | 浪费1Byte |
如果调整上述变量的声明顺序如下,则可以降低内存消耗:
class D
{
int m_nA;
int m_nC;
short m_nD;
char m_cE;
char m_cB;
};
此时,内存分布如下:
地址 | 内容(4Byte) |
---|---|
0x00 | m_nA |
0x04 | m_nC |
0x08 | m_nD,m_cE,m_cB |
通过调整顺序可以避免不必要的内存浪费,从而降低内存消耗。上述例子通过调整顺序,可以将原本需要消耗16Byte的内存降低为12Byte。
说完内存对齐,再回头看看class A。在内存中的变量分布应该如下:
地址 | 内容(4Byte) | 备注 |
---|---|---|
0x00 | m_nA | |
0x04 | m_cB | 浪费3Byte |
0x08 | m_pC | |
0x0C | vtable |
从上表可以看出,实际内存占用应为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) |
---|---|
0x00 | m_nA |
0x04 | m_cB |
0x08 | vtable |
那么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 0x04 m_nA 0x08 m_cB 0x0C m_nBA 0x10 m_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中的虚函数表分别如何处理?
要解决如上问题,关键点有两个:
- pA、pB地址如如何分布
- A和B中的虚函数表如何处理
通过以上知识,我们可以做出基本判断:
- C中至少有一个虚函数表指针,占用4个字节
- 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的虚表:
前两个指针为:
- 0x003712da
- 0x00371488
父类B中的虚表:
显然,FunC应该是放在A的虚表中,地址为:0x00371488。理由是0x73696874与其他地址偏差较大,因此不可能是FunC的地址,排除掉。
其他
针对如下结构体,计算长度:
struct st1
{
char a;
int b;
int c[0];
};
其中有两个注意点:
- char a和int b之间,由于内存对其的原因,char a后面有3个byte的空闲空间,因此a和b共需要占用8个字节的空间
- int c[0]这里不占用任何空间,但是程序中可以访问到成员c,其地址是(&b + 1)(这里+1偏移四个字节)
因此,该结构体的sizeof计算结果因为8字节。
孔子云:“温故而知新,可以为师矣”。