C++ 系列:C++ 对象模型
1 何为C++对象模型
C++对象模型可以概括为以下2部分:
1、语言中直接支持面向对象程序设计的部分;
2、对于各种支持的底层实现机制
语言中直接支持面向对象程序设计的部分,如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等等,这里我简单过一下,重点在底层实现机制。
在C语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。在C++中,通过抽象数据类型(abstract data type,ADT),在类中定义数据和函数,来实现数据和函数直接的绑定。
概括来说,在C++类中有两种成员数据:static、nonstatic;三种成员函数:static、nonstatic、virtual。
如下面的Base类定义:
Base类定义: |
#pragma once #include<iostream> using namespace std; class Base { public: Base(int); virtual ~Base(void); int getIBase() const; static int instanceCount(); virtual void print() const; protected: int iBase; static int count; }; |
Base类在机器中我们如何构建出各种成员数据和成员函数的呢?
2 基本C++对象模型
在介绍C++使用的对象模型之前,介绍2种对象模型:简单对象模型(a simple object model)、表格驱动对象模型(a table-driven object model)。
简单对象模型(a simple object model)
所有的成员占用相同的空间(跟成员类型无关),对象只是维护了一个包含成员指针的一个表。表中放的是成员的地址,无论上成员变量还是函数,都是这样处理。对象并没有直接保存成员而是保存了成员的指针。
表格对象模型(a table-driven object model)
这个模型在简单对象的基础上又添加了一个间接层。将成员分成函数和数据,并且用两个表格保存,然后对象只保存了两个指向表格的指针。这个模型可以保证所有的对象具有相同的大小,比如简单对象模型还与成员的个数相关。其中数据成员表中包含实际数据;函数成员表中包含的实际函数的地址(与数据成员相比,多一次寻址)。
3 C++对象模型
这个模型从结合上面2中模型的特点,并对内存存取和空间进行了优化。在此模型中,non static 数据成员被放置到对象内部,static数据成员, static and nonstatic 函数成员均被放到对象之外。对于虚函数的支持则分两步完成:
1. 每一个class产生一堆指向虚函数的指针,放在表格之中。这个表格称之为虚函数表(virtual table,vtbl)。
2. 每一个对象被添加了一个指针,指向相关的虚函数表vtbl。通常这个指针被称为vptr。vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。
另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。
这个模型的优点在于它的空间和存取时间的效率;缺点如下:如果应用程序本身未改变,但当所使用的类的non static数据成员添加删除或修改时,需要重新编译。
模型验证测试
为了验证上述C++对象模型,我们编写如下测试代码。
模型验证测试: |
void test_base_model() { Base b1(1000); cout << "对象b1的起始内存地址:" << &b1 << endl; cout << "type_info信息:" << ((int*)*(int*)(&b1) - 1) << endl; RTTICompleteObjectLocator str= *((RTTICompleteObjectLocator*)*((int*)*(int*)(&b1) - 1)); //abstract class name from RTTI string classname(str.pTypeDescriptor->name); classname = classname.substr(4,classname.find("@@")-4); cout << classname <<endl; cout << "虚函数表地址:\t\t\t" << (int*)(&b1) << endl; cout << "虚函数表 — 第1个函数地址:\t" << (int*)*(int*)(&b1) << "\t即析构函数地址:" << (int*)*((int*)*(int*)(&b1)) << endl; cout << "虚函数表 — 第2个函数地址:\t" << ((int*)*(int*)(&b1) + 1) << "\t"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&b1)) + 1); pFun(); b1.print(); cout << endl; cout << "推测数据成员iBase地址:\t\t" << ((int*)(&b1) +1) << "\t通过地址取值iBase的值:" << *((int*)(&b1) +1) << endl; cout << "Base::getIBase(): " << b1.getIBase() << endl; b1.instanceCount(); cout << "静态函数instanceCount地址: " << b1.instanceCount << endl; } |
根据C++对象模型,实例化对象b1的起始内存地址,即虚函数表地址。
l 虚函数表的中第1个函数地址是虚析构函数地址;
l 虚函数表的中第2个函数地址是虚函数print()的地址,通过函数指针可以调用,进行验证;
l 推测数据成员iBase的地址,为虚函数表的地址 + 1,((int*)(&b1) +1);
l 静态数据成员和静态函数所在内存地址,与对象数据成员和函数成员位段不一样;
下面是测试代码输出:(从下面2个图验证了,上面的观点。)
注意:本测试代码及后面的测试代码中写的函数地址,是对应虚函数表项的地址,不是实际的函数地址。
图:测试代码输出结果
图:vs断点观察(注意看虚函数表中第一个函数的地址,名称与测试代码输出一致)
上面介绍并验证了基本的C++对象模型,引入继承之后,C++对象模型又是怎样的?
C++对象模型中加入单继承
不管是单继承、多继承,还是虚继承,如果基于“简单对象模型”,每一个基类都可以被派生类中的一个slot指出,该slot内包含基类对象的地址。这个机制的主要缺点是,因为间接性而导致空间和存取时间上的额外负担;优点则是派生类对象的大小不会因其基类的改变而受影响。
如果基于“表格驱动模型”,派生类中有一个slot指向基类表,表格中的每一个slot含一个相关的基类地址(这个很像虚函数表,内含每一个虚函数的地址)。这样每个派生类对象汗一个bptr,它会被初始化,指向其基类表。这种策略的主要缺点是由于间接性而导致的空间和存取时间上的额外负担;优点则是在每一个派生类对象中对继承都有一致的表现方式,每一个派生类对象都应该在某个固定位置上放置一个基类表指针,与基类的大小或数量无关。第二个优点是,不需要改变派生类对象本身,就可以放大,缩小、或更改基类表。
不管上述哪一种机制,“间接性”的级数都将因为集成的深度而增加。C++实际模型是,对于一般继承是扩充已有存在的虚函数表;对于虚继承添加一个虚函数表指针。
无重写的单继承
无重写,即派生类中没有于基类同名的虚函数。
Derived类: |
#pragma once #include "base.h" class Derived : public Base { public: Derived(int); virtual ~Derived(void); virtual void derived_print(void); protected: int iDerived; }; |
Base、Derived的类图如下所示:
Base的模型跟上面的一样,不受继承的影响。Derived不是虚继承,所以是扩充已存在的虚函数表,所以结构如下图所示:
为了验证上述C++对象模型,我们编写如下测试代码。
测试代码: |
void test_single_inherit_norewrite() { Derived d(9999); cout << "对象d的起始内存地址:" << &d << endl; cout << "type_info信息:" << ((int*)*(int*)(&d) - 1) << endl; RTTICompleteObjectLocator str= *((RTTICompleteObjectLocator*)*((int*)*(int*)(&d) - 1)); //abstract class name from RTTI string classname(str.pTypeDescriptor->name); classname = classname.substr(4,classname.find("@@")-4); cout << classname <<endl; cout << "虚函数表地址:\t\t\t" << (int*)(&d) << endl; cout << "虚函数表 — 第1个函数地址:\t" << (int*)*(int*)(&d) << "\t即析构函数地址" << endl; cout << "虚函数表 — 第2个函数地址:\t" << ((int*)*(int*)(&d) + 1) << "\t"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&d)) + 1); pFun(); d.print(); cout << endl; cout << "虚函数表 — 第3个函数地址:\t" << ((int*)*(int*)(&d) + 2) << "\t"; pFun = (Fun)*(((int*)*(int*)(&d)) + 2); pFun(); d.derived_print(); cout << endl; cout << "推测数据成员iBase地址:\t\t" << ((int*)(&d) +1) << "\t通过地址取得的值:" << *((int*)(&d) +1) << endl; cout << "推测数据成员iDerived地址:\t" << ((int*)(&d) +2) << "\t通过地址取得的值:" << *((int*)(&d) +2) << endl; } |
输出结果如下图所示:
有重写的单继承
派生类中重写了基类的print()函数。
Derived_Overwrite类: |
#pragma once #include "base.h" class Derived_Overrite : public Base { public: Derived_Overrite(int); virtual ~Derived_Overrite(void); virtual void print(void) const; protected: int iDerived; }; |
Base、Derived_Overwrite的类图如下所示:
重写print()函数在虚函数表中表现如下:
为了验证上述C++对象模型,我们编写如下测试代码。
测试代码: |
void test_single_inherit_rewrite() { Derived_Overrite d(111111); cout << "对象d的起始内存地址:\t\t" << &d << endl; cout << "虚函数表地址:\t\t\t" << (int*)(&d) << endl; cout << "虚函数表 — 第1个函数地址:\t" << (int*)*(int*)(&d) << "\t即析构函数地址" << endl; cout << "虚函数表 — 第2个函数地址:\t" << ((int*)*(int*)(&d) + 1) << "\t"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&d)) + 1); pFun(); d.print(); cout << endl; cout << "虚函数表 — 第3个函数地址:\t" << *((int*)*(int*)(&d) + 2) << "【结束】\t"; cout << endl; cout << "推测数据成员iBase地址:\t\t" << ((int*)(&d) +1) << "\t通过地址取得的值:" << *((int*)(&d) +1) << endl; cout << "推测数据成员iDerived地址:\t" << ((int*)(&d) +2) << "\t通过地址取得的值:" << *((int*)(&d) +2) << endl; } |
输出结果如下图所示:
特别注意下,前面的模型虚函数表中最后一项没有打印出来,本实例中共2个虚函数,打印虚函数表第3项为0。其实虚函数表以0x0000000结束,类似字符串以’\0’结束。
C++对象模型中加入多继承
从单继承可以知道,派生类中只是扩充了基类的虚函数表。如果是多继承的话,又是如何扩充的?
1) 每个基类都有自己的虚表。
2) 子类的成员函数被放到了第一个基类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个基类的虚表中的print()函数都被overwrite成了子类的print ()。这样做就是为了解决不同的基类类型的指针指向同一个子类实例,而能够调用到实际的函数。
上面3个类,Derived_Mutlip_Inherit继承自Base、Base_1两个类,Derived_Mutlip_Inherit的结构如下所示:
为了验证上述C++对象模型,我们编写如下测试代码。
测试代码: |
void test_multip_inherit() { Derived_Mutlip_Inherit dmi(3333); cout << "对象dmi的起始内存地址:\t\t" << &dmi << endl; cout << "虚函数表_vptr_Base地址:\t" << (int*)(&dmi) << endl; cout << "_vptr_Base — 第1个函数地址:\t" << (int*)*(int*)(&dmi) << "\t即析构函数地址" << endl; cout << "_vptr_Base — 第2个函数地址:\t" << ((int*)*(int*)(&dmi) + 1) <<"\t"; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&dmi)) + 1); pFun(); cout << endl; cout << "_vptr_Base — 第3个函数地址:\t" << ((int*)*(int*)(&dmi) + 2) <<"\t"; pFun = (Fun)*(((int*)*(int*)(&dmi)) + 2); pFun(); cout << endl; cout << "_vptr_Base — 第4个函数地址:\t" << *((int*)*(int*)(&dmi) + 3) <<"【结束】\t"; cout << endl; cout << "推测数据成员iBase地址:\t\t" << ((int*)(&dmi) +1) << "\t通过地址取得的值:" << *((int*)(&dmi) +1) << endl; SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY | FOREGROUND_GREEN); cout << "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" << endl; SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY | FOREGROUND_RED); cout << "虚函数表_vptr_Base1地址:\t" << ((int*)(&dmi) +2) << endl; cout << "_vptr_Base1 — 第1个函数地址:\t" << (int*)*((int*)(&dmi) +2) <<"\t即析构函数地址" << endl; cout << "_vptr_Base1 — 第2个函数地址:\t" << ((int*)*((int*)(&dmi) +2) + 1) << "\t"; typedef void(*Fun)(void); pFun = (Fun)*((int*)*((int*)(&dmi) +2) + 1); pFun(); cout << endl; cout << "_vptr_Base1 — 第3个函数地址:\t" << *((int*)*(int*)((int*)(&dmi) +2) + 2) << "【结束】\t"; cout << endl; cout << "推测数据成员iBase1地址:\t" << ((int*)(&dmi) +3) << "\t通过地址取得的值:" << *((int*)(&dmi) +3) << endl; SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY | FOREGROUND_GREEN); cout << "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" << endl; SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY | FOREGROUND_RED); cout << "推测数据成员iDerived地址:\t" << ((int*)(&dmi) +4) << "\t通过地址取得的值:" << *((int*)(&dmi) +4) << endl; } |
输出结果如下图所示:
C++对象模型中加入虚继承
虚继承是为了解决重复继承中多个间接父类的问题的,所以不能使用上面简单的扩充并为每个虚基类提供一个虚函数指针(这样会导致重复继承的基类会有多个虚函数表)形式。
虚继承的派生类的内存结构,和普通继承完全不同。虚继承的子类,有单独的虚函数表,另外也单独保存一份父类的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。派生类的内存中,首先是自己的虚函数表,然后是派生类的数据成员,然后是0x0,之后就是基类的虚函数表,之后是基类的数据成员。
如果派生类没有自己的虚函数,那么派生类就不会有虚函数表,但是派生类数据和基类数据之间,还是需要0x0来间隔。
因此,在虚继承中,派生类和基类的数据,是完全间隔的,先存放派生类自己的虚函数表和数据,中间以0x分界,最后保存基类的虚函数和数据。如果派生类重载了父类的虚函数,那么则将派生类内存中基类虚函数表的相应函数替换。
简单虚继承(无重复继承情况)
简单虚继承的2个类Base、Derived_Virtual_Inherit1的关系如下所示:
Derived_Virtual_Inherit1的对象模型如下图:
为了验证上述C++对象模型,我们编写如下测试代码。
测试代码: |
void test_single_vitrual_inherit() { Derived_Virtual_Inherit1 dvi1(88888); cout << "对象dvi1的起始内存地址:\t\t" << &dvi1 << endl; cout << "虚函数表_vptr_Derived..地址:\t\t" << (int*)(&dvi1) << endl; cout << "_vptr_Derived — 第1个函数地址:\t" << (int*)*(int*)(&dvi1) << endl; typedef void(*Fun)(void); Fun pFun = (Fun)*((int*)*(int*)(&dvi1)); pFun(); cout << endl; cout << "_vptr_Derived — 第2个函数地址:\t" << *((int*)*(int*)(&dvi1) + 1) << "【结束】\t"; cout << endl; cout << "=======================:\t" << ((int*)(&dvi1) +1) << "\t通过地址取得的值:" << (int*)*((int*)(&dvi1) +1) << "\t" <<*(int*)*((int*)(&dvi1) +1) << endl; cout << "推测数据成员iDerived地址:\t" << ((int*)(&dvi1) +2) << "\t通过地址取得的值:" << *((int*)(&dvi1) +2) << endl; cout << "=======================:\t" << ((int*)(&dvi1) +3) << "\t通过地址取得的值:" << *((int*)(&dvi1) +3) << endl; cout << "虚函数表_vptr_Base地址:\t" << ((int*)(&dvi1) +4) << endl; cout << "_vptr_Base — 第1个函数地址:\t" << (int*)*((int*)(&dvi1) +4) << "\t即析构函数地址" << endl; cout << "_vptr_Base — 第2个函数地址:\t" << ((int*)*((int*)(&dvi1) +4) +1) << "\t"; pFun = (Fun)*((int*)*((int*)(&dvi1) +4) +1); pFun(); cout << endl; cout << "_vptr_Base — 第3个函数地址:\t" << ((int*)*((int*)(&dvi1) +4) +2) << "【结束】\t" << *((int*)*((int*)(&dvi1) +4) +2); cout << endl; cout << "推测数据成员iBase地址:\t\t" << ((int*)(&dvi1) +5) << "\t通过地址取得的值:" << *((int*)(&dvi1) +5) << endl; } |
输出结果如下图所示:
菱形继承(含重复继承、多继承情况)
菱形继承关系如下图:
Derived_Virtual的对象模型如下图:
为了验证上述C++对象模型,我们编写如下测试代码。
测试代码: |
void test_multip_vitrual_inherit() { Derived_Virtual dvi1(88888); cout << "对象dvi1的起始内存地址:\t\t" << &dvi1 << endl; cout << "虚函数表_vptr_inherit1地址:\t\t" << (int*)(&dvi1) << endl; cout << "_vptr_inherit1 — 第1个函数地址:\t" << (int*)*(int*)(&dvi1) << endl; typedef void(*Fun)(void); Fun pFun = (Fun)*((int*)*(int*)(&dvi1)); pFun(); cout << endl; cout << "_vptr_inherit1 — 第2个函数地址:\t" << ((int*)*(int*)(&dvi1) + 1) << endl; pFun = (Fun)*((int*)*(int*)(&dvi1) + 1); pFun(); cout << endl; cout << "_vptr_inherit1 — 第3个函数地址:\t" << ((int*)*(int*)(&dvi1) + 2) << "\t通过地址取得的值:" << *((int*)*(int*)(&dvi1) + 2) << "【结束】\t"; cout << endl; cout << "======指向=============:\t" << ((int*)(&dvi1) +1) << "\t通过地址取得的值:" << (int*)*((int*)(&dvi1) +1)<< "\t" <<*(int*)*((int*)(&dvi1) +1) << endl; cout << "推测数据成员iInherit1地址:\t" << ((int*)(&dvi1) +2) << "\t通过地址取得的值:" << *((int*)(&dvi1) +2) << endl; // cout << "虚函数表_vptr_inherit2地址:\t" << ((int*)(&dvi1) +3) << endl; cout << "_vptr_inherit2 — 第1个函数地址:\t" << (int*)*((int*)(&dvi1) +3) << endl; pFun = (Fun)*((int*)*((int*)(&dvi1) +3)); pFun(); cout << endl; cout << "_vptr_inherit2 — 第2个函数地址:\t" << (int*)*((int*)(&dvi1) +3) + 1 <<"\t通过地址取得的值:" << *((int*)*((int*)(&dvi1) +3) + 1) <<"【结束】\t" << endl; cout << endl; cout << "======指向=============:\t" << ((int*)(&dvi1) +4) << "\t通过地址取得的值:" << (int*)*((int*)(&dvi1) +4) << "\t" <<*(int*)*((int*)(&dvi1) +4)<< endl; cout << "推测数据成员iInherit2地址:\t" << ((int*)(&dvi1) +5) << "\t通过地址取得的值:" << *((int*)(&dvi1) +5) << endl; cout << "推测数据成员iDerived地址:\t" << ((int*)(&dvi1) +6) << "\t通过地址取得的值:" << *((int*)(&dvi1) +6) << endl; cout << "=======================:\t" << ((int*)(&dvi1) +7) << "\t通过地址取得的值:" << *((int*)(&dvi1) +7) << endl; // cout << "虚函数表_vptr_Base地址:\t" << ((int*)(&dvi1) +8) << endl; cout << "_vptr_Base — 第1个函数地址:\t" << (int*)*((int*)(&dvi1) +8) << "\t即析构函数地址" << endl; cout << "_vptr_Base — 第2个函数地址:\t" << ((int*)*((int*)(&dvi1) +8) +1) << "\t"; pFun = (Fun)*((int*)*((int*)(&dvi1) +8) +1); pFun(); cout << endl; cout << "_vptr_Base — 第3个函数地址:\t" << ((int*)*((int*)(&dvi1) +8) +2) << "【结束】\t" << *((int*)*((int*)(&dvi1) +8) +2); cout << endl; cout << "推测数据成员iBase地址:\t\t" << ((int*)(&dvi1) +9) << "\t通过地址取得的值:" << *((int*)(&dvi1) +9) << endl; } |
输出结果如下图所示:
至此,C++对象模型介绍的差不多了,清楚了C++对象模型之后,很多疑问就能迎刃而解了。下面结合模型介绍一些典型问题。
如何访问成员?
前面介绍了C++对象模型,下面介绍C++对象模型的对访问成员的影响。其实清楚了C++对象模型,就清楚了成员访问机制。下面分别针对数据成员和函数成员是如何访问到的,给出一个大致介绍。
对象大小问题
其中:3个类中的函数都是虚函数
l Derived继承Base
l Derived_Virtual虚继承Base
测试对象大小: |
void test_size() { Base b; Derived d; Derived_Virtual dv; cout << "sizeof(b):\t" << sizeof(b) << endl; cout << "sizeof(d):\t" << sizeof(d) << endl; cout << "sizeof(dv):\t" << sizeof(dv) << endl; } |
输出如下:
因为Base中包含虚函数表指针,所有size为4;Derived继承Base,只是扩充基类的虚函数表,不会新增虚函数表指针,所以size也是4;Derived_Virtual虚继承Base,根据前面的模型知道,派生类有自己的虚函数表及指针,并且有分隔符(0x00000000),然后才是虚基类的虚函数表等信息,故大小为4+4+4=12。
空类Empty: |
#pragma once class Empty { public: Empty(void); ~Empty(void); }; |
Empty p,sizeof(p)的大小是多少?事实上并不是空的,它有一个隐晦的1byte,那是被编译器安插进去的一个char。这将使得这个class的两个对象得以在内中有独一无二的地址。
数据成员如何访问(直接取址)
跟实际对象模型相关联,根据对象起始地址+偏移量取得。
静态绑定与动态绑定
程序调用函数时,将使用那个可执行代码块呢?编译器负责回答这个问题。将源代码中的函数调用解析为执行特定的函数代码块被称为函数名绑定(binding,又称联编)。在C语言中,这非常简单,因为每个函数名都对应一个不同的额函数。在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而编译器可以再编译过程中完成这种绑定,这称为静态绑定(static binding),又称为早期绑定(early binding)。
然而虚函数是这项工作变得更加困难。使用哪一个函数不是能在编译阶段时确定的,因为编译器不知道用户将选择哪种类型。所以,编译器必须能够在程序运行时选择正确的虚函数的代码,这被称为动态绑定(dynamic binding),又称为晚期绑定(late binding)。
使用虚函数是有代价的,在内存和执行速度方面是有一定成本的,包括:
l 每个对象都将增大,增大量为存储虚函数表指针的大小;
l 对于每个类,编译器都创建一个虚函数地址表;
l 对于每个函数调用,都需要执行一项额外的操作,即到虚函数表中查找地址。
虽然非虚函数比虚函数效率稍高,单不具备动态联编能力。
函数成员如何访问(间接取址)
跟实际对象模型相关联,普通函数(nonstatic、static)根据编译、链接的结果直接获取函数地址;如果是虚函数根据对象模型,取出对于虚函数地址,然后在虚函数表中查找函数地址。
多态如何实现?
多态的实现
多态(Polymorphisn)在C++中是通过虚函数实现的。通过前面的模型【参见“有重写的单继承”】知道,如果类中有虚函数,编译器就会自动生成一个虚函数表,对象中包含一个指向虚函数表的指针。能够实现多态的关键在于:虚函数是允许被派生类重写的,在虚函数表中,派生类函数对覆盖(override)基类函数。除此之外,还必须通过指针或引用调用方法才行,将派生类对象赋给基类对象。
上面2个类,基类Base、派生类Derived中都包含下面2个方法:
void print() const;
virtual void print_virtual() const;
这个2个方法的区别就在于一个是普通成员函数,一个是虚函数。编写测试代码如下:
测试多态代码: |
void test_polmorphisn() { Base b; Derived d; b = d; b.print(); b.print_virtual(); Base *p; p = &d; p->print(); p->print_virtual(); } |
根据模型推测只有p->print_virtual()才实现了动态,其他3调用都是调用基类的方法。原因如下:
l b.print();b.print_virtual();不能实现多态是因为通过基类对象调用,而非指针或引用所以不能实现多态。
l p->print();不能实现多态是因为,print函数没有声明为虚函数(virtual),派生类中也定义了print函数只是隐藏了基类的print函数。
为什么析构函数设为虚函数是必要的
析构函数应当都是虚函数,除非明确该类不做基类(不被其他类继承)。基类的析构函数声明为虚函数,这样做是为了确保释放派生对象时,按照正确的顺序调用析构函数。
从前面介绍的C++对象模型可以知道,如果析构函数不定义为虚函数,那么派生类就不会重写基类的析构函数,在有多态行为的时候,派生类的析构函数不会被调用到(有内存泄漏的风险!)。
例如,通过new一个派生类对象,赋给基类指针,然后delete基类指针。
测试析构函数: |
void test_vitual_destructor() { Base *p = new Derived(); delete p; } |
如果基类的析构函数不是析构函数:
注意,缺少了派生类的析构函数调用。把析构函数声明为虚函数,调用就正常了:
PART2
前言
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家 一个清晰的剖析。
当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见。
言归正传,让我们一起进入虚函数的世界。
虚函数表
对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。 没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
按照上面的说法,我们可以通过Base的实例来得到虚函数表。 下面是实际例程:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
实际运行经果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
这个时候你应该懂了吧。什么?还是有点晕。也是,这样的代码看着太乱了。没问题,让我画个图解释一下。如下所示:
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性
每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1->f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)
二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
如:
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
结束语
C++这门语言是一门Magic的语言,对于程序员来说,我们似乎永远摸不清楚这门语言背着我们在干了什么。需要熟悉这门语言,我们就必需要了解C++里面的那些东西,需要去了解C++中那些危险的东西。不然,这是一种搬起石头砸自己脚的编程语言。
在文章束之前还是介绍一下自己吧。我从事软件研发有十个年头了,目前是软件开发技术主管,技术方面,主攻Unix/C/C++,比较喜欢网络上的技术,比如分布式计算,网格计算,P2P,Ajax等一切和互联网相关的东西。管理方面比较擅长于团队建设,技术趋势分析,项目管理。欢迎大家和我交流,我的MSN和Email是:haoel@hotmail.com
附录一:VC中查看虚函数表
我们可以在VC的IDE环境中的Debug状态下展开类的实例就可以看到虚函数表了(并不是很完整的)
附录 二:例程
下面是一个关于多重继承的虚函数表访问的例程:
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
virtual void f() { cout << "Derive::f" << endl; }
virtual void g1() { cout << "Derive::g1" << endl; }
};
typedef void(*Fun)(void);
int main()
{
Fun pFun = NULL;
Derive d;
int** pVtab = (int**)&d;
//Base1's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);
pFun = (Fun)pVtab[0][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);
pFun = (Fun)pVtab[0][1];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);
pFun = (Fun)pVtab[0][2];
pFun();
//Derive's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);
pFun = (Fun)pVtab[0][3];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[0][4];
cout<<pFun<<endl;
//Base2's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
pFun = (Fun)pVtab[1][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
pFun = (Fun)pVtab[1][1];
pFun();
pFun = (Fun)pVtab[1][2];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[1][3];
cout<<pFun<<endl;
//Base3's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
pFun = (Fun)pVtab[2][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
pFun = (Fun)pVtab[2][1];
pFun();
pFun = (Fun)pVtab[2][2];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[2][3];
cout<<pFun<<endl;
return 0;
}
PART3
前言
07年12月,我写了一篇《C++虚函数表解析》的文章,引起了大家的兴趣。有很多朋友对我的文章留了言,有鼓励我的,有批评我的,还有很多问问题的。我在这里一并对大家的留言表示感谢。这也是我为什么再写一篇续言的原因。因为,在上一篇文章中,我用了的示例都是非常简单的,主要是为了说明一些机理上的问题,也是为了图一些表达上方便和简单。不想,这篇文章成为了打开C++对象模型内存布局的一个引子,引发了大家对C++对象的更深层次的讨论。当然,我之前的文章还有很多方面没有涉及,从我个人感觉下来,在谈论虚函数表里,至少有以下这些内容没有涉及:
1)有成员变量的情况。
2)有重复继承的情况。
3)有虚拟继承的情况。
4)有钻石型虚拟继承的情况。
这些都是我本篇文章需要向大家说明的东西。所以,这篇文章将会是《C++虚函数表解析》的一个续篇,也是一篇高级进阶的文章。我希望大家在读这篇文章之前对C++有一定的基础和了解,并能先读我的上一篇文章。因为这篇文章的深度可能会比较深,而且会比较杂乱,我希望你在读本篇文章时不会有大脑思维紊乱导致大脑死机的情况。;-)
对象的影响因素
简而言之,我们一个类可能会有如下的影响因素:
1)成员变量
2)虚函数(产生虚函数表)
3)单一继承(只继承于一个类)
4)多重继承(继承多个类)
5)重复继承(继承的多个父类中其父类有相同的超类)
6)虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)
上述的东西通常是C++这门语言在语义方面对对象内部的影响因素,当然,还会有编译器的影响(比如优化),还有字节对齐的影响。在这里我们都不讨论,我们只讨论C++语言上的影响。
本篇文章着重讨论下述几个情况下的C++对象的内存布局情况。
1)单一的一般继承(带成员变量、虚函数、虚函数覆盖)
2)单一的虚拟继承(带成员变量、虚函数、虚函数覆盖)
3)多重继承(带成员变量、虚函数、虚函数覆盖)
4)重复多重继承(带成员变量、虚函数、虚函数覆盖)
5)钻石型的虚拟多重继承(带成员变量、虚函数、虚函数覆盖)
我们的目标就是,让事情越来越复杂。
知识复习
我们简单地复习一下,我们可以通过对象的地址来取得虚函数表的地址,如:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
我们同样可以用这种方式来取得整个对象实例的内存布局。因为这些东西在内存中都是连续分布的,我们只需要使用适当的地址偏移量,我们就可以获得整个内存对象的布局。
本篇文章中的例程或内存布局主要使用如下编译器和系统:
1)Windows XP 和 VC++ 2003
2)Cygwin 和 G++ 3.4.4
单一的一般继承
下面,我们假设有如下所示的一个继承关系:
请注意,在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而了类覆盖了父类的f()方法,孙子类覆盖了子类的g_child()及其超类的f()。
我们的源程序如下所示:
class Parent {
public:
int iparent;
Parent ():iparent (10) {}
virtual void f() { cout << " Parent::f()" << endl; }
virtual void g() { cout << " Parent::g()" << endl; }
virtual void h() { cout << " Parent::h()" << endl; }
};
class Child : public Parent {
public:
int ichild;
Child():ichild(100) {}
virtual void f() { cout << "Child::f()" << endl; }
virtual void g_child() { cout << "Child::g_child()" << endl; }
virtual void h_child() { cout << "Child::h_child()" << endl; }
};
class GrandChild : public Child{
public:
int igrandchild;
GrandChild():igrandchild(1000) {}
virtual void f() { cout << "GrandChild::f()" << endl; }
virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};
我们使用以下程序作为测试程序:(下面程序中,我使用了一个int** pVtab 来作为遍历对象内存布局的指针,这样,我就可以方便地像使用数组一样来遍历所有的成员包括其虚函数表了,在后面的程序中,我也是用这样的方法的,请不必感到奇怪,)
typedef void(*Fun)(void);
GrandChild gc;
int** pVtab = (int**)&gc;
cout << "[0] GrandChild::_vptr->" << endl;
for (int i=0; (Fun)pVtab[0][i]!=NULL; i++){
pFun = (Fun)pVtab[0][i];
cout << " ["<<i<<"] ";
pFun();
}
cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl;
cout << "[2] Child.ichild = " << (int)pVtab[2] << endl;
cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl;
其运行结果如下所示:(在VC++ 2003和G++ 3.4.4下)
[0] GrandChild::_vptr-> [0] GrandChild::f() [1] Parent::g() [2] Parent::h() [3] GrandChild::g_child() [4] Child::h1() [5] GrandChild::h_grandchild() [1] Parent.iparent = 10 [2] Child.ichild = 100 [3] GrandChild.igrandchild = 1000 |
使用图片表示如下:
可见以下几个方面:
1)虚函数表在最前面的位置。
2)成员变量根据其继承和声明顺序依次放在后面。
3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。
多重继承
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记来标明子类的虚函数表)。而且每个类中都有一个自己的成员变量:
我们的类继承的源代码如下所示:父类的成员初始为10,20,30,子类的为100
class Base1 {
public:
int ibase1;
Base1():ibase1(10) {}
virtual void f() { cout << "Base1::f()" << endl; }
virtual void g() { cout << "Base1::g()" << endl; }
virtual void h() { cout << "Base1::h()" << endl; }
};
class Base2 {
public:
int ibase2;
Base2():ibase2(20) {}
virtual void f() { cout << "Base2::f()" << endl; }
virtual void g() { cout << "Base2::g()" << endl; }
virtual void h() { cout << "Base2::h()" << endl; }
};
class Base3 {
public:
int ibase3;
Base3():ibase3(30) {}
virtual void f() { cout << "Base3::f()" << endl; }
virtual void g() { cout << "Base3::g()" << endl; }
virtual void h() { cout << "Base3::h()" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
int iderive;
Derive():iderive(100) {}
virtual void f() { cout << "Derive::f()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
};
我们通过下面的程序来查看子类实例的内存布局:下面程序中,注意我使用了一个s变量,其中用到了sizof(Base)来找下一个类的偏移量。(因为我声明的是int成员,所以是4个字节,所以没有对齐问题。关于内存的对齐问题,大家可以自行试验,我在这里就不多说了)
typedef void(*Fun)(void);
Derive
d;
int**
pVtab = (int**)&d;
cout
<< "[0] Base1::_vptr->" << endl;
pFun
= (Fun)pVtab[0][0];
cout
<< " [0] ";
pFun();
pFun
= (Fun)pVtab[0][1];
cout
<< " [1] ";pFun();
pFun
= (Fun)pVtab[0][2];
cout
<< " [2] ";pFun();
pFun
= (Fun)pVtab[0][3];
cout
<< " [3] "; pFun();
pFun
= (Fun)pVtab[0][4];
cout
<< " [4]
"; cout<<pFun<<endl;
cout
<< "[1] Base1.ibase1 = " << (int)pVtab[1]
<< endl;
int s
= sizeof(Base1)/4;
cout
<< "[" << s << "]
Base2::_vptr->"<<endl;
pFun
= (Fun)pVtab[s][0];
cout
<< " [0] "; pFun();
Fun
= (Fun)pVtab[s][1];
cout
<< " [1] "; pFun();
pFun
= (Fun)pVtab[s][2];
cout
<< " [2] "; pFun();
pFun
= (Fun)pVtab[s][3];
out
<< " [3] ";
cout<<pFun<<endl;
cout
<< "["<< s+1 <<"] Base2.ibase2 =
" << (int)pVtab[s+1] << endl;
s
= s + sizeof(Base2)/4;
cout
<< "[" << s << "]
Base3::_vptr->"<<endl;
pFun
= (Fun)pVtab[s][0];
cout
<< " [0] "; pFun();
pFun
= (Fun)pVtab[s][1];
cout
<< " [1] "; pFun();
pFun
= (Fun)pVtab[s][2];
cout
<< " [2] "; pFun();
pFun
= (Fun)pVtab[s][3];
cout
<< " [3] ";
cout<<pFun<<endl;
s++;
cout
<< "["<< s <<"] Base3.ibase3 =
" << (int)pVtab[s] << endl;
s++;
cout
<< "["<< s <<"] Derive.iderive =
" << (int)pVtab[s] << endl;
其运行结果如下所示:(在VC++ 2003和G++ 3.4.4下)
[0] Base1::_vptr-> [0] [1] [2] [3] [4] [1] Base1.ibase1 = 10 [2] Base2::_vptr-> [0] [1] [2] [3] [3] Base2.ibase2 = 20 [4] Base3::_vptr-> [0] [1] [2] [3] 00000000 [5] Base3.ibase3 = 30 [6] Derive.iderive = 100 |
使用图片表示是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
PART3
重复继承
下面我们再来看看,发生重复继承的情况。所谓重复继承,也就是某个基类被间接地重复继承了多次。
下图是一个继承图,我们重载了父类的f()函数。
其类继承的源代码如下所示。其中,每个类都有两个变量,一个是整形(4字节),一个是字符(1字节),而且还有自己的虚函数,自己overwrite父类的虚函数。如子类D中,f()覆盖了超类的函数,f1() 和f2() 覆盖了其父类的虚函数,Df()为自己的虚函数。
class B
{
public:
int ib;
char cb;
public:
B():ib(0),cb('B')
{}
virtual void f()
{ cout << "B::f()" << endl;}
virtual void Bf()
{ cout << "B::Bf()" << endl;}
};
class B1 : public B
{
public:
int ib1;
char cb1;
public:
B1():ib1(11),cb1('1')
{}
virtual void f()
{ cout << "B1::f()" << endl;}
virtual void f1()
{ cout << "B1::f1()" << endl;}
virtual void Bf1()
{ cout << "B1::Bf1()" << endl;}
};
class B2: public B
{
public:
int ib2;
char cb2;
public:
B2():ib2(12),cb2('2')
{}
virtual void f()
{ cout << "B2::f()" << endl;}
virtual void f2()
{ cout << "B2::f2()" << endl;}
virtual void Bf2()
{ cout << "B2::Bf2()" << endl;}
};
class D : public B1, public B2
{
public:
int id;
char cd;
public:
D():id(100),cd('D')
{}
virtual void f()
{ cout << "D::f()" << endl;}
virtual void f1()
{ cout << "D::f1()" << endl;}
virtual void f2()
{ cout << "D::f2()" << endl;}
virtual void Df()
{ cout << "D::Df()" << endl;}
};
我们用来存取子类内存布局的代码如下所示:(在VC++
2003和G++ 3.4.4下)
typedef void(*Fun)(void);
int** pVtab = NULL;
Fun pFun = NULL;
D d;
pVtab =
(int**)&d;
cout
<< "[0] D::B1::_vptr->" << endl;
pFun =
(Fun)pVtab[0][0];
cout
<< " [0]
"; pFun();
pFun =
(Fun)pVtab[0][1];
cout
<< " [1]
"; pFun();
pFun =
(Fun)pVtab[0][2];
cout
<< " [2]
"; pFun();
pFun =
(Fun)pVtab[0][3];
cout
<< " [3]
"; pFun();
pFun =
(Fun)pVtab[0][4];
cout
<< " [4]
"; pFun();
pFun =
(Fun)pVtab[0][5];
cout
<< " [5] 0x" <<
pFun << endl;
cout
<< "[1] B::ib = " << (int)pVtab[1] <<
endl;
cout
<< "[2] B::cb = " << (char)pVtab[2] <<
endl;
cout
<< "[3] B1::ib1 = " << (int)pVtab[3] <<
endl;
cout
<< "[4] B1::cb1 = " << (char)pVtab[4] <<
endl;
cout
<< "[5] D::B2::_vptr->" << endl;
pFun =
(Fun)pVtab[5][0];
cout
<< " [0]
"; pFun();
pFun = (Fun)pVtab[5][1];
cout
<< " [1]
"; pFun();
pFun =
(Fun)pVtab[5][2];
cout
<< " [2]
"; pFun();
pFun =
(Fun)pVtab[5][3];
cout
<< " [3]
"; pFun();
pFun =
(Fun)pVtab[5][4];
cout
<< " [4] 0x" <<
pFun << endl;
cout
<< "[6] B::ib = " << (int)pVtab[6] <<
endl;
cout
<< "[7] B::cb = " << (char)pVtab[7] <<
endl;
cout
<< "[8] B2::ib2 = " << (int)pVtab[8] <<
endl;
cout
<< "[9] B2::cb2 = " << (char)pVtab[9] <<
endl;
cout
<< "[10] D::id = " << (int)pVtab[10] <<
endl;
cout
<< "[11] D::cd = " << (char)pVtab[11] <<
endl;
程序运行结果如下:
GCC 3.4.4 |
VC++ 2003 |
[0] D::B1::_vptr-> [0] D::f() [1] B::Bf() [2] D::f1() [3] [4] D::f2() [5] 0x1 [1] B::ib = 0 [2] B::cb = B [3] B1::ib1 = 11 [4] B1::cb1 = 1 [5] D::B2::_vptr-> [0] D::f() [1] B::Bf() [2] D::f2() [3] [4] 0x0 [6] B::ib = 0 [7] B::cb = B [8] B2::ib2 = 12 [9] B2::cb2 = 2 [10] D::id = 100 [11] D::cd = D |
[0] D::B1::_vptr-> [0] D::f() [1] B::Bf() [2] D::f1() [3] [4] D::Df() [5] [1] B::ib = 0 [2] B::cb = B [3] B1::ib1 = 11 [4] B1::cb1 = 1 [5] D::B2::_vptr-> [0] D::f() [1] B::Bf() [2] D::f2() [3] [4] 0x00000000 [6] B::ib = 0 [7] B::cb = B [8] B2::ib2 = 12 [9] B2::cb2 = 2 [10] D::id = 100 [11] D::cd = D |
下面是对于子类实例中的虚函数表的图:
我们可以看见,最顶端的父类B其成员变量存在于B1和B2中,并被D给继承下去了。而在D中,其有B1和B2的实例,于是B的成员在D的实例中存在两份,一份是B1继承而来的,另一份是B2继承而来的。所以,如果我们使用以下语句,则会产生二义性编译错误:
D d;
d.ib =
0; //二义性错误
d.B1::ib =
1; //正确
d.B2::ib =
2; //正确
注意,上面例程中的最后两条语句存取的是两个变量。虽然我们消除了二义性的编译错误,但B类在D中还是有两个实例,这种继承造成了数据的重复,我们叫这种继承为重复继承。重复的基类数据成员可能并不是我们想要的。所以,C++引入了虚基类的概念。
钻石型多重虚拟继承
虚拟继承的出现就是为了解决重复继承中多个间接父类的问题的。钻石型的结构是其最经典的结构。也是我们在这里要讨论的结构:
上述的“重复继承”只需要把B1和B2继承B的语法中加上virtual 关键,就成了虚拟继承,其继承图如下所示:
上图和前面的“重复继承”中的类的内部数据和接口都是完全一样的,只是我们采用了虚拟继承:其省略后的源码如下所示:
class B {……};
class B1
: virtual public B{……};
class B2: virtual public B{……};
class D
: public B1, public B2{ …… };
在查看D之前,我们先看一看单一虚拟继承的情况。下面是一段在VC++2003下的测试程序:(因为VC++和GCC的内存而局上有一些细节上的不同,所以这里只给出VC++的程序,GCC下的程序大家可以根据我给出的程序自己仿照着写一个去试一试):
int** pVtab = NULL;
Fun pFun = NULL;
B1 bb1;
pVtab =
(int**)&bb1;
cout
<< "[0] B1::_vptr->" << endl;
pFun =
(Fun)pVtab[0][0];
cout
<< " [0] ";
pFun(); //B1::f1();
cout
<< " [1] ";
pFun =
(Fun)pVtab[0][1];
pFun(); //B1::bf1();
cout
<< " [2] ";
cout <<
pVtab[0][2] << endl;
cout
<< "[1] = 0x";
cout <<
(int*)*((int*)(&bb1)+1) <<endl; //B1::ib1
cout
<< "[2] B1::ib1 = ";
cout <<
(int)*((int*)(&bb1)+2) <<endl; //B1::ib1
cout
<< "[3] B1::cb1 = ";
cout <<
(char)*((int*)(&bb1)+3) << endl; //B1::cb1
cout
<< "[4] = 0x";
cout <<
(int*)*((int*)(&bb1)+4) << endl; //NULL
cout
<< "[5] B::_vptr->" << endl;
pFun =
(Fun)pVtab[5][0];
cout
<< " [0] ";
pFun(); //B1::f();
pFun =
(Fun)pVtab[5][1];
cout
<< " [1] ";
pFun(); //B::Bf();
cout
<< " [2] ";
cout
<< "0x" << (Fun)pVtab[5][2] << endl;
cout
<< "[6] B::ib = ";
cout <<
(int)*((int*)(&bb1)+6) <<endl; //B::ib
cout
<< "[7] B::cb = ";
其运行结果如下(我结出了GCC的和VC++2003的对比):
GCC 3.4.4 |
VC++ 2003 |
[0] B1::_vptr -> [0] : B1::f() [1] : B1::f1() [2] : B1::Bf1() [3] : 0 [1] B1::ib1 : 11 [2] B1::cb1 : 1 [3] B::_vptr -> [0] : B1::f() [1] : B::Bf() [2] : 0 [4] B::ib : 0 [5] B::cb : B [6] NULL : 0 |
[0] B1::_vptr-> [0] [1] [2] 0 [1] = 0x00454310 ç该地址取值后是-4 [2] B1::ib1 = 11 [3] B1::cb1 = 1 [4] = 0x00000000 [5] B::_vptr-> [0] B1::f() [1] B::Bf() [2] [6] B::ib = 0 [7] B::cb = B |
这里,大家可以自己对比一下。关于细节上,我会在后面一并再说。
下面的测试程序是看子类D的内存布局,同样是VC++ 2003的(因为VC++和GCC的内存布局上有一些细节上的不同,而VC++的相对要清楚很多,所以这里只给出VC++的程序,GCC下的程序大家可以根据我给出的程序自己仿照着写一个去试一试):
D d;
pVtab =
(int**)&d;
cout
<< "[0] D::B1::_vptr->" << endl;
pFun =
(Fun)pVtab[0][0];
cout
<< " [0]
"; pFun(); //D::f1();
pFun =
(Fun)pVtab[0][1];
cout << " [1]
"; pFun(); //B1::Bf1();
pFun =
(Fun)pVtab[0][2];
cout
<< " [2]
"; pFun(); //D::Df();
pFun =
(Fun)pVtab[0][3];
cout
<< " [3] ";
cout << pFun
<< endl;
//cout <<
pVtab[4][2] << endl;
cout
<< "[1] = 0x";
cout
<< (int*)((&dd)+1) <<endl; //????
cout
<< "[2] B1::ib1 = ";
cout <<
*((int*)(&dd)+2) <<endl; //B1::ib1
cout
<< "[3] B1::cb1 = ";
cout <<
(char)*((int*)(&dd)+3) << endl; //B1::cb1
//---------------------
cout
<< "[4] D::B2::_vptr->" << endl;
pFun =
(Fun)pVtab[4][0];
cout
<< " [0]
"; pFun(); //D::f2();
pFun =
(Fun)pVtab[4][1];
cout
<< " [1]
"; pFun(); //B2::Bf2();
pFun =
(Fun)pVtab[4][2];
cout
<< " [2] ";
cout << pFun
<< endl;
cout
<< "[5] = 0x";
cout <<
*((int*)(&dd)+5) << endl; // ???
cout
<< "[6] B2::ib2 = ";
cout <<
(int)*((int*)(&dd)+6) <<endl; //B2::ib2
cout
<< "[7] B2::cb2 = ";
cout <<
(char)*((int*)(&dd)+7) << endl; //B2::cb2
cout
<< "[8] D::id = ";
cout <<
*((int*)(&dd)+8) << endl; //D::id
cout
<< "[9] D::cd = ";
cout <<
(char)*((int*)(&dd)+9) << endl;//D::cd
cout
<< "[10] = 0x";
cout <<
(int*)*((int*)(&dd)+10) << endl;
//---------------------
cout
<< "[11] D::B::_vptr->" << endl;
pFun =
(Fun)pVtab[11][0];
cout
<< " [0]
"; pFun(); //D::f();
pFun =
(Fun)pVtab[11][1];
cout
<< " [1]
"; pFun(); //B::Bf();
pFun =
(Fun)pVtab[11][2];
cout
<< " [2] ";
cout << pFun
<< endl;
cout
<< "[12] B::ib = ";
cout <<
*((int*)(&dd)+12) << endl; //B::ib
cout
<< "[13] B::cb = ";
cout <<
(char)*((int*)(&dd)+13) <<endl;//B::cb
下面给出运行后的结果(分VC++和GCC两部份)
GCC 3.4.4 |
VC++ 2003 |
[0] B1::_vptr -> [0] : D::f() [1] : D::f1() [2] : B1::Bf1() [3] : D::f2() [4] : D::Df() [5] : 1 [1] B1::ib1 : 11 [2] B1::cb1 : 1 [3] B2::_vptr -> [0] : D::f() [1] : D::f2() [2] : B2::Bf2() [3] : 0 [4] B2::ib2 : 12 [5] B2::cb2 : 2 [6] D::id : 100 [7] D::cd : D [8] B::_vptr -> [0] : D::f() [1] : B::Bf() [2] : 0 [9] B::ib : 0 [10] B::cb : B [11] NULL : 0 |
[0] D::B1::_vptr-> [0] D::f1() [1] [2] D::Df() [3] [1] = 0x0013FDC4 ç 该地址取值后是-4 [2] B1::ib1 = 11 [3] B1::cb1 = 1 [4] D::B2::_vptr-> [0] D::f2() [1] [2] [5] = 0x4539260 ç 该地址取值后是-4 [6] B2::ib2 = 12 [7] B2::cb2 = 2 [8] D::id = 100 [9] D::cd = D [10] = 0x00000000 [11] D::B::_vptr-> [0] D::f() [1] B::Bf() [2] 00000000 [12] B::ib = 0 [13] B::cb = B |
关于虚拟继承的运行结果我就不画图了(前面的作图已经让我产生了很严重的厌倦感,所以就偷个懒了,大家见谅了)
在上面的输出结果中,我用不同的颜色做了一些标明。我们可以看到如下的几点:
1)无论是GCC还是VC++,除了一些细节上的不同,其大体上的对象布局是一样的。也就是说,先是B1(黄色),然后是B2(绿色),接着是D(灰色),而B这个超类(青蓝色)的实例都放在最后的位置。
2)关于虚函数表,尤其是第一个虚表,GCC和VC++有很重大的不一样。但仔细看下来,还是VC++的虚表比较清晰和有逻辑性。
3)VC++和GCC都把B这个超类放到了最后,而VC++有一个NULL分隔符把B和B1和B2的布局分开。GCC则没有。
4)VC++中的内存布局有两个地址我有些不是很明白,在其中我用红色标出了。取其内容是-4。接道理来说,这个指针应该是指向B类实例的内存地址(这个做法就是为了保证重复的父类只有一个实例的技术)。但取值后却不是。这点我目前还并不太清楚,还向大家请教。
5)GCC的内存布局中在B1和B2中则没有指向B的指针。这点可以理解,编译器可以通过计算B1和B2的size而得出B的偏移量。
结束语
C++这门语言是一门比较复杂的语言,对于程序员来说,我们似乎永远摸不清楚这门语言背着我们在干了什么。需要熟悉这门语言,我们就必需要了解C++里面的那些东西,需要我们去了解他后面的内存对象。这样我们才能真正的了解C++,从而能够更好的使用C++这门最难的编程语言。
参考文献
http://www.cnblogs.com/skynet/p/3343726.html
http://blog.csdn.net/haoel/article/details/1948051
http://blog.csdn.net/haoel/article/details/3081328/
http://blog.csdn.net/haoel/article/details/3081385/
最新文章
- ORA-03113: end-of-file on communication channel
- flash 自定义右键功能
- 每天一个java基础知识--static
- 对MySQL DELETE语法的详细解析
- Asp.Net Core- 配置组件详解
- qstring.h赏析
- REST架构概述
- Marriage is Stable
- Mycat 分片规则详解--范围取模分片
- kafka topic 相关操作
- String、StringBuffer、StringBuilder对比
- emacs单词首字母,单词,区域大小写转换
- ASP.NET Core JWT认证授权介绍
- MySQL导出用户权限
- RPG游戏开发基础教程
- CentOS 6.4 i386 版本安装 FastDFS、使用Nginx作为文件访问WEB服务器
- 笔记本上安装centos7
- JS代码执行机制
- Bootstrap-Other:CSS编码规范
- React-Native 样式指南