C++(对象模型):09---Function之(多态、虚函数、虚函数表剖析)

  • 我们在前面已经介绍了虚函数的一般实现模型:
    • 每一个类有一个虚函数表(virtual table),内含该类的虚函数地址,然后每个对象有一个vptr(虚函数表指针),指向于虚函数表

一、多态的实现

  • 为了支持虚函数机制,必须能够对于多态对象有某种形式的“执行期类型判断法”
  • 也就是说,对于下面的操作将需要ptr在执行期的某些相关信息(其中,z是虚函数),如此一来就能够找到并调用z()的适当实体:
ptr->z();

实现多态的方式一(假设把信息附加在指针/引用上)

  • 最直接了当但成本最高的方式是把必要的信息加在ptr身上。在这样的策略之下,一个指针(或引用)含有两项信息:
    • ①它所参考的对象的地址(也就是当前它所含有的东西)
    • ②对象类型的某种编码,或是某个结构(内含某些信息,用于正确决议出z()函数实例)的地址
  • 这种方法会带来两个问题:
    • ①明显增加了空间负担,即使程序并不实用多态
    • ②打断了与C程序间的链接兼容性
  • 总结,此种实现方式不可取,见下面的实现方式

实现多态的方式二(把信息附加在对象上)

  • 如果不能把额外信息和指针/引用放在一起,可以考虑把信息放在对象本身上
  • 但是并不是所有类型都需要这些附加信息的。例如没有虚函数的结构体或类就不要这些附加信息,当含有虚函数可以支持“执行期多态”的时候就需要这些额外的信息
//此结构体不需要附加的信息,因为没有虚函数
struct date{
    int m,d,y;
};

//此类不需要附加的信息,因为没有虚函数
class date{
public:
    int m,d,y;
};

//此类需要附加的信息,因为有虚函数
class date{
public:
    virtual ~geom();
};

多态

  • 多态的意思为“以一个public基类的指针/引用,寻址一个派生类对象”
  • 例如下面用public基类的指针寻址派生类对象。但是这种多态形式是消极的,因为可以在编译时期完成(当然,virtual base class的情况除外)
class Point;
class Point2d:public Point; //必须为公有继承
class Point3d:public Point; //必须为公有继承


Point *ptr;
ptr = new Point2d; //基类指针寻址派生类
ptr = new Point3d; //基类指针寻址派生类
  • 当被指出的对象真正被使用时,多态也就变成积极的了,此时的多态才真正的被显示出来。例如下面通过ptr指针调用虚函数
ptr->z();
  • 运行时类型识别(runtime type identification,RTTI)性质于1993年被引入C++语言之前,C++对“积极多态”的唯一支持,就是对于虚函数调用的决议操作。有了RTTI,就能够在执行期查询一个多态的指针或多态的引用了(RTTI将会在后面的文章介绍)

在多态中,我们需要哪些信息?

  • 上面介绍了,当含有虚函数时,对象就需要含有一些附加信息,那么需要什么信息哪?还是以“ptr->z()”这个调用为例
    • ①指针/引用所指对象的真实类型,这可使我们选择正确的z()实体
    • ②虚函数z()的实体位置,以便我们能调用它
  • 在真正实现上,每一个多态的类对象身上增加两个成员:
    • ①一个字符串或数字,表示class的类型
    • ②一个指针,指向于虚函数表,虚函数表带有程序的虚函数的执行期地址

虚函数表的构建(编译期)

  • 虚函数表中的虚函数地址是如何被建构起来?
  • 在C++中,虚函数(可经由其类对象调用)可以在编译时期获知,此外,这一组地址是固定不变的,执行期不可能新增或替换。由于程序执行时,虚函数表的大小和内容都不会变,所以其建构和存取皆可以由编译器完全控制,不需要执行期的任何介入

虚函数的调用(执行期)

  • 然而,虽然编译期已经准备好了这些函数地址,但是另一个问题是如何找到这些函数地址,以下两个步骤可以完成这项任务:
    • ①通过类中的虚函数表指针,找到虚函数表
    • ②虚函数表有许多的槽(索引),虚函数被指派在某一个索引中,根据索引找到虚函数
  • 执行期要做的,就是在特定的虚函数表中激活相应虚函数的调用

二、单一继承下的虚函数

  • 一个类只会有一个虚函数表,表中含有其对应的所有虚函数地址,这些虚函数分为:
    • ①该虚函数为其改写(override)了基类的虚函数
    • ②继承基类的虚函数
    • ③纯虚函数。纯虚函数可以扮演“纯虚函数的空间保卫战角色”,也可以当做执行期异常处理函数(有时会用到)

演示案例

class Point
{
public:
	virtual ~Point();
	virtual Point& mult(float) = 0;

	float x()const { return _x; }
	virtual float y()const { return 0; }
	virtual float z()const { return 0; }
protected:
	Point(float x = 0.0);
	float _x;
};

class Point2d :public Point
{
public:
	Point2d(float x = 0.0, float y = 0.0) :Point(x), _y(y) {}
	~Point2d();

	Point2d& mult(float);//改写(override)基类的纯虚函数
	float y()const { return _y; }//改写(override)基类的虚函数
protected:
	float _y;
};

class Point3d :public Point2d
{
public:
	Point3d(float x = 0.0, float y = 0.0,float z=0.0) :Point2d(x,y), _z(z) {}
	~Point3d();

	Point3d& mult(float);//改写(override)基类的虚函数
	float z()const { return _z; }
protected:
	float _z;
};
  • Point类
    • slot0为指向该虚函数表的类类型(Point2d)
    • 虚析构函数被赋值在虚函数表的slot1中
    • 虚函数mult()被赋值在slot2中
    • 虚函数y()被赋值在slot3中
    • 虚函数z()被赋值在slot4中
  • Point2d类:
    • slot0为指向该虚函数表的类类型(Point2d)
    • 虚析构函数被赋值在虚函数表的slot1中
    • 继承并改写基类的纯虚函数mult()被赋值在slot2中
    • 继承并改写基类的虚函数y()被赋值在slot3中
    • 继承并没有改动的基类的虚函数z()被赋值在slot4中
  • Point3d类:
    • slot0为指向该虚函数表的类类型(Point3d)
    • 虚析构函数被赋值在虚函数表的slot1中
    • 继承并改写基类的纯虚函数mult()被赋值在slot2中
    • 继承并没有改动的基类的虚函数y()被赋值在slot3中
    • 继承并改写基类的虚函数z()被赋值在slot4中

  • 如果我们有如下的代码,在编译时期,上图中的虚函数表以及对应的函数都会构建成功,但是详细信息我们是不知道的:
    • 我们并不知道ptr所指对象的真正类型
    • 我们不知道哪一个z()函数实体会被调用
ptr->z();
  • 所有的内容都会等到执行期才会生成,当我们程序执行到上面的代码时,就会被翻译为如下的形式,其中:
    • 会识别出ptr所指的对象的类型,vptr为该对象类型所指的虚函数表指针
    • 然后通过vptr调用对应的虚函数
( *ptr->vptr )( ptr );
  • 在单一继承下,虚函数机制的行为十分良好,不但有效率而且很容器塑造出原型来,但是在多重继承和虚拟继承之中,对虚函数支持就不是那么美好了

三、多重继承下的虚函数

  • 多重继承下的虚函数,其复杂度围绕在派生类以及“必须在执行期调整this指针”这些概念上
  • 假设以下面的代码进行讲解

class Base1
{
public:
	Base1();
	virtual ~Base1();
	virtual void speakClearly();
	virtual Base1 *clone()const;
protected:
	float data_Base1;
};

class Base2
{
public:
	Base2();
	virtual ~Base2();
	virtual void mumble();
	virtual Base2 *clone()const;
protected:
	float data_Base2;
};

class Derived :public Base1, public Base2
{
public:
	Derived();
	virtual ~Derived();
	virtual Derived *clone()const;
protected:
	float data_Derived;
};

基类指针指向于派生类

  • 我们首先在堆上申请一个Base2对象,使其指向派生类
Base2 *pbase2 = new Derived;
  • 编译器必须进行如下的更改,调整Derived对象的地址,然后使其指向Base2
//下面为伪代码
Derived *temp = new Derived;
Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;//根据C++的对象模型,调整位置
/*
  首先调整pbase2指针,指向于完整Derived对象的起点,
  然后使用派生类Derived的虚析构函数
*/
delete pbase2;

动态绑定

  • 通过上面我们知道,基类指针指向于派生类时,通过指针调用成员/方法需要不断的调整基类指针,我们把这个操作称为“this指针调整”
  • 因此上面演示案例中的offset加法不能在程序编译期直接决定,因为并不知道基类指针到底指向于哪个对象,只有在执行期的时候才能确定,这个就是动态绑定

多重继承下的虚函数的实现

  • 在多重继承下,派生类中会生成两种类型的虚函数表:
    • 主要虚函数表:此虚函数表内包含该类及所有基类的所有虚函数,针对第一个继承的类设计
    • 次要虚函数表:除了第一个继承的类之外,后面继承的类,针对每个类创建一个虚函数表,次要虚函数表只包含派生类及针对于特定的这个基类中的所有虚函数
  • 所以,如果一个类多重继承于n个类,那么共有n个虚函数表,其中为1个主要虚函数表,n-1个次要虚函数表
  • 所以,Derived的主要虚函数表放在其继承的Base1 vptr所指之处,因为Base1是被Derived第一个继承的类:
    • 虚析构函数放入到slot1中
    • 继承于Base1并且未修改的speakClearly()虚函数放入到slot2中
    • 重写了基类Base1的clone()虚函数放入到slot3中
    • 继承于Base2并且未修改的mumble()虚函数放入到slot4中
  • 其中Base2是其第二个继承的基类,所以其是次要虚函数表,其中:
    • 虚析构函数放在slot1中
    • 继承于Base2并且未修改的mumble()虚函数放入到slot2中
    • 重写了基类Base2的clone()虚函数放入到slot3中

四、虚拟继承下的虚函数

  • 考虑下面的虚拟继承体系,其中Point2d派生出Point3d

class Point2d
{
public:
	Point2d(float = 0.0, float = 0.0);
	virtual ~Point2d();

	virtual void mumble();
	virtual float z();
protected:
	float _x, _y;
};

class Point3d :virtual public Point2d
{
public:
	Point3d(float = 0.0, float = 0.0, float = 0.0);
	~Point3d();

	float z();
protected:
	float _z;
};

虚拟继承下的虚函数实现

  •  现在对于虚拟继承下的虚函数实现比较复杂,仍然没有一个标准,因此就不再详细介绍

发布了1363 篇原创文章 · 获赞 910 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104084764