Vc++内存布局
测试平台
Windows server 2012 R2 and visual studio 2013 professional。
本篇文章意在介绍vc++中类的内存布局方式,只是研究其大概的排列方式,对于内存对齐方面则没有进行深入探讨。如有需要研究类的具体大小可以参考其他博客。
测试方式
在vs中,查看类的内存布局主要有两种方式。一种是实例化一个类,然后在debug状态下查看其数据成员。另外一种是在工程属性的c/c++ 的command line 中加入/d1reportSingleClassLayout[className],其中的[className]是你想查看内存布局的类的名字,经过build之后查看build的output,里面就有这个类的内存布局。其实也可以将所有包含的类的布局打印出来,命令是/d1reportAllClassLayout。但是这个命令打印出来的东西非常多,不好找。所以还是推荐用前面的查看单个类布局的命令来查看。下面是测试用代码。
#include <iostream>
class CommonBase
{
int co;
};
class Base1 : virtual public CommonBase
{
public:
virtual void print1() {}
virtual void print2() {}
private:
int b1;
int co;
};
class Base2 : virtual public CommonBase
{
public:
virtual void dump1() {}
virtual void dump2() {}
private:
int b2;
};
class Derived : public Base1, public Base2
{
public:
void print2() {}
void dump2() {}
private:
int d;
};
int main(void)
{
Derived tempinstance;
return 0;
}
下图(图 1:debug时查看内存)是采取debug时查看的方法来查看内存布局的结果。其实这个图只是表示了这几个类的层次关系,而并不代表真实的内存布局方式。例如,在此图中Base1和Base2中都包含了CommonBase 成员,且tempinstance也包含了Commonbase 成员。但是这几个CommonBase指向的都是同一个对象,即此图显示的只是逻辑结构,而不是内存结构。要查看准确的结果还得利用编译选项来打印。
图 1:debug时查看内存
图 2:启用编译选项 显示的是如何启用编译选项,就是在Additional Option 里面输入我们的命令,然后ok就行了。图 3:编译输出的Derived 布局 就是展示了一部分的输出结果,剩下的还有虚函数表和虚基类表,结果见 图 4:虚函数表与虚基类表。
图 2:启用编译选项
图 3:编译输出的Derived 布局
图 4:虚函数表与虚基类表。
无虚函数覆盖情况
类中占据内存的只有两个部分,类的数据成员和虚函数成员。而普通函数和静态函数的地址可以在编译时直接得到,因此在运行时一个类的对象并没有专门存储这些地址的内存开销。普通函数和静态函数都是存储在代码段里面,而静态数据成员则存储在数据段中。
无虚函数无继承有包含情况
这种情况下就和c语言的结构体包含的方式一模一样,数据成员的排列按照声明顺序排开。这里面唯一需要考虑的就是内存对齐,下面是测试代码。
#include <iostream>
class CommonBase
{
int co;
static int sco;
void selfprint(){};
};
class target
{
CommonBase Baseinstance;
void selfprint(){};
int ta;
static int sta;
};
int main(void)
{
target tempinstance;
return 0;
}
采取编译选项输出来的结果,见图 5:无虚函数无继承编译选项结果 。采取debug方式的结果,见图 6:无虚函数无继承debug结果 。由此可以看出普通函数和静态成员都不占对象内存。在后面的测试中将不再考虑这种情况的干扰,因为最简单。
图 5:无虚函数无继承编译选项结果
图 6:无虚函数无继承debug结果
有虚函数无继承无包含情况
测试代码只是将target类改写了一下,代码见下。
class target
{
virtual void virtualprint1(){}
virtual void virtualprint2(){}
void selfprint(){};
int ta;
static int sta;
};
采取编译选项的结果,见图 8:有虚函数无继承无包含编译选项结果。采取debug的结果见图 7:有虚函数无继承无包含debug结果。值得注意的是这里开始出现了vfptr这个域,它代表的意思是当前对象的虚函数表的指针,这个虚函数表按照声明顺序记录了这个对象的虚函数地址。在有虚函数的时候,每个对象的起始地址一定与这个对象的虚函数表地址域vfptr是相同的。对于this adjustor这个的意义将在下文中提。
图 7:有虚函数无继承无包含debug结果
图 8:有虚函数无继承无包含编译选项结果
无虚函数单继承无包含情况
测试代码见下,仍然只修改了target。
class target:CommonBase
{
void selfprint(){};
int ta;
static int sta;
};
对于debug结果,见图 9:无虚函数有继承无包含debug结果 。对于编译选项结果,见图 10:无虚函数有继承无包含编译选项结果 。这种情况下类似于无虚函数无继承有包含情况,此时基类地址与派生类的地址是一模一样的,这样在转换指针类型的时候无需任何代价,类似于c中嵌套结构体的强制类型转换。
图 9:无虚函数有继承无包含debug结果
图 10:无虚函数有继承无包含编译选项结果
无虚函数多继承无包含情况
测试代码如下,增加了一个anotherbase类,并让target继承了这个类。Debug结果,见图 11:无虚函数多继承无包含debug结果 。编译选项结果,见图 12无虚函数多继承无包含编译选项结果 。可见,仍然是按照声明顺序排列的。由于没有虚函数,所以派生类与基类之间的偏移量都是编译期可以确定的,在强制类型转换的时候,指针只需要加上一个固定的偏移量就行了,对于偏移量的结果见图 13:无虚函数多继承无包含类型转换偏移。
class CommonBase
{
int co;
static int sco;
void selfprint(){};
};
class anotherbase
{
int anco;
static int sanco;
void selfprint(){};
};
class target:CommonBase,anotherbase
{
void selfprint(){};
int ta;
static int sta;
};
int main(void)
{
target tempinstance;
target* derivedobject;
derivedobject = &tempinstance;
std::cout << "the distance between target address and Commonbase address is" << (int) derivedobject - (int) ((CommonBase*) derivedobject) << std::endl;
std::cout << "the distance between target address and anotherbase address is" << (int) derivedobject - (int) ((anotherbase*) derivedobject) << std::endl;
return 0;
}
图 11:无虚函数多继承无包含debug结果
图 12无虚函数多继承无包含编译选项结果
图 13:无虚函数多继承无包含类型转换偏移
有虚函数单继承无覆盖情况
这里修改了Commonbase 和target,使这两个都有虚函数但是不同名。测试代码如下。Debug结果见图 14:有虚函数单继承无覆盖debug结果,程序输出见图 15:有虚函数单继承无覆盖类型转换指针偏移,编译选项结果见图 16:有虚函数单继承无覆盖编译选项结果 。可以看出这里开始有差异了,在debug结果中vfptr只有一个表项,而编译选项中有两个表项。debug结果没完全输出所有的虚函数。真正的内存布局还是得看编译选项的结果。这里的commonbase与target公用一个虚函数表,commonbase的虚函数在前,target的虚函数在后。
#include <iostream>
class CommonBase
{
int co;
static int sco;
void selfprint(){};
virtual void basevirtualprint(){};
};
class target:CommonBase
{
void selfprint(){};
int ta;
static int sta;
virtual void targetvirtualprint(){};
};
int main(void)
{
target tempinstance;
target* derivedobject;
derivedobject = &tempinstance;
std::cout << "the distance between target address and Commonbase address is" << (int) derivedobject - (int) ((CommonBase*) derivedobject) << std::endl;
/*std::cout << "the distance between target address and anotherbase address is" << (int) derivedobject - (int) ((anotherbase*) derivedobject) << std::endl;*/
return 0;
}
图 14:有虚函数单继承无覆盖debug结果
图 15:有虚函数单继承无覆盖类型转换指针偏移
图 16:有虚函数单继承无覆盖编译选项结果
有虚函数多继承无覆盖情况
测试代码如下,这里的结果可以由上面的几次实验推出来。可以看出每一个基类都有它的vfptr域,而派生类的虚函数表与第一个基类的虚函数表是合并在一起的,处在基类表项的后面。
#include <iostream>
class CommonBase
{
int co;
static int sco;
void selfprint(){};
virtual void basevirtualprint(){};
};
class anotherbase
{
int anco;
static int sanco;
void selfprint(){};
virtual void anothervirtualprint(){};
};
class target:CommonBase,anotherbase
{
void selfprint(){};
int ta;
static int sta;
virtual void targetvirtualprint(){};
};
int main(void)
{
target tempinstance;
target* derivedobject;
derivedobject = &tempinstance;
std::cout << "the distance between target address and Commonbase address is" << (int) derivedobject - (int) ((CommonBase*) derivedobject) << std::endl;
std::cout << "the distance between target address and anotherbase address is" << (int) derivedobject - (int) ((anotherbase*) derivedobject) << std::endl;
return 0;
}
图 17:有虚函数多继承无覆盖debug结果
图 18:有虚函数多继承无覆盖程序输出
图 19:有虚函数多继承无覆盖编译选项结果
有虚函数混合多继承无覆盖
测试代码如下,只是增加了额外的一个基类thirdbase,其他代码并没有多大变化。内存布局仍然与声明顺序一样。
class thirdbase
{
int thirdco;
void selfprint(){};
static int sthirdco;
};
图 20:有虚函数混合多继承无覆盖debug
图 21:有虚函数混合多继承无覆盖程序输出
图 22:有虚函数混合多继承无覆盖编译选项输出
有虚函数覆盖情况
非菱形继承情况
测试代码如下,只是把target的一个虚函数改名为commonbase中的一个虚函数,使之覆盖。由下面两个图可以看出,commonbase的vfptr表中的basevirtualprint地址的确被改写成为了target 中的basevirtualprint。可以通过让这两个函数打印出不同的东西来验证,我就懒得写了。
class target:CommonBase,anotherbase,thirdbase
{
void selfprint(){};
int ta;
static int sta;
virtual void basevirtualprint(){};
};
图 23:非菱形继承虚函数覆盖debug
图 24:非菱形继承虚函数覆盖编译选项
菱形简单多继承
这里把anotherbase和thirdbase都修改为commonbase的派生类,然后再让target去继承这两个类。其中thirdbase和target都重写了虚函数basevirtualprint。由图 25图 26 可以看出,在这种继承关系下,target有两个commonbase成员。而且commonbase,thirdbase,anotherbase中的虚函数basevirtualprint都被target中的basevirtualprint给覆盖了。代码如下。
一个非常值得注意的一点就是,在图 26 thirdbase的虚函数表中,basevirtualprint出现了调整块:&thunk: this-=12; goto target::basevirtualprint..
因为如果通过将target*转换为thirdbase*并来调用basevirtualprint的话,由于这个是虚函数,所以调用的还是target::basevirtualprint,但是这个函数默认接收的是target*类型的this指针,指向一个target对象的开始地址。如果我们直接将thirdbase*指针当作this传进去的话,会出现重大错误。因此在传进this之前,我们需要将thirdbase*的this指针调整为target*的this指针,因此才会出现this-=12,然后我们再goto到真实的函数地址上去执行。这就是调整块的作用。在微软VC++实现中,对于有虚函数的多重继承,只有当派生类虚函数覆盖了多个基类的虚函数时,才使用调整块。
#include <iostream>
class CommonBase
{
int co;
static int sco;
void selfprint(){};
virtual void basevirtualprint(){};
};
class anotherbase:CommonBase
{
int anco;
static int sanco;
void selfprint(){};
virtual void anothervirtualprint(){};
};
class thirdbase:CommonBase
{
int thirdco;
void selfprint(){};
static int sthirdco;
virtual void basevirtualprint(){};
};
class target :anotherbase,thirdbase
{
void selfprint(){};
int ta;
static int sta;
virtual void basevirtualprint(){};
};
int main(void)
{
target tempinstance;
target* derivedobject;
derivedobject = &tempinstance;
std::cout << "the distance between target address and anotherbase address is" << (int) derivedobject - (int) ((anotherbase*) derivedobject) << std::endl;
std::cout << "the distance between target address and thirdbase address is" << (int) derivedobject - (int) ((thirdbase*) derivedobject) << std::endl;
return 0;
}
图 25:菱形简单多继承debug
图 26:菱形简单多继承编译选项
菱形虚继承
这里的代码跟上面的差不多,只不过是将anotherbase和thirdbase的继承方式都改变为虚继承。在debug的结果 图 27中,看上去target有三个commonbase,这只是幻觉。真实情况得看编译选项下的结果,见 图 28。
首先我们解释一下为什么会出现vbptr这个域。这个域的存在是为了在派生类对象中寻找基类对象的起始地址。在单继承和多重继承的情况下,内嵌的基类实例地址比起派生类实例地址来,要么地址相同(单继承,以及多重继承的最靠左基类) ,要么地址相差一个固定偏移量(多重继承的非最靠左基类) 。 然而,当虚继承时,一般说来,派生类地址和其虚基类地址之间的偏移量是不固定的,因为如果这个派生类又被进一步继承的话,最终派生类会把共享的虚基类实例数据放到一个与上一层派生类不同的偏移量处。因此这样的情况就催生了vbptr这个域,指向的是在当前对象中当前虚基类与这个虚基类的其他父类(自己是自己的父类,注意)对象的偏移。当这个虚基类有自己的虚函数时,这个虚基类的起始是vfptr,然后才是vbptr,接着的才是其他的数据域。而计算偏移的时候是当前vbptr的地址与其他父类的起始地址之差。所以,vbptr所指向的表项的第一项一般来说是-4,或者是0 。因为第一项计算的是自己的vbptr与自己的起始地址之间的差,当有vfptr这个域时,为-4;没有vfptr这个域时,为0 。在图 28 中,我们可以很明显的看出差别。
这里还要注意的一点就是虚继承的vfptr表项。在thirdbase中不存在vfptr了,而在anotherbase的vfptr中也没有basevirtualprint这个项。原因是,根据显示的制定虚继承方式,我们已经知道anotherbase和thirdbase虚继承自同一个基类,而target又覆盖了基类中的一个虚函数。因此这里的basevirtualprint就没有必要进行多次存储了,都压缩到了commonbase的vfptr中存储了。
在图 28的末尾,我们发现了一个之前没见过的东西:target::basevirtualprint this adjustor: 24
其实这里就是人为规定了在调用target::basevirtualprint的时候,到this+24处内容所指的虚函数表去找这个函数。
图 27:菱形虚继承debug结果
图 28:菱形虚继承编译选项结果
混合继承
这里的混合继承包括虚继承和普通继承。相对于之前的代码增加了第三个基类fourthbase。这里的结果又有了很大的不同,debug结果与编译选项结果顺序都不一样了。Debug是fourthbase在thirdbase之后,而在编译结果中fouthbase是在thirdbase之前,不知道为什么,猜测是一个有vfptr另外一个木有。测试代码如下。
class fourthbase
{
int fa;
static int sfa;
virtual void fourthvirtualprint(){};
};
class target :anotherbase,thirdbase,fourthbase
{
void selfprint(){};
int ta;
static int sta;
virtual void basevirtualprint(){};
};
图 29:混合继承debug
图 30:混合继承程序输出
图 31:混合继承编译选项结果第一部分
图 32:混合继承编译选项结果第二部分
虚函数访问代价
根据前面的分析,我们可以发现vc++是如何访问虚函数的。
- 当使用a.b来访问对象a中的虚函数b时,我们首先得到a::b this adjustor 后面的值c,然后获得this+c处的内容d,这个d就是一个虚表的指针,然后根据这个虚表来查找a::b所对应的项的内容即函数指针e,然后再进行函数调用(*e)(this,…..)。
- 当使用a->b来访问函数b时,我们首先根据a指向的虚表来查找a::b所在的项。我们得到这个项的内容c,然后执行(*c)(this,…)。如果c是一个调整块,我们还需要调整this,然后再进行一次跳转。