C++多态之虚函数

多态性指的是相同对象在收到不同消息或者不同对象收到相同消息产生不同的实现动作。C++中多态可以分为两种:静态多态和动态多态。其中静态多态主要的实现方式就是通过重载,重载又可以分为函数重载和运算符重载,内容比较多,这篇文章里不会做过分详细的描述。而动态重载的主要实现方式就是虚函数,因为虚函数体现了继承和多态两大特性,可以说虚函数是C++中最重要的概念之一,正因有了虚函数我们才可以做到通过统一的访问方式动态调用不同类中相同的函数。

虽然本文不会去描述重载的细节,但是为了对C++三大特性之一的多态性有比较深刻的认识,还是很有必要好好比较一番重载和虚函数(重写)的异同的,接下来我将逐条罗列二者的区别。

虚函数(重写):

  1. 范围:有继承关系的多个类中的同名函数。
  2. 参数:函数的函数名、参数均相同。
  3. 修饰符:被重载的函数必须有virtual修饰。

重载:

  1. 范围:位于同一个类中的同名函数。
  2. 参数:函数参数列表必须不同。
  3. 修饰符:virtual修饰符可有可无。

比较完二者的区别,接下来把重心回到虚函数中来,我们必须需要知道什么是虚函数,虚函数是怎么使用的,虚函数的实现原理以及虚函数和纯虚函数的比较。

虚函数的主要工作是:无论是基类还是派生类,其中同名但又体现不同特性的成员函数可以通过统一的基类指针访问。我们举个简单的例子,定义账单类(作为基类),类中成员函数print可以打印出每个月的账单,现有计算机学院账单类继承了账单类,同样定义了成员函数print,且这个函不仅仅只有函数名,连返回值和参数列表均和账单类中的print函数完全相同,唯一的区别就是要额外打印出计算机学院的名字。若定义指针指向基类对象(账单类)的指针p,之后用这个指针取计算机学院对象的地址,之后用p调用print函数,这里本意是取得派生类对象的地址,之后调用可以打印出计算机学院名字的那个print函数。但事与愿违,这里只能调用基类的print函数,确实是使用了基类的函数打印了计算机类账单的变量,但很显然不是我们所想的样子。

最简单的解决方法就是重新定义一个指针,指针是指向计算机学院账单对象的,这样当我们使用这个指针访问print函数时,就能精确的调用该派生类中的print函数了(即可以打印出计算机学院名字)。但是这种方式在程序很大的时候会造成管理十分混乱,程序员需要同时记得很多指针以及指针类型,这样给程序开发维护带来了很大的困难。

另一种解决方案更加简单粗暴,可以直接将各个函数中同名的函数换成不同的名字,每次调用时候都精确给出调用函数的名字,从而保证准确调用函数。但这种方式没能发挥面向对象编程统一接口的思想,同样给开发带来了困难。

我们渴望有一种方式,只需要定义一个指向基类的指针,之后通过这个指针调用任何派生类中的函数时,都能准确通过定义的对象的类型自动去找到相应的同名函数。这就是我们所要讲的虚函数。

继续使用之前的例子,我们可以在基类(账单类)中将print成员函数前加上virtual,这样print就成了虚函数。之后我们在通过派生出计算机账单类的时候,该子类中的print函数也会是虚函数(无论是否显式的加上virtual关键字)。

之后当我们使用指向基类的指针重新取得派生类的对象地址时,使用这个地址调用print函数就可以打印出计算机学院的名字了,即可以立即为指针虽然是指向基类类型的,但因为重新取了新对象的地址,当执虚函数时就会准确调用取得地址的那个对象中的虚函数。

接下来说一下虚函数的原理。虚函数的实现主要是因为一个指针和一张指向函数入口的表。每一个包含虚函数的类都包含一个指针变量(系统自动插入类的低地址处),我们叫它vptr,所以包含虚函数的类是要比实际可见的大小多出一个指针变量大小的。每个类的这个指针指向一张虚函数表,虚函数表的每一个记录是一个虚函数的指针。在调用类的构造函数时,指向此基础类的指针此时已经变成了指向具体类的this指针,这样靠此this指针即可得到正确的vtable,如此才能真正与函数进行连接,这就是动态联编,也是虚函数实现多态的基本原理。

最后,比较一下虚函数和纯虚函数的区别。虚函数之前已经说得比较明白了,我们也看到之前的例子中,所有的基类都可以定义对象,并且基类中的虚函数具有完整的定义,只不过它和其派生类中的其他同名函数在函数体部分略有不同。但是在现实生活中,并不是所有的基类创建出的对象都是有意义的,比如动物作为基类,其可以派生出的老虎大象类,然后用老虎大象类去定义对象是有意义的,但是动物作为对象却没有什么意义。这种情况我们可以使用纯虚函数,纯虚函数的实现方法是在函数原型后面加“=0”。

纯虚函数和之前虚函数最大的区别是:纯虚函数在基类中只有函数声明却没有函数定义,它起到的仅仅是一个接口的作用,其具体实现部分在其派生类中实现。正因纯虚函数没有完整的定义,所以包含纯虚函数的基类是不能实例化的,它只能派生,这种不能实例化的类也被称为抽象类。如果其派生出的派生类中依旧没有将纯虚函数改写的话,那么它的派生类依旧是抽象类。

Advertisements