【三大特性】对象模型
一、概述
引用《深度探索C++对象模型》这本书中的话:
有两个概念可以解释C++对象模型:
- 语言中直接支持面向对象程序设计的部分。
- 对于各种支持的底层实现机制。
直接支持面向对象程序设计,包括了构造函数、析构函数、多态、虚函数等等,这些内容在很多书籍上都有讨论,也是C++最被人熟知的地方(特性)。而对象模型的底层实现机制却是很少有书籍讨论的。对象模型的底层实现机制并未标准化,不同的编译器有一定的自由来设计对象模型的实现细节。在我看来,对象模型研究的是对象在存储上的空间与时间上的更优,并对C++面向对象技术加以支持,如以虚指针、虚表机制支持多态特性。
下面是整体的标题索引:
二、基本C++对象模型
- 简单对象模型(a simple object model)
- 表格对象模型(a table-driven object model)
- 非继承情况下的C++对象模型
三、继承下的C++对象模型
3.1、C++对象模型中加入单继承
- 无重写的单继承
- 有重写的单继承
3.2、C++对象模型中加入多继承
3.3、C++对象模型中加入虚继承
- 简单虚继承(无重复继承情况)
- 菱形继承(含重复继承、多继承情况)
四、相关问题
4.1、如何访问成员?
- 对象大小问题
- 数据成员如何访问(直接取址)
- 函数成员如何访问(间接取址)
二、基本C++对象模型
/********* 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;
};
2.1、简单对象模型
所有的成员占用相同的空间(跟成员类型无关),对象只是维护了一个包含成员指针的一个表。表中放的是成员的地址,无论上成员变量还是函数,都是这样处理。对象并没有直接保存成员而是保存了成员的指针。
2.2、表格对象模型
这个模型在简单对象的基础上又添加了一个间接层。将成员分成函数和数据,并且用两个表格保存,然后对象只保存了两个指向表格的指针。这个模型可以保证所有的对象具有相同的大小,比如简单对象模型还与成员的个数相关。其中数据成员表中包含实际数据;函数成员表中包含的实际函数的地址(与数据成员相比,多一次寻址)。
2.3、C++对象模型
这个模型从结合上面2种模型的特点,并对内存存取和空间进行了优化。在此模型中,non static 数据成员被放置到对象内部,static数据成员, static and nonstatic 函数成员均被放到对象之外。
对于虚函数的支持则分两步完成:
- 每一个class产生一堆指向虚函数的指针,放在表格之中。这个表格称之为虚函数表(virtual table,vtbl)。
- 每一个对象被添加了一个指针,指向相关的虚函数表vtbl。通常这个指针被称为vptr。vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。
另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。
这个模型的优点在于它的空间和存取时间的效率;缺点如下:如果应用程序本身未改变,但当所使用的类的non static数据成员添加删除或修改时,需要重新编译。
2.4、事例代码
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 RTTIstring 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++对象模型
3.1、C++对象模型中加入单继承
3.1.1、无重写的单继承
3.1.2、有重写的单继承
3.2、C++对象模型中加入多继承
单继承及多继承 可见【底层汇编实现】虚表 内存分布
3.3、C++对象模型中加入虚继承
虚继承是为了解决重复继承中多个间接父类的问题的,所以不能使用上面简单的扩充并为每个虚基类提供一个虚函数指针(这样会导致重复继承的基类会有多个虚函数表)形式。
虚继承的派生类的内存结构,和普通继承完全不同。虚继承的子类,有单独的虚函数表,另外也单独保存一份父类的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。派生类的内存中,首先是自己的虚函数表,然后是派生类的数据成员,然后是0x0,之后就是基类的虚函数表,之后是基类的数据成员。
如果派生类没有自己的虚函数,那么派生类就不会有虚函数表,但是派生类数据和基类数据之间,还是需要0x0来间隔。
因此,在虚继承中,派生类和基类的数据,是完全间隔的,先存放派生类自己的虚函数表和数据,中间以0x分界,最后保存基类的虚函数和数据。如果派生类重载了父类的虚函数,那么则将派生类内存中基类虚函数表的相应函数替换。
3.3.1、简单虚继承(无重复继承情况)
3.3.2、菱形继承(含重复继承、多继承情况)
四、相关问题
4.1、如何访问成员?
4.1.1、对象大小问题
- 3个类中的函数都是虚函数
- Derived继承Base
- Derived_Virtual虚继承Base
【结论】:
- 因为Base中包含虚函数表指针,所有size为4;
- Derived继承Base,只是扩充基类的虚函数表,不会新增虚函数表指针,所以size也是4;
- Derived_Virtual虚继承Base,根据前面的模型知道,派生类有自己的虚函数表及指针,并且有分隔符(0x00000000),然后才是虚基类的虚函数表等信息,故大小为4+4+4=12。
4.1.2、数据成员如何访问(直接取址)
跟实际对象模型相关联,根据对象起始地址+偏移量取得。
4.1.3、函数成员如何访问(间接取址)
跟实际对象模型相关联,普通函数(nonstatic、static)根据编译、链接的结果直接获取函数地址;如果是虚函数根据对象模型,取出对于虚函数地址,然后在虚函数表中查找函数地址。