介绍

  • 对于一个派生类以及他的基类,若两者都有一个相同名字的方法,但这两个方法的实际行为并不相同,那应将这个方法声明为虚方法(virtual)。原因在于,如果方法是通过引用或指针调用的,在不声明位虚方法的情况下,程序将根据引用类型或指针类型确定使用的是具体哪个方法。而声明为虚方法,程序将根据引用或指针具体所指向的对象的类型来调用方法
  • e.g.

  使用引用来调用方法

  首先是不使用虚函数的情况

  结果如下:

  然后是使用了虚函数的情况:

  结果如下:

  • 另外,在一个具有派生类的基类中,惯例是将析构函数声明为虚函数,这可以保证释放派生类对象时按照正确的顺序释放。

与派生类有关的的指针和引用类型的兼容性

  • 一个基类指针或引用可以引用派生类对象,而不必进行显式类型转换。其内部逻辑是,公有继承建立的是is-a关系,即派生类也是基类,派生类只不过是基类的一个特例而已(比如说,基类"人"和派生类"中国人",中国人也是人,所以一个指向"人"的指针或引用也可以指向"中国人"对象)。

静态联编与动态联编

  • 联编:通常来说联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序彼此关联的过程。
  • 静态联编:静态联编是指在编译阶段就将函数实现和函数调用关联起来,因此静态联编也叫早绑定,在编译阶段就必须了解所有的函数或模块执行所需要检测的信息
  • 动态联编:动态联编是指在程序执行的时候才将函数实现和函数调用关联,因此也叫运行时绑定或者晚绑定。

  • C中的联编都是静态联编
  • C++中的联编一般是静态联编,在涉及模板、虚函数时则是动态联编

动态联编、静态联编与继承有何关系?

  • 这主要与上面说到的虚函数(虚方法)有关。虚函数使得在编译过程中并不知道哪一个函数会被调用,这就要求在程序执行时才将函数实现和函数调用关联,即动态联编。

一些关于动态联编的问题

  • Q:静态联编能解决的问题,动态联编也能解决,为什么不摒弃静态联编或将动态联编设为默认?

    A:动态联编虽好,但它需要在程序运行时采取一些方法跟踪基类指针或引用指向的对象类型,这导致了额外的处理开销。

  • Q:动态联编是怎么实现的?

    A:在C++中,动态联编的体现就是虚函数。故这个问题可以看作是:"虚函数是怎么实现的"。通常,编译器是这么处理虚函数的:对于一个具有虚函数的对象,为其添加一个隐藏成员。 该成员是一个指针变量,指向一个函数地址数组。数组中的内容是为类对象声明的虚函数的地址。这个数组被称为虚函数表(vtbl)。一个基类对象中的该指针指向基类中的所有虚函数的地址表。 而一个派生类对象的该指针则指向另一个独立的虚函数表。对于一个在基类与派生类中都存在的虚函数(指同名),若派生类中该函数没有被重新定义,则派生类虚函数表中,该函数的地址 与基类中该函数的地址相同。若重新定义了,则派生类的虚函数表将保存该函数的新地址。另外,如果在派生类中生命了一个基类中没有的虚函数,其地址也会被加入派生类的虚函数表中。

    贴一张C++ Primer Plus上的图


在调用虚函数的时候,程序首先查看引用或指针指向的对象的虚函数表的地址,然后再访问相应的虚函数表,再根据实际调用的函数,访问虚函数表中相应的函数的地址。

正是因为这种实现机制,虚函数会带来更大的空间与时间消耗,因为:

1.对于每一个具有虚函数的对象,都要增加一个指针变量
2.对于每一个具有虚函数的类,都要生成一个虚函数表
3.执行过程中还要进行到虚函数表中查找虚函数地址的操作

有关虚函数的注意事项

  • 构造函数不可以是虚函数
  • 析构函数最好是虚函数,除非该类没有派生类
  • 友元函数不能是虚函数,因为友元不是成员函数,而只有成员函数才能成为虚函数
  • 在派生类中重新定义虚函数时,应保持函数原型与基类中的相同(至少参数列表要相同)
    这是因为,在派生类中重新定义虚函数时,并不会生成该函数的两个重载版本,而是在派生类中隐藏掉基类中所有同名的虚函数。 以下代码是一个错误示范。

纯虚函数与抽象基类

  • 使用抽象基类来进行类的设计是一种更系统化、更有序的类设计方法。抽象基类指的是对于要创建的多个类,提取其公有特征,将这些特征整合成一个基类。抽象基类与普通的类的区别在于,抽象基类中包含有至少一个纯虚函数,而纯虚函数只提供接口不提供实现,也就是只需要声明,不需要实现,具体的实现在派生类中进行。要申明一个纯虚函数,只需要在虚函数的声明后面加上一个=0即可。另一个不同在于,抽象基类无法实例化。对于一个继承自抽象基类的派生类,若在这个类中给出了纯虚函数的实现,则这个派生类将转化为具体类,可以实例化。如果没有实现,则仍是抽象类,不能实例化。

  • 抽象基类的意义是什么?

1.最重要的原因是,可以将接口与实现分离。接口是软件产品最有价值的资源,设计接口比实现接口需要耗费更昂贵的成本。因此,要将接口保护起来,以免在针对客户需求修改实现的时候,程序员不小心把接口破坏掉。
2.引入抽象基类和纯虚函数方便实现C++的多态特性。可以用抽象基类的指针去调用子类对象的方法。
3.很多时候,许多基类被实例化是不合理的。例如“形状”这个基类,被实例化之后反而会让人相当费解,所以干脆将“形状”这个类定义为抽象类,由它派生出正方形,三角形等子类。

(引用自C++为什么要定义抽象基类)

来一段代码: