C++对象模型
何为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类在机器中我们如何构建出各种成员数据和成员函数的呢?
基本C++对象模型
在介绍C++使用的对象模型之前,介绍2种对象模型:简单对象模型(a simple object model)、表格驱动对象模型(a table-driven object model)。
简单对象模型(a simple object model)
所有的成员占用相同的空间(跟成员类型无关),对象只是维护了一个包含成员指针的一个表。表中放的是成员的地址,无论上成员变量还是函数,都是这样处理。对象并没有直接保存成员而是保存了成员的指针。
表格对象模型(a table-driven object model)
这个模型在简单对象的基础上又添加了一个间接层。将成员分成函数和数据,并且用两个表格保存,然后对象只保存了两个指向表格的指针。这个模型可以保证所有的对象具有相同的大小,比如简单对象模型还与成员的个数相关。其中数据成员表中包含实际数据;函数成员表中包含的实际函数的地址(与数据成员相比,多一次寻址)。
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 << "虚函数表地址: " << (int*)(&b1) << endl; cout << "虚函数表 — 第1个函数地址: " << (int*)*(int*)(&b1) << " 即析构函数地址:" << (int*)*((int*)*(int*)(&b1)) << endl; cout << "虚函数表 — 第2个函数地址: " << ((int*)*(int*)(&b1) + 1) << " "; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&b1)) + 1); pFun(); b1.print(); cout << endl; cout << "推测数据成员iBase地址: " << ((int*)(&b1) +1) << " 通过地址取值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 << "虚函数表地址: " << (int*)(&d) << endl; cout << "虚函数表 — 第1个函数地址: " << (int*)*(int*)(&d) << " 即析构函数地址" << endl; cout << "虚函数表 — 第2个函数地址: " << ((int*)*(int*)(&d) + 1) << " "; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&d)) + 1); pFun(); d.print(); cout << endl;
cout << "虚函数表 — 第3个函数地址: " << ((int*)*(int*)(&d) + 2) << " "; pFun = (Fun)*(((int*)*(int*)(&d)) + 2); pFun(); d.derived_print(); cout << endl;
cout << "推测数据成员iBase地址: " << ((int*)(&d) +1) << " 通过地址取得的值:" << *((int*)(&d) +1) << endl; cout << "推测数据成员iDerived地址: " << ((int*)(&d) +2) << " 通过地址取得的值:" << *((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的起始内存地址: " << &d << endl; cout << "虚函数表地址: " << (int*)(&d) << endl; cout << "虚函数表 — 第1个函数地址: " << (int*)*(int*)(&d) << " 即析构函数地址" << endl; cout << "虚函数表 — 第2个函数地址: " << ((int*)*(int*)(&d) + 1) << " "; typedef void(*Fun)(void); Fun pFun = (Fun)*(((int*)*(int*)(&d)) + 1); pFun(); d.print(); cout << endl;
cout << "虚函数表 — 第3个函数地址: " << *((int*)*(int*)(&d) + 2) << "【结束】 "; cout << endl;
cout << "推测数据成员iBase地址: " << ((int*)(&d) +1) << " 通过地址取得的值:" << *((int*)(&d) +1) << endl; cout << "推测数据成员iDerived地址: " << ((int*)(&d) +2) << " 通过地址取得的值:" << *((int*)(&d) +2) << endl; } |
输出结果如下图所示:
特别注意下,前面的模型虚函数表中最后一项没有打印出来,本实例中共2个虚函数,打印虚函数表第3项为0。其实虚函数表以0x0000000结束,类似字符串以’