C++ 虚函数的对象模型

1.无继承的普通类:

在有虚函数的情况下类会为其增加一个隐藏的成员,虚函数表指针,指向一个虚函数表,虚函数表里面就是类的各个虚函数的地址了。那么,虚函数表指针是以什么模型加入到类里面的,虚函数表里面又是怎么安排的呢。简单来看下就可以知道了。

#include"stdafx.h"
#pragma pack(8)

class A{
public:
       int a; double a2;
       A() :a(0xaaaaaaaa), a2(0){}
       virtual void funA2(){}
       virtual ~A(){}
       virtual void funA(){}
};

int _tmain(int argc, _TCHAR* argv[])
{
       A a;
       return 0;
} 

定义一个A的变量,然后看其内存布局:

最开始的 4个字节就是虚函数表指针了,类A中有double类型的成员变量 a2,所以类 A的有效字节对齐数是 8,因此可以看到在虚函数表指针后又填充了 4个字节。放完虚函数表指针然后才到类 A 的成员变量。所以在普通类里面,如果有虚函数的话就会在最开始的地方添加一个隐藏的成员变量,虚函数表指针,然后才到正常的成员变量。然后我们再去看下虚函数表里面是什么样子的:

虚函数表也是以4字节为一项,每一项保存一个虚函数的地址。保存的虚函数的地址按照函数声明的顺序排放,第一项存放第一个声明的虚函数,第二项存放第二个,依此类推。我们看下这个表里面的每个项都是什么。

依次选择:调试 --> 窗口 --> 反汇编,打开汇编窗口,可以看到源程序的汇编代码。

我们先来看第一个虚函数:

virtual void funA2(){}

由上上图可知,该函数的地址是:0x00d41028(注意是小端序),在汇编窗口中找到该地址:

可以看到0x00d41028 处放置了一条 jmp 指令,virtual void funA2() 的真正地址是 0x00d41550

我们可以在汇编窗口中找到 0x00d41550地址,结果如下:

果然是 funA2()的起始位置~

第二个虚函数:

       virtual ~A(){}

该虚函数的地址是:0x00d411b8(注意是小端序),在汇编窗口中找到该地址:

可以看到0x00d411b8 处放置了一条 jmp 指令,virtual ~A(){} 的真正地址是 0x00d414e0

我们可以在汇编窗口中找到 0x00d414e0地址,结果如下:

果然是 ~A() 的起始位置~

第三个虚函数:

       virtual void funA(){}

该虚函数的地址是:0x00d410aa(注意是小端序),在汇编窗口中找到该地址:

可以看到0x00d410aa 处放置了一条 jmp 指令,virtual  void  funA(){} 的真正地址是 0x00d41590

我们可以在汇编窗口中找到 0x00d41590地址,结果如下:

果然是 funA()的起始位置~

可以看到这虚函数表中的每一项地址实际上并不是虚函数的直接地址,而是一个跳转到相应虚函数的地址。

所以在有虚函数的情况下类的安排也是很简单的,和没有虚函数的情况相比就是在最前面加一个虚函数表指针而已。其他的东西就和没有虚函数的类的情况的时候一样了。然后好像也没有什么然后了,复杂的是在后面~

2.单继承的情况:

单继承大概又可以分为两种情况,一种是基类没有虚函数的情况,一种是基类已经有虚函数表指针的情况。我们分别来看下。

2.1 基类无虚函数的单继承

#include "stdafx.h"
#pragma pack(8)
class F2{ public: int f2; double f22; F2() :f2(0xf2f2f2f2), f22(0){} };
class B : F2{
public:
  int b;
  B() :b(0xbbbbbbbb){}
  virtual void funB(){}
};

int _tmain(int argc, _TCHAR* argv[])
{
  B b;
  return 0;
}

B的布局抓数据如下:

可以看到虚函数表指针还是放在最开始的地方,也遵循它自己的地址对齐规则,主动填充了4个字节在后面。然后就是F2作为一个整体结构存放在其后,最后才是成员变量b,整个结构也要自身对齐,所以填充了4个字节在最后。虚函数表里面的就是B的虚函数funB的地址了。因为只有一个虚函数,所以虚函数表里面也就只有一项。

同样,我们打开反汇编窗口,找到 0x012e1221 地址处:

可以看到 0x012e1221处放置了一条 jmp 指令,virtual void funB(){} 的真正地址是 0x012e14e0

