4.2 虚拟成员函数

前端之家收集整理的这篇文章主要介绍了4.2 虚拟成员函数前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

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()实例的位置

  3. @H_403_75@

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

    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 值以及函数地址。则此时虚函数调用变化如下:

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

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

      1. thunk方法

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

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

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

      2. @H_403_75@

        • 对于第二个问题:被继承下来的 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 指针

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

猜你在找的设计模式相关文章