4.2 虚拟成员函数

来源:互联网 发布:bad air sponge 知乎 编辑:程序博客网 时间:2024/06/05 15:25

Q1:C++多态形式:静态多态性、动态多态性

• 多态:以一个 “public base class” 的指针寻址出一个 “derived class object”(深入探索C++对象模型定义)

• 静态多态性通常称为编译时多态,到底模板是不是多态???我个人认为不是

• 动态多态性通常称为运行时多态,通过虚函数来实现

• 动态多态性的两个条件:

○ 在基类中必须使用虚函数或纯虚函数○ 调用函数时使用基类的指针或引用




Q2:消极多态与积极多态(深入探索C++对象模型中定义)

• 消极多态:指针的多态机能主要扮演一个输送机制的角色,经过这个指针,我们可以在程序的任何地方采用一组派生类类型。这样的传输机制的多态形式称为消极多态

Eg:

    point2d p2d;    point * ptr = &p2d;   //此时,ptr带来的多态性体现为传输机制,通过基类指针传输派生类对象

• 积极多态:当指针所指对象被真正使用时,多态也就变为积极的了
Eg:

ptr->f();        //ptr 所指对象被真正使用




Q3:对虚函数调用的深入探究

• 对如下调用,若z()是一个虚函数,那需要那些信息才能在执行期调用正确的z()实例:

ptr->z()
  1. ptr所指对象的真实类型。(用以选择正确的z()实例)

  2. z()实例的位置

因此,需要在每一个多态的类对象上增加两个成员:

1. 一个字符串或数字,表示 class 的类型;(vptr所指表格的第一项即为类型说明,用以支持RTTI)2. 一个指针,指向某个表格,表格中持有程序的虚函数的执行期地址(vptr)

• 虚函数表的构建与存取皆有编译器掌控,不需要任何执行期的介入




Q4:单一继承下的虚函数

• 在单一继承情况下,一个类对象中,可能包含多个vptr,但只会有一个 virtual table。每一个 table 中内含其对应类对象中所有 active virtual functions 函数实例的地址,这些函数包括:

• 这一个类所定义的函数实例,包括改写的继承自 base class 的函数实例• 继承自 base class 的函数实例,指该类并不会改写的继承实例• 一个 pure_virtual_called()的实例。既可以扮演纯虚函数的空间保卫 角色,又可以当作执行期异常处理函数


• 每一个虚函数都被指派一个固定的索引,这个索引在整个继承体系中保持与特定的虚函数的关系。

Eg:

        class Point        {        public:            virtual ~Point();            virtual Point& mult(float) = 0;            virtual float y()const{ return 0; }            virtual float z()const { return 0; }        protected:            float _x;        };        class Point2d : public Point        {        public:            virtual ~Point2d();            Point2d& mult(float);            float y() const { return _y; }        protected:            float _y;        };

其虚函数表中的取值情况如下所示:

Point 的虚函数表如下:

这里写图片描述

Point2d 的虚函数表如下:

这里写图片描述




Q5:多重继承下的虚函数

• 多重继承中支持虚函数的两个难点在于:

1. 对第二个以及后继的 base classes 的虚函数的处理2. 必须在执行期调整 this 指针

*备注:对于虚函数改写(覆盖)而言,允许一个虚函数的返回值类型有所变化,可能是 base type,也可能是 publicly derived type(仅限指向类类型的指针与引用)。但函数名称,参数列必须与被改写的函数完全相同


附加知识点:定义一个基类指针指向一个派生类对象,通过该指针调用改写函数时,有以下两种情况:

1. 若改写函数不是虚函数,则调用时调用的为基类的函数实例2. 若改写的函数是虚函数,则调用时调用的为派生类的函数实例

*备注:非虚函数并不在类实例中,不影响类实例的大小。而虚函数则影响类实例的大小,因为会造成附加的 vptr 指针出现在类实例中