我们可以在汇编窗口中找到 0x012e14e0地址,结果如下:

果然是  virtual void funB(){}  的起始位置~

所以在基类没有虚函数的情况下,会产生一个虚函数表指针,而且也还是先存放类的虚函数表指针,然后才到基类等。其实在类有虚函数的情况下(暂不考虑虚继承),虚函数表指针都是会存放在最开始的。我们再来看下如果继承的基类已经有了虚函数表指针的情况会是什么样子。

2.2 基类有虚函数的单继承

#include "stdafx.h"
#pragma pack(8)
class A
{
public:
	int a; double a2;
	A() :a(0xaaaaaaaa), a2(0){}
	virtual void funA2(){}
	virtual ~A(){}
	virtual void funA(){}
};

class B : A{
public:
	int b;
	B() :b(0xbbbbbbbb){}
	virtual void funB(){}
	virtual void funA2(){}
};

int _tmain(int argc, _TCHAR* argv[])
{
	B b;
	return 0;
}

A的布局我们已经知道了,现在B继承A,而且还有覆盖了A的虚函数,来看下布局。

很明显,在基类已经有虚函数表指针的情况下派生类不会再主动产生一个虚函数表指针,基类的虚函数表指针是可以和派生类共用的,因为基类的虚函数肯定也是属于派生类的,如果派生类有虚函数覆盖掉基类的虚函数的话就会把虚函数表里面的相应的项改成正确的地址,而且虚函数表指针刚好也是放在类的最开始的位置。所以在这种情况下就是先放基类然后再排放成员变量。我们来看下现在派生类和基类共用的虚函数表是什么样子的。

虚指针表中共有 4 项,像前面的分析方法一样,我们结合反汇编窗口,可以得出如下结论(注意是小端序):

虚函数表有4个项:

1、         第一个项的虚函数已经被B里面的那个funA2所取代了,因为B里面的funA2已经覆盖了基类A里面的funA2,所以在虚函数表里面也要相应的改变,这也正是虚函数得以正确调用的前提。

2、         第二个项,也被替换成了B的虚析构函数,我们在代码里面没明写出B的虚析构函数,编译器会自动生成一个,而且B的虚析构函数也是会覆盖掉基类A的虚析构函数的。

3、         第三项还是A里面的函数funA,因为在派生类里面没有被覆盖,所以还应该是基类里面的函数。

4、         第四项是基类A没有的函数funB,所以在这个共用的虚函数表里面基类A只是用到了前3项而已,后面的项就是没有覆盖掉基类的其他虚函数了,而且是按照声明顺序依次排放的。

所以我们暂时可以得出的结论是,有虚函数的类在单继承的情况下,如果基类没有虚函数表指针的话会产生一个隐藏的成员变量,虚函数表指针,放在类的最前面,然后才是基类,最后是派生类的各个成员;如果基类已经有了虚函数表指针的话就不需要再产生一个虚函数表指针,派生类可以和基类共用一个虚函数表,此时派生类的布局是先放基类然后再放派生类的各个成员变量。如果派生类有函数覆盖了基类里面的虚函数的话,虚函数表里面的相应项就会改成这个函数的真正地址,其他没有覆盖的虚函数按照声明的顺序依次排放在虚函数表的后面各项中。

3.多继承的情况

鉴于有虚函数的类的第一项都要是虚函数表指针,所以在多继承的情况下会跟普通情况有所不同。但是有虚函数的类多继承情况下的对象模型也还是比较简单和明确的。

大概也有两种情况,一种是所有的基类都没有虚函数的情况,一种是基类中有些有虚函数有些又没有虚函数的混杂情况。

对于第一种情况,内存布局大概是这样,比如类A的基类都是没有虚函数的话

class A:F0,F1,F2{int a; (其他成员变量)…… virtual voidfun1(){} ……};

那么A肯定也还是要生成一个虚函数表指针的,放在最开始的位置,这种情况下的等价模型大概是这样 :

class A{void * vf_ptr;F0{};F1{};F2{};int a; (其他成员变量)……};

注意各个的字节对齐就可以了,特别是虚函数表指针。

对于第二种情况,基类是混杂的情况的时候,比如类A:

class A : F0, F1, V0, V1, F2, V2 { int a; (其他成员变量)…… virtual void fun1(){} ……};

