美文网首页C++Fuck iOS EveryDay
多态,虚函数,纯虚函数,虚函数表

多态,虚函数,纯虚函数,虚函数表

作者: 曹小恒 | 来源:发表于2018-04-03 18:39 被阅读39次

    多态性:一个接口,多种方法.程序在运行时才确定调用的函数,是 oop 的核心概念.

    • 多态性通过虚函数来实现,子类可以重新定义父类(重写:override).

    重写有两种,一种是重写虚函数(体现多态),另一种就是重写成员函数(并没有体现)

    和重写相对的另一个概念是重载(overloading),指的是多个重名的函数他们的参数列表不同(个数,类型),编译器通过函数的调用参数列表来决定调用的

    • 多态和非多态的本质区别在于:早绑定和晚绑定.函数调用的地址是编译期间就能确定的,那就是非多态;需要在运行的时候才确定,那就是多态
    • 面向对象三大特性
      1. 封装:模块化代码,实现代码重用
      2. 继承扩展已经实现的代码,实现代码重用
      3. 多态: 实现接口重用,同一个接口可以自适应到各自对象的实现方法上面去.
    • 实现多态的方法
      1. 声明基类指针,指向一个子类对象
      2. 调用虚函数
      3. 如果没有多态性,则调用的函数将一直是基类的相应函数,即,函数调用的地址是固定的,不能实现一个接口,多种方法.
    #include<iostream>  
    using namespace std;  
      
    class A  
    {  
    public:  
        void foo()  
        {  
            printf("1\n");  
        }  
        virtual void fun()  
        {  
            printf("2\n");  
        }  
    };  
    class B : public A  
    {  
    public:  
        void foo()  
        {  
            printf("3\n");  
        }  
        void fun()  
        {  
            printf("4\n");  
        }  
    };  
    int main(void)  
    {  
        A a;  
        B b;  
        A *p = &a;  
        p->foo();  
        p->fun();  
        p = &b;  
        p->foo();  
        p->fun();  
        return 0;  
    }  
    

    输出为

    1 2 1 4
    

    对于输出1 2是没有问题的,在第三和第四个输出的时候,因为基类指针指向了子类,而foo()函数没有被虚拟化,所以,这是一个早绑定,只能调用基类的同名函数,fun()是一个基类中的虚函数,所以可以被晚绑定,调用子类中的函数,从而实现了一个借口,多个函数的多态性.

    • 小结
      1. 如果有 virtual 才有多态(覆盖基类函数)
      2. 没有多态,按照原类型调用(隐藏)

    纯虚函数:在基类中定义的虚函数,没有定义,派生类需要定义自己的实现方法.

    virtual void function() = 0;
    

    派生类中必须进行重写以实现多态性. 含有纯虚函数的类成为抽象类,不能生成对象.

    • 编译多态性: 通过重载实现
    • 运行多态性: 通过虚函数实现(覆盖)

    虚函数表(vtable): 虚函数是通过虚函数表来实现的,这个表主要是一个类虚函数的地址表,这张表解决了继承和多态的问题,保证了真实反映和使用实际的函数.这个表在一个对象实例的最前面,我们可以通过变量虚函数表的函数指针,调用相应的函数.

    class Base {
         public:
                virtual void f() { cout << "Base::f" << endl; }
    
                virtual void g() { cout << "Base::g" << endl; }
    
                virtual void h() { cout << "Base::h" << endl; }
    };
    Base b;
    

    这个对象实例b的结构如下:

    图片.png
    1. 一对一继承(子类没有覆盖重写虚函数,这样在实际中没有意义,仅为对比)
      如果子类也有自己的虚函数,并继承父类的虚函数表,则其虚函数表的结构为:


      图片.png

      1)虚函数按照声明顺序放在表中
      2)父类的虚函数放在子类的前面

    2. 一对一继承(有虚函数被覆盖)
      父子两个对象的表分别如下:


      图片.png

    则最后子类的虚函数表将为:


    图片.png

    1)覆盖的虚函数,父类位置被子类顶替
    2)其余顺序不变
    因此,如果有

    Base *b = new Derive();
    b->f();
    

    该父类对象指向的地址是子类的地址,对象b将调用覆盖后的 Derive::f().
    这就是多态实现的原理.

    1. 多重继承(无覆盖)
      如果子类对父类的虚函数没有覆盖:


      图片.png

      那么子类实例中的虚函数表是这样子的:


      图片.png
      1)每个父类有自己的虚表
      2)子类的虚函数在第一个父类之后
      3)父类顺序是按照声明顺序来的

      当每次设计到这些虚函数的时候,需要对应到相应的虚函数表(根据基类的定义),比如:

    Base2 *ptr = new d();
    ptr->f()  //调用第二个表的第一个函数(Base2 的 f())
    
    1. 多重继承(有虚函数覆盖)


      图片.png

      所有相应的同名虚函数都要被覆盖:


      图片.png

    安全性

    • 子类中没有重载的虚函数虽然出现在第一个虚函数表中,但是相应的父类指针并不能访问这个函数.程序将报错.但是仍然可以通过地址访问的方式调用.
    • 对于父类中private的方法,如果是虚函数,并被子类通过共有继承,那么这个函数将出现在子类的虚函数表中,并可以通过地址访问的方式被调用,这是很危险的.

    相关文章

      网友评论

        本文标题:多态,虚函数,纯虚函数,虚函数表

        本文链接:https://www.haomeiwen.com/subject/jcfchftx.html