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

  • 相关阅读:
    java io系列23之 BufferedReader(字符缓冲输入流)
    java io系列22之 FileReader和FileWriter
    java io系列21之 InputStreamReader和OutputStreamWriter
    java io系列20之 PipedReader和PipedWriter
    java io系列19之 CharArrayWriter(字符数组输出流)
    java io系列18之 CharArrayReader(字符数组输入流)
    java io系列17之 System.out.println("hello world")原理
    java io系列16之 PrintStream(打印输出流)详解
    java io系列15之 DataOutputStream(数据输出流)的认知、源码和示例
    java io系列14之 DataInputStream(数据输入流)的认知、源码和示例
  • 原文地址:https://www.cnblogs.com/curo0119/p/8535660.html
Copyright © 2011-2022 走看看