V0、V1、V2是有虚函数的基类,F是没虚函数的基类,而且继承的声明顺序随意。像这种情况的话类A的对象模型大概是这样的:先排放基类中有虚函数的基类,按照声明顺序,然后再排放基类中没有虚函数的基类,也是按照声明顺序。比如A此时的对象模型就大概是这样:

class A{V0{};V1{};V2{};F0{};F1{};F2{};int a; (其他成员变量)……};

因为基类已经有了虚函数表指针了,所以派生类A也是可以和第一个有虚函数表指针的基类共用一个虚函数表的,这个和单继承的时候的道理是一样的,自然派生类就不会在生成一个虚函数表指针了。我们来实际来下这两种情况的实例。

3.1 基类没有虚函数

#include"stdafx.h"
#pragma pack(8)
class F0{ public:char f0; F0() :f0(0xf0){} };
class F1{ public:int f1; F1() :f1(0xf1f1f1f1){} };

class C : F1, F0{
public:
       int c;
       virtual void funC(){}
       virtual void funB(){}
       virtual void funA2(){}
       C() :c(0x33333333){}
};

int _tmain(int argc, _TCHAR* argv[])
{
       C c;
       return 0;
} 

在派生类有虚函数而基类都没有虚函数的情况下,派生类仍然会产生一个虚函数表指针放在最开始,然后才到各个基类,最后就是成员变量了。结合反汇编窗口,来看下虚函数表里面是些什么。

虚函数表指针共有 3 项,像前面的分析方法一样,我们结合反汇编窗口,可以得出如下结论(注意是小端序):

可以看到由于派生类的虚函数没有覆盖任何基类里面的虚函数所以虚函数表里面的各项就是各个虚函数按照声明的顺序的地址了。然后再来看下基类有虚函数而且派生类还有覆盖掉基类的虚函数的情况。

3.2 基类中有虚函数

#include"stdafx.h"
#pragma pack(8)
class F0{ public:char f0; F0() :f0(0xf0){} };
class F1{ public:int f1; F1() :f1(0xf1f1f1f1){} };

class A
{
public:
       int a; double a2;
       A() :a(0xaaaaaaaa), a2(0){}
       virtual void funA2(){}
       virtual ~A(){}
       virtual void funA(){}
};

class B : A{
public:
       int b;
       B() :b(0xbbbbbbbb){}
       virtual void funB(){}
       virtual void funA2(){}
};

class C : F1, A,F0, B{
public:
       int c;
       virtual void funC(){}
       virtual void funB(){}
       virtual void funA2(){}
       C() :c(0x33333333){}
};

int _tmain(int argc, _TCHAR* argv[])
{
       C c;
       return 0;
}

类C的模型大概是这样:

class C{
public:
   A a;
   B b;
   F1 f1;
   F0 f0;
   int c;
};

很明显,虽然F1声明在基类的最前面但是存放顺序还是先存放有虚函数的基类A然后到也是有虚函数的基类B,再才是各个没有虚函数的基类F1、F0。最后才是派生类C的成员变量。C的虚函数funB 覆盖了基类B里面的虚函数,而另一个虚函数funA2既覆盖了基类A里面的虚函数也覆盖了基类B继承自基类A里面的虚函数funA2,理论上基类A和基类B里面被覆盖掉的虚函数其在各自虚函数表里面的对应项都要被改变成正确的函数地址,也就是C里面的虚函数的真实地址。然后我们看下A和B的虚函数表是什么样子的。

A和C共用的虚函数表:

虚函数表指针共有 4项,像前面的分析方法一样,我们结合反汇编窗口,可以得出如下结论(注意是小端序):

B的虚函数表:

虚函数表指针共有 4项,像前面的分析方法一样,我们结合反汇编窗口,可以得出如下结论(注意是小端序):

可以看到派生类和基类A共享的虚函数表里面的各个项已经修改成了函数的真正的地址,在最后还加了一个没有覆盖掉任何基类虚函数的虚函数地址项。而基类B里面的项就有点意外了,它并不是直接修改成跳转到正确的地址上去,而是使用了一个调整块的东西,把EAX寄存器减去相应的值,然后再跳转到正确的函数里面去,这个暂时不在这里赘述,反正最后还是跳转到了C里面的那个函数里面去就是了。其他的项有覆盖的也还是一样都要修改成正确的函数地址。

