2、虚函数


定义

在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体},简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。

作用

实现了多态的机制,关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

虚函数表

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的,简称V_Table,它就像一个地图一样,指明了实际所应该调用的函数。虚表中有当前类的各个虚函数的入口地址,每个对象都有一个指向当前类的虚表的指针(虚指针vptr)。
动态绑定的实现

  • 构造函数中为对象的虚指针赋值
  • 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
  • 通过该入口地址调用虚函数
    图片说明

特点

  • 用virtual关键字说明的函数
  • 虚函数是实现运行时多态性基础
  • C++中的虚函数是动态绑定的函数
  • 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。
  • 一般成员函数可以是虚函数
  • 构造函数不能是虚函数
  • 析构函数可以是虚函数

纯虚函数

定义:纯虚函数(也叫抽象函数)是一种特殊的虚函数,它可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体给出定义。凡是具有纯虚函数的类叫做抽象类,这种类不能声明对象,知识作为基类为派生类服务,除非在派生类中完全实现基类中所有的纯虚函数,否则,派生类也成了抽象类,不能实例化对象。
格式:

class <类名>
{
virtual <类型><函数名>(<参数表>)=0;
…
};

虚函数与纯虚函数的联系与区别

  • 纯虚函数是一种特殊的虚函数
  • 虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。
  • 虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class)只有声明而没有定义。
  • 虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。
  • 虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是提供一个统一的接口。
  • 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定(run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样。
  • 纯虚函数只有定义,没有实现;而虚函数既有定义,也有实现的代码。
  • 多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
    a.编译时多态性:通过重载函数实现
    b 运行时多态性:通过虚函数实现。
  • 如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类(ABC)是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。

问答

(1)为什么C++中默认析构函数不设置为虚函数?
答:因为虚函数需要额外的虚函数表和虚函数指针,占用额外的内存,对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存,故其默认析构函数不设置为虚函数,只有当需要作为父类时,才设为虚函数。
(2)析构函数是否定义为虚函数的区别?
答:①析构函数是虚函数时:基类指针可指向派生类的对象(多态性),如果删除该指针(delete []p),就会调用该指针指向的派生类析构函数,派生类析构函数又会自动调用基类的析构函数,这样整个派生类会被完全释放。
②析构函数不定义为虚函数时:编译器实施静态绑定,在删除基类指针时,就会调用基类的析构函数而不调用派生类析构函数,就可能造成派生类对象析构不完全,造成内存泄漏。
(3)为什么构造函数不能是虚函数?
答:①从存储空间角度:虚函数相应指向一个V-Table虚函数表的指针,这个指针实际上是存储在对象的内存空间中的,而构造函数时,对象还没实例化,内存空间也没分配,故构造函数不能是虚函数。
②从使用角度:虚函数主要用于信息不全的情况,能使重载的函数得到相应的调用。而构造函数本身就是为了初始化实例。