zoukankan      html  css  js  c++  java
  • C++虚函数与多态

    C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。(这里我觉得要补充,重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)

    什么是多态?

    父类指针指向一个子类对象,然后通过父亲的指针调用子类的成员函数,这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

    多态是基于虚函数的,虚函数是基于重写的

    下面是构成多态的条件:(缺一不可!!!!!)

      • 必须存在继承关系;
      • 继承关系中必须有同名的虚函数,并且它们是遮蔽(覆盖)关系。
      • 存在父类的指针,通过该指针调用虚函数(通过父类指针操作子类对象)。

    什么时候声明虚函数?

    首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。

    如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

    虚函数实现多态的原理?

    虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。

    虚函数列表的每个元素都是一个函数指针,指向一个虚函数或者子类重写的函数(子类重写后就会覆盖原来父类的)

    如何找到虚函数列表呢?

    在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

    通俗点解释就是:每个虚函数列表都有一根装着虚函数列表地址的指针vfptr,需要vfptr去记录这个类所使用的虚函数列表,

    子类先将父类的虚函数列表拷贝过来,通过父类指针去调用虚函数,通过vfptr去找到虚函数列表中的函数看子类里面有没有重写虚函数的,如果有,则覆盖该函数

    v_table是在编译时就存在的,只有一份,属于全局的

    vfptr是在创建对象时存在的且是父类中的第一个成员(前四个字节),在构造函数中默认初始化指向自己类的虚函数列表

    关于如何获取虚函数的地址可以见:https://www.cnblogs.com/jiayayao/p/6279483.html

    接下来我们来具体看一下虚函数的应用:

    1、一般继承(无虚函数覆盖)

      下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

      请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示: 

      对于实例:Derive d; 的虚函数表如下:

     

      我们可以看到下面几点:

      1)虚函数按照其声明顺序放于表中。

      2)父类的虚函数在子类的虚函数前面。

    2、一般继承(有虚函数覆盖)

      覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

      为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

      我们从表中可以看到下面几点,

      1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。

      2)没有被覆盖的函数依旧。

      这样,我们就可以看到对于下面这样的程序, 

      Base *b = new Derive(); 

      b->f();

      由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

    3、多重继承(无虚函数覆盖)

      下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。

      对于子类实例中的虚函数表,是下面这个样子:

     

      我们可以看到:

      1) 每个父类都有自己的虚表。

      2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

      这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

    4、多重继承(有虚函数覆盖)

      下面我们再来看看,如果发生虚函数覆盖的情况。

      下图中,我们在子类中覆盖了父类的f()函数。

      下面是对于子类实例中的虚函数表的图:

    我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了

    使用虚函数要注意的问题

    1) 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。

    2、在子类中的同名虚函数是可加关键字virtual也可不加,但是为方便代码阅读,建议是进行添加

    3) 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。

    4) 只有派生类的虚函数遮蔽基类的虚函数了才能构成多态(通过父类指针访问子类函数)

    例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。

    5) 构造函数不能是虚函数。对于父类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。

    6)当子类中的构造函数中存在new对象或者空间时,为避免内存的泄露,需要将父类中的析构函数定义为虚函数。

    这是因为析构函数定义为虚函数后,在main函数中调用delete Ani进行析构的时候,会自动调用子类的析构函数,由于调用子类的析构函数会自动析构父类;而析构父类是无法自动析构子类的。

    7)普通的全局函数、静态成员函数和构造函数都是不能够指定为虚函数的。对于inline修饰的内联函数,当用virtual对其进行修饰时,inline是失效的。

    8)父类即使单纯是一个空的构造函数和空的析构函数(空类),在对父类进行实例化为对象的时候也是占用1个内存空间的,即sizeof(father)=1,。

    该1个单元的内存空间是用以标定该对象的存在的,当该父类中有其他如int成员时,就不需要该标定空间.

         当父类中定义了虚函数时,该父类的对象的内存空间为4(sizoef(father)=4多了一个虚函数列表指针)。而从该父类继承的所有子类都是会存在一个虚函数表指针的。

    虚函数实现多态的优点?

    复用和扩展

    虚函数实现多态的缺点?

    虚函数列表占用空间,效率相对于普通函数慢,安全性不好

    哪些函数需要虚函数?什么时候实现多态?

    如果父类指针指向子类的对象,需要使用子类的东西,这就是需要虚函数的

    什么时候出现父类指针指向子类对象?

    需要把不同的类型统一成同一个种类,实现相同功能,但是实现方式不同

    https://www.cnblogs.com/jiayayao/p/6279483.html

    http://blog.csdn.net/zhanghow/article/details/53588871

  • 相关阅读:
    leetcode_question_67 Add Binary
    几种常用控件的使用方法
    JavaBean讲解 规范
    [置顶] JDK-CountDownLatch-实例、源码和模拟实现
    恋人分手后需要做的不是挽回而是二次吸引
    leetcode_question_70 Climbing Stairs
    偶然碰到的Win7 64位下CHM 的问题解决
    FTP中各文件目录的说明
    深入理解line-height与vertical-align(1)
    行内元素和块级元素
  • 原文地址:https://www.cnblogs.com/curo0119/p/8535660.html
Copyright © 2011-2022 走看看