时间: 2024-10-12 20:43:46

C++ 虚函数的对象模型的相关文章

【深度探索c++对象模型】Function语义学之虚函数

虚函数的一般实现模型:每一个class有一个virtual table,内含该class之中有作用的virtual function的地址,然后每个object有一个vptr,指向virtual table. 识别class是否支持多态,唯一恰当的方法是看它是否有virtual function,只要class拥有virtual function,它就需要额外的执行期信息. 考虑ptr->z(),ptr是基类指针,z是虚函数,为了找到并调用z()的适当实体,我们需要两项信息: 1.ptr所指对象

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr)、C++对象模型

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr).C++对象模型 一.虚函数表指针(vptr)及虚基类表指针(bptr) C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括: virtual function机制:用以支持一个有效率的"执行期绑定": virtual base class:用以实现多次在继承体系中的基类,有一个单一而被共享的实体. 1.虚函数表指针 C++中,有两种数据

C++ 虚继承和虚函数同时存在的对象模型

如果说没有虚函数的虚继承只是一个噩梦的话,那么这里就是真正的炼狱.这个C++中最复杂的继承层次在VC上的实现其实我没有完全理解,摸爬滚打了一番也算得出了微软的实现方法吧,至于一些刁钻的实现方式我也想不到什么理由来解释它,也只算是知其然不知其所以然吧. 分2个阶段来探讨: 1.      有虚函数的派生类虚继承了没有虚函数的基类: 2.      有虚函数的派生类虚继承了有虚函数的基类: 1.  基类无虚函数 1.1 虚.实基类都没有虚函数 这种情况也还算比较简单.因为虚函数表指针一定是会放在最开

看到的关于虚函数继承及虚继承解释比较好的文章的复制

(来源于:http://blog.chinaunix.net/uid-25132162-id-1564955.html) 1.空类,空类单继承,空类多继承的sizeof #include <iostream> using namespace std; class Base1 { }; class Base2 { }; class Derived1:public Base1 { }; class Derived2:public Base1, public Base2 { }; int main(

C++中构造函数能调用虚函数吗?(答案是语法可以,输出错误),但Java里居然可以

环境:XPSP3 VS2005 今天黑总给应聘者出了一个在C++的构造函数中调用虚函数的问题,具体的题目要比标题复杂,大体情况可以看如下的代码: [cpp] view plain copy class Base { public: Base() { Fuction(); } virtual void Fuction() { cout << "Base::Fuction" << endl; } }; class A : public Base { public:

虚函数——虚表总结

非虚拟继承 [带虚函数的类] class Base { public: virtual void FunTest1() { cout<<"Base::FunTest1()"<<endl; } virtual void FunTest2() { cout<<"Base::FunTest2()"<<endl; } int _data1; }; int main() { Base b; b._data1 = 0x01; re

C++虚函数及虚函数表解析

一.背景知识(一些基本概念) 虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数.纯虚函数(Pure Virtual Function):基类中没有实现体的虚函数称为纯虚函数(有纯虚函数的基类称为虚基类).C++  “虚函数”的存在是为了实现面向对象中的“多态”,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数.通过动态赋值,实现调用不同的子类的成员函数(动态绑定).正是因为这种

C++ 虚函数的内存分配

1.无继承的普通类: 在有虚函数的情况下类会为其增加一个隐藏的成员,虚函数表指针,指向一个虚函数表,虚函数表里面就是类的各个虚函数的地址了.那么,虚函数表指针是以什么模型加入到类里面的,虚函数表里面又是怎么安排的呢.简单来看下就可以知道了. #include"stdafx.h" #pragma pack(8) class A{ public: int a; double a2; A() :a(0xaaaaaaaa), a2(0){} virtual void funA2(){} vir

[GeekBand] C++继承关系下虚函数内存分布

本文参考文献:GeekBand课堂内容,授课老师:侯捷 :深度探索C++对象模型(侯捷译) :网络资料,如:http://blog.csdn.net/sanfengshou/article/details/4574604 说明:由于条件限制,仅测试了Windows平台下的VS2013 IDE.其余平台结果可能不同,但原理都类似.建议读者自己在其他平台进行测试. 1.什么是虚函数? 虚函数是类的非静态成员函数,在类中的基本形式如下:virtual 函数返回值类型 虚函数名(形参表) 如:virtu