• 对多重继承的讨论以如下例子来进行:

    class Base1    {    public:        virtual ~Base1();        virtual void speakClearly();        virtual Base1 * clone()const;    };    class Base2    {    public:        virtual ~Base2();        virtual void mumble();        virtual Base2 * clone()const;        void test();    };    class Derived : public Base1, public Base2    {    public:        virtual ~Derived();        virtual Derived * clone()const;    };

此问题中派生类 Derived 对虚函数的支持的困难度体现 Base2 子对象上。即有以下三个问题需要解决:

1. 虚析构函数2. 被继承下来的 Base2::mumble();3. 一组clone()函数的实例

• 对于第一个问题:虚析构函数的分析:

• Eg1:

Base2 * pbase2 = new Derived;

该操作需要对新的 Derived 对象的地址进行调整以指向其 Base2 子对象,即编译时产生如下代码:

Derived * temp = new Derived;Base2 * pbase2 = temp ?  temp + sizeof( Base1 ) : 0;  //事实上此处的加法操作并不能够在编译期直接设定,因为pbase2所指向的真正对象类型在编译期未知

即将 pbase2 的值调整为该 Derived 对象的 Base2 子对象的位置,否则,任何非多态的运用都会失败。如:

pbase2->test(this); //因为 this 指针不指向 Base2 子对象

• Eg2:

delete pbase2;

对于这种情况,指针需要被再一次调整,使其指向 Derived 对象的起始处,调用正确的虚析构函数实例,然后施行 delete 运算符

• 对于调整 this 指针时不知道偏移的两种处理方法:

  1. 将 virtual table 扩大的方法

    此时,使得每一个 虚函数的表格中放置的不再是一个指针,而是一个集合,其中内含可能的 offset 值以及函数地址。则此时虚函数的调用变化如下:

(*pbase2->vptr[1])(pbase2); (*pbase2->vptr[1].faddr)(pbase2 + pbase2->vptr[1].offset);

这种情况下,连坐处罚了所有的虚函数调用操作,不管其是否需要 offset 调整

  1. thunk方法

    所谓的 thunk 方法是一小段代码,用来以适当的 offset 调整 this 指针, 并跳转到虚函数处。此时,虚函数表中存放的仍然是指针

    • 若不需要调整this指针的虚函数,此时 slot 中存放的就是虚函数的地址

    • 若需要调整 this 指针的虚函数,此时 slot 中存放的是一个相关的 thunk 的地址

• 对于第二个问题:被继承下来的 Base2::mumble():

• Eg1:

Derived * pder = new Derived;pder->mumble();

对于这种情况,Derived 的指针必须再次调整以指向第二个基类子对象,用以调用该子对象的 mumble() 函数

• 对于第三个问题:clone()函数的实例

• Eg:

Base2 * pb1 = new Derived; Base2 * pb2 = pb1->clone();

在这个过程中,pb1->clone()执行时,pb1会被调整到指向 Derived 对象的起始地址,调用 Derived::clone(),传回一个指向新的 Derived 对象的指针,该对象地址再次进行调整后指向 Base2 子对象,并传送给 pb2

• 如果虚函数够小(平均大小是8行),则将使用 sun 编译器提供的 “split functions” 的技术: 以相同算法产生出两个函数,其中第二个在返回之前,为指针加上必要的 offset

*此时,通过 Derived 指针或 Base1 指针调用函数都不需要调整返回值,而通过 Base2 指针所调用的,则是另一个调整返回值的函数

• 在多重继承中,一个派生类内含 n-1 个额外的虚函数表,其中 n 表示其上一层的基类的个数。(因此,单一继承的派生类只有一个虚函数表)




Q6:虚拟继承下的虚函数

• 虚基类不同于普通基类,虚基类位于实例对象的底部(即实例对象地址的最高处),因此,在此情况下,虚基类与派生类之间的转换必须要调整 this 指针

Eg:

class A{};class B : public virtual A{};

此时虽然类 B 仅有一个基类 A,但由于基类 A 是虚基类,在这种情况下,虚基类 A 位于类 B 的底部,因此,在类 A 与类 B 之间的转换需要调整 this 指针

• 更复杂的情况不予讨论。建议:不要在一个虚基类中声明非静态数据成员

0 0