我个人认为虚函数的作用有两个:
增加安全性;提醒子类去做该做的事情。
提高效率;不是指程序执行效率,而是编码效率。
首先我这里要纠正一下:
一个函数被定义为虚函数,不代表这个函数未被实现;
定义函数为虚函数,表示允许基类使用指针去调用子类实现的这个函数;
当你将函数定义为纯虚函数时,才表示函数未被实现。
纯虚函数相当于一个接口,主要作用是为了规范,程序员必须实现这个继承类的函数。
接下来我们看一个虚函数的经典应用,搞清楚这个例子,你对虚函数会有一个新的认识。
虚函数的虚就虚在“运行时”编译,而C++中的另一个模版是在“编译时”编译,这两者的概念是不同的。
即父类调用子类的函数不是在编译时确定,而是在程序运行时确定。
虚函数要达到多态的效果,只能借助指针或引用。
虚函数的底层实现
虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数。我们先定义两个类
实现原理:虚函数表+虚表指针
关键字:虚函数底层实现机制;虚函数表;虚表指针
编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。
举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:
如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。
如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。
下面的图片体现了上述的底层实现机制:
调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。
使用虚函数后的变化
对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
每个类编译器都创建一个虚函数地址表。
对每个函数调用都需要增加在表中查找地址的操作。
纯虚函数
纯虚函数是声明在基类中,没有定义,继承的子类都必须实现该方法。将一个函数声明为纯虚函数,只需在该函数后加一个“=0”。
virtual int Test()=0;
为何要引入纯虚函数?
其一,是为了实现多态机制;
其二,在很多场景下,基类生成的对象都不符合要求,只能通过子类去实现。基类通过指针调用子类函数,类似于泛型,使用不变的代码实现可变的算法。
为了解决上述两个问题,C++引入了纯虚函数,将函数定义为纯虚函数,编译器要求派生类必须重写纯虚函数,以到达实现多态的目的。
纯虚函数所在的类我们称为抽象类,不能生成对象。不能创建抽象类的实例,只能创建子类的实例。
纯虚函数的目的是为了让子类继承函数的接口。
纯虚函数的意义是让所有子类都能够执行纯虚函数的动作,但是基类无法为纯虚函数提供一个缺省的实现。通俗来讲,声明纯虚函数是为了告诉子类,你必须要实现纯虚函数,至于怎么实现纯虚函数,这是你的事情。
抽象类
上面提到纯虚函数所在的类为抽象类,抽象类是一种特殊的类,多用于抽象和设计,通常处于继承结构的上层。例如,在设计模式中,你应该经常会看到抽象类。
作用
将相关的操作作为接口放在基类中,为子类提供一个公共的模版,子类将具体实现基类中的接口操作。
注意
抽象类不能用作于定义对象;
抽象类只能用作于基类,不能作用于子类,纯虚函数的实现由子类完成。如果子类中只是继承了纯虚函数,未对纯虚函数进行定义,该类仍然为抽象类。如果子类实现了基类中的纯虚函数,该类是可以建立具体对象的类,就不在是抽象类。
总结
(1) 基类方法中声明了方法为虚后,该方法在基类派生类中是虚的。
(2) 若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型。
(3)如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚。
构造函数不能为虚函数。
基类的析构函数应该为虚函数。
友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。
如果派生类没有重定义函数,则会使用基类版本。
重新定义继承的方法若和基类的方法不同(协变除外),会将基类方法隐藏;如果基类声明方法被重载,则派生类也需要对重载的方法重新定义,否则调用的还是基类的方法。