您好,登录后才能下订单哦!
何为C++对象模型?
语言中直接支持面向对象的部分
对于各种支持的底层实现机制
语言中直接支持面向对象程序设计的部分,如构造函数,析构函数,虚函数,继承(单继承,虚继承)、多态等等。
在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++对象模型
这个模型结合表格驱动模型总的特点,并对内存存取和空间进行了优化。在此模型中,nostatic数据成员被放在对象内部,static数据成员,static和nonstatic函数成员均被列到对象之外,对于虚函数的支持则分成两步完成
每一个class产生一堆指向虚函数的指针,放在表格中。这个表格称为虚函数表。
每一个对象被添加一个指针,指向相关函数的vtbl。通常这个指针被称为Vptr。vptr的设定和重置都有每个class的构造函数,析构函数和拷贝赋值运算符自动完成。
另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTI运行时类型识别是由编译器在编译期生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象才会生成。
优点:它的空间和存取时间的效率高。
缺点:如果程序本身未改变,但当使用的类的nonstatic数据成员添加删除或修改时,需要重新编译
C++对象模型加入单继承
不管单继承多继承还是虚继承,如果基于“简单对象模型”,每一个基类都可以被派生类中的slot指出,该slot内包含基类对象的地址,这个机制主要缺点是,因为间接性而导致空间和存取时间上的额外负担,优点则是派生类对象的大小不会因为基类的改变而受影响。
无重写的单继承
//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的类图
注:子类没有重新父类函数的情况
|
有重写的单继承
//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; };
注:子类有函数重写父类函数的情况 1.父类的虚函数在虚函数表的【前面】 2.子类的虚函数在虚函数表的【后面】 3.子类重写父类的虚函数,替换父类对应的虚函数,出现在虚函数表里【前面】 |
C++对象模型中加入多继承
从单继承可以知道,派生类中只是扩充了基类的虚函数表。如果是多继承的话,又是如何扩充的呢?
1)每个基类都有自己的虚表
2)子类的成员函数放到了第一个基类的表中
3)内存布局中,其父类布局一次按声明顺序排列
4)每个基类的虚表中的Print()函数被overwrite成了子类的Print()。这样做就是为了解决不同基类类型的指针指向同一个子类的实例,而能够调用到实际的函数。
上面3个类,Derived_Mutlip_Inherit继承自Base、Base_1两个类,Derived_Mutlip_Inherit的结构如下所示:
C++对象模型中加入虚继承
虚继承是为了解决重复继承中多个间接父类的问题的,所以不能使用上面简单的扩充并为每个虚基类 提供一个虚函数指针(这样做会导致重复继承的基类会有多个虚函数表)形式。虚继承的派生类的内存结构,和普通继承完全不同,虚继承的子类,有单独的虚函数表,另外也单独保存一份父类的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界,派生类的内存中,首先是自己的虚函数表,然后是派生类成员的数据,然后是0x0,之后就是基类的虚函数表,之后是基类数据成员。
如果派生类没有自己的虚函数,那么派生类就不会有虚函数表,但是派生类数据和基类数据之间,还需要用0x0来间隔。因此在虚继承中,派生类和基类的数据,是完全间隔的,先存放派生类自己的虚函数表和数据,中间以0x分界,然后保存基类的虚函数和数据,如果派生类重载了父类的虚函数,那么则派生类内存中的基类虚函数表的相应函数替换。
简单虚继承(无重复继承)
简单虚继承的2个类Base、Derived_Virtual_Inherit1的关系如下所示:
Derived_Virtual_Inherit1的对象模型如下图:
注:虚 基类的信息是独立de
|
菱形继承(含重复继承、多继承情况)
菱形继承关系如下:
Derived_Virtual的对象模型如下图:
注:虚基类的信息是独立的,多继承的布局和之前一样
1.基类的虚函数表指针及数据成员按照继承的排序序列【前面】
2.虚基类的信息与子类的信息使用0x00000000与子类分隔开来,虚基类函数表指针及数据成员【后面】
3.子类重写所有基类(包含虚基类)的虚函数表对应的函数
对象大小问题
三个类中的函数都是虚函数
●Derived继承Base
●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-Virtusl虚继承Base,根据前面的模型知道,派生类有自己的虚函数表及指针,并且有分隔符(0x00000000)然后才有虚基类的虚函数表等信息,故大小为4+4+4=12
数据成员是如何访问的?
跟实际对象模型相关联,根据对象地址+偏移量取得
静态绑定与动态绑定
绑定:把函数体与函数调用相联系称为绑定
程序调用函数时,将使用哪个可执行的代码块?编译器负责回答这个问题,将源代码中的函数调用解析为执行特定函数代码块被称为函数名绑定,在VC语言中,这非常简单,因为每个函数名都对应一个不同的额函数,在C++中,由于函数重载的缘故,这项任务复杂,编译器必须查看函数参数以及函数名才能确定使用哪个函数,然而编译器可以在编译过程中完成这种绑定,这称为静态绑定,有称为早起绑定。
然而虚函数使得这项工作变得困难,使用那个函数不是能在编译阶段确定的,因为编译器不知道用户选择哪种类型。所以编译器必须在程序运行时选择正确的函数代码,这称为动态绑定,又称为晚期绑定。
使用虚函数是有代价的,在内存和执行速度方面是有一定成本的,包括:
●每个对象都将增大,增大量为存储虚函数表指针的大小;
●对于每个类,编译器都创建一个虚函数地址表;
●对于每个函数调用,都需要执行一项额外的操作,即到虚函数表中查找地址。
虽然非虚函数比虚函数效率稍高,单不具备动态联编能力。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。