虚函数

  • 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。

    对于第一句话,从代码角度来说,就是父类写了一个virtual函数,如果子类覆盖它的函数不加virtual,也能实现多态。因为virtual修饰符会被隐形继承。子类的空间中有父类的所有变脸(static除外)。同一个函数只存在一个实体(inline除外)。子类覆盖它的函数不加virtual,也能实现多态。在子类的空间里,有父类的私有变量。私有变量不能直接访问。

  • 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。

  • 只有使用指针或者引用调用才会启用虚拟机制,如果指名道姓地调用一个对象,c++编译器会自动优化,去除任何的额外开销。

实现机制

虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。

虚函数表

虚函数表存放的内容:类的虚函数的地址。
虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
注:虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。

示例

  1. #include <iostream>
  2. using namespace std;
  3. class Base
  4. {
  5. public:
  6. virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
  7. virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
  8. virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
  9. };
  10. class Derive : public Base
  11. {
  12. public:
  13. virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
  14. virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
  15. virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
  16. };
  17. int main()
  18. {
  19. Base *p = new Derive();
  20. p->B_fun1(); // Base::B_fun1()
  21. return 0;
  22. }

主函数中基类的指针 p 指向了派生类的对象,当调用函数 B_fun1() 时,通过派生类的虚函数表找到该函数的地址,从而完成调用。

基类的虚函数表
image.png
派生类的虚函数表
image.png

虚析构函数

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

纯虚函数

纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。

  1. virtual int A() = 0;

虚函数 VS 纯虚函数

  • 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
  • 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
  • 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
  • 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
  • 虚基类是虚继承中的基类,具体见下文虚继承。