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

 

posted @ 2018-03-09 19:51  Curo  阅读(325)  评论(0编辑  收藏  举报