【转】C++类-内存分布
C++类内存分布 - 转载自Jerry19880126 - 博客园 的文章
在上面这篇文章的基础上做了些整理。
主要讨论了C++类对象的内存分布结构。
来看看编译器是怎么处理类成员内存分布的,特别是在继承、虚函数存在的情况下。
原文地址:http://www.cnblogs.com/jerry19880126/p/3616999.html
0、准备
工欲善其事,必先利其器,我们先用好Visual Studio工具,像下面这样一步一步来:
选择左侧的C/C++->命令行
,在其他
选项这里写上
/d1 reportAllClassLayout
它可以看到所有相关类的内存布局,- 如果写上
/d1 reportSingleClassLayoutXXX(XXX为类名)
,则只会打出指定类XXX的内存布局。
1、普通类
下面可以定义一个类,像下面这样:
class Base{
int a;
int b;
public:
void CommonFunction();
};
编译后,可以看到输出框里面有这样的排布:
这里不想花精力在内存对齐因素上,所以成员变量都设为int型。
从这里可以看到普通类
的排布方式:
- 成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。
2、普通继承
再看下继承,往后面添加如下代码:
class DerivedClass: public Base{
int c;
public:
void DerivedCommonFunction();
};
编译,然后看到如下的内存分布(父类的内存分布不变,这里只讨论子类成员变量的内存分布):
可以看到子类继承了父类的成员变量
在内存排布上:
-先是排布了 父类的成员变量 ,接着排布 子类的成员变量,同样,成员函数不占字节。
3、包含虚函数的类
下面给基类加上虚函数,暂时注释掉DerivedClass,看一下这时的内存排布:
class Base{
int a;
int b;
public:
void CommonFunction();
virtual void VirtualFunction();
};
这个内存结构图分成了两个部分:上面是内存分布,下面是虚表。
- 内存分布:
VS带的编译器把虚函数表指针
放在了内存的开始处(地址偏移为0),然后再是成员变量; - 虚函数表:
紧跟在&Base1_meta
后面的0表示,这张虚表
对应的虚指针
在内存中的分布;
下面列出了虚函数,左侧的0是这个虚函数 的序号,这里只有一个虚函数,所以只有一项;
如果有多个虚函数,会有序号为1、2...的虚函数列出来。
- 编译器是在调用
构造函数
时创建这个虚表指针
以及虚表
的。
4、包含虚函数的继承
下面加上子类,并在子类中添加虚函数,像下面这样:
class DerivedClass: public Base{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};
可以看到子类内存的排布如下:
- 上半部是
内存分布
,可以看到,
虚表指针被继承了,且仍位于内存排布的起始处,
下面是父类的成员变量a和b,最后是子类的成员变量c,
注意虚表指针只有一个,子类并没有再生成虚表指针了; - 下半部的
虚表
情况与父类(3、包含虚函数的类)是一样的。
5、多态实现
编译器是如何利用虚表指针与虚表来实现多态的呢?
- 当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数表;
- 当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。
所以,如果是调用
Base *p = new Derived();
- 生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,
- 接着由
Derived*
到Base*
的隐式类型转换
并没有改变虚表指针
, - 若调用
p->VirtualFunction()
,实际上是p->vfptr->VirtualFunction()
,
它在构造的时候就已经指向了子类的VirtualFunction()
,所以调用的是子类的虚函数。
这就多态了。
6.1、变化1
我们把子类换个代码,像这样:
class DerivedClass1 : public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction2();
};
注意到这时我们并没有覆盖父类的虚方法,而是重声明了一个新的子类虚方法,内存分布如下:
还是只有一个虚表指针
,但是下方虚表
的内容变化了,
- 虚表的0号是父类的
VirtualFunction()
,而1号放的是子类的VirtualFunction2()
。
也就是说,如果定义了
DerivedClass
的对象(或者指向它的指针或引用),那么在构造时,虚表指针就会指向这个虚表;
如果调用的是VirtualFunction()
,那么会从父类中寻找对应的虚函数(!子类没有覆盖父类的函数);
如果调用的是VirtualFunction2()
,那么会从子类中寻找对应的虚函数。
????子类没有重载父类的虚函数是一件毫无意义的事情
6.2、变化2
我们再改造一下子类,像这样:
class DerivedClass1 : public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
virtual void VirtualFunction2();
};
我们既覆盖父类的虚函数,也有新添的虚函数,那么可以料想的到,是下面的这种内存分布:
7、多重继承
下面来讨论多重继承,代码如下:
class Base{
int a;
int b;
public:
void CommonFunction();
virtual void VirtualFunction();
};
class DerivedClass1: public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
};
class DerivedClass2 : public Base{
int d;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
};
class DerivedDerivedClass : public DerivedClass1, public DerivedClass2{
int e;
public:
void DerivedDerivedCommonFunction();
virtual void VirtualFunction();
};
内存分布从父类到子类,依次如下:
Base中有一个虚表指针,地址偏移为0
DerivedClass1继承了Base,内存排布是先父类后子类。
DerivedClass2的情况是类似于DerivedClass1的。
下面我们重点看看这个类DerivedDerivedClass,
- 由外向内看,它并列地排布着继承而来的两个父类DerivedClass1与 DerivedClass2,还有自身的成员变量e。
- DerivedClass1包含了它的成员变量c,以及Base,Base有一个0地址偏移的虚表指针,然后是成员变量a和b;
- DerivedClass2的内存排布类似于DerivedClass1,注意到DerivedClass2里面竟然也有一份 Base。
-这里有两份虚表了,分别针对DerivedClass1
与DerivedClass2
,
在&DerivedDericedClass_meta
下方的数字0是首地址偏移量,
靠下面的虚表的那个-16表示指向这个虚表的虚指针
的内存偏移,这正是DerivedClass2
中的{vfptr}
在DerivedDerivedClass
的内存偏移。
这里的描述和这篇文章的描述有些出入。
8、虚继承
如果采用虚继承,像下面这样:
class DerivedClass1: virtual public Base{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};
class DerivedClass2 : virtual 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类没有变化,但往下看:
DerivedClass1
就已经有变化了,原来是先排虚表指针与Base
成员变量,vfptr
位于0地址偏移处;但现在有两个虚表指针了,一个是vbptr
,另一个是vfptr
。
vbptr
是这个DerivedClass1
对应的虚表指针,它指向DerivedClass1
的虚表vbtable
,另一个vfptr
是虚基类表对应的虚指针,它指向vftable
。
下面列出了两张虚表,第一张表是vbptr
指向的表,8表示{vbptr}
与{vfptr}
的偏移;
第二张表是vfptr
指向的表,-8指明了这张表所对应的虚指针位于内存的偏移量。
DerivedClass2
的内存分布类似于DerivedClass1
,同样会有两个虚指针,分别指向两张虚表(第二张是虚基类表)。
下面来仔细看一下DerivedDerivedClass
的内存分布,这里面有三个虚指针了,但base
却只有一份。第一张虚表是内含DerivedClass1
的,20表示它的虚指针{vbptr}
离虚基表指针{vfptr}
的距离,
第二张虚表是内含DerivedClass2
的,12表示它的虚指针{vbptr}
离虚基表指针{vfptr}
的距离,
最后一张表是虚基表,-20指明了它对应的虚指针{vfptr}
在内存中的偏移。
虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(更多的虚表指针)。
9、总结
下面总结一下(当基类有虚函数时):
- 每个类都有虚指针和虚表;
- 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。
有多少个虚函数,虚表里面的项就会有多少。
多重继承时,可能存在多个的基类虚表与虚指针; - 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,
多重继承时虚基表与虚基表指针有且只有一份。