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

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

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

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

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

  • 多态和非多态的本质区别在于:早绑定和晚绑定.函数调用的地址是编译期间就能确定的,那就是非多态;需要在运行的时候才确定,那就是多态
  • 面向对象三大特性
    1. 封装:模块化代码,实现代码重用
    2. 继承扩展已经实现的代码,实现代码重用
    3. 多态: 实现接口重用,同一个接口可以自适应到各自对象的实现方法上面去.
  • 实现多态的方法
    1. 声明基类指针,指向一个子类对象
    2. 调用虚函数
    3. 如果没有多态性,则调用的函数将一直是基类的相应函数,即,函数调用的地址是固定的,不能实现一个接口,多种方法.
#include  
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的方法,如果是虚函数,并被子类通过共有继承,那么这个函数将出现在子类的虚函数表中,并可以通过地址访问的方式被调用,这是很危险的.

你可能感兴趣的:(多态,虚函数,纯虚函数,虚函数表)