概念
所谓的虚函数,就是那些被virtual关键字所修饰的类成员函数。
- 作用:实现多态性,将接口和实现进行分离。通俗的来说,就是相同的函数具有不同的实现,但因为个体差异而采取不同的策略。
抽象基类:
- 在基类中假如至少一个纯虚函数,使基类称为抽象类。
纯虚函数
纯虚函数是基类中声明的虚函数,它在基类中没有定义,但是要求任何派生类都要定义自己的实现方法。
在基类中实现纯虚函数的方法是在函数原型后加上“=0”,例如:
virtual void func() = 0;
关于重写
如果一个函数在基类中被声明为virtual,那么在所有的派生类中它都是virtual,在派生类中virtual函数的重定义通常称为重写。
晚捆绑
对于晚捆绑:
- 晚捆绑是相对于早捆绑而言的。
- 对于捆绑:指的是将函数体与函数调用相联系称为捆绑。
- 当捆绑在程序运行之前完成时,就称为早捆绑。
- 当捆绑根据对象的类型,发生在运行时,就称为晚捆绑。
当使用晚捆绑时,无需检查对象的类型,只需要检查对象是否支持特性和方法即可。
为了引发晚捆绑,C++要求在基类中声明这个函数时使用virtual关键字即可。
晚捆绑只对virtual函数起作用,而且只在使用含有virtual函数的基类的地址时发生。
如何实现晚捆绑
虚函数主要有以下两个步骤:
- 每一个类产生出一堆指向虚函数的指针,放在表格中。这个表格被称为virtual table(vtbl)。
- 每一个类对象被安插一个指针,指向相关的virtual table,通常这个指针被称为vptr。
结构图如下:
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个唯一的vtbl。
如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类的这个虚函数地址,然后编译器在这个类中放置vptr。
当使用简单继承时,对于每个对象都只有一个vtbl。vptr必须被初始化为指向相应的vtbl的起始地址。
虚函数的存放类型信息
- 假如没有虚函数,那么对象的长度就是所期望的长度:比如当个int的长度。
- 而带有单个虚函数的One Virtual,对象的长度是No Virtual的长度加上一个void指针的长度。
- 如果有一个或多个虚函数,编译器都只在这个结构中插入一个单个指针,这个指针指向虚函数表。在32为的机器上,一个指针占3字节的空间,因此求sizeof得到4;如果是64位的机器,一个指针占8字节的空间,因此求sizeof则得到8。
关于抽象类和纯虚函数
- 当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出来的类也将会是一个抽象类。
- 声明一个纯虚函数,就等于告诉编译器在vtbl中为函数保留一个位置,但是这个位置不放地址。只要有一个函数在类中被声明为纯虚函数,则vtbl就是不完全的。
- 纯虚函数禁止对抽象类的函数以传值方式调用,这是一种防止切片的方法。抽象类可以保证在向上类型转换期间总是使用指针或引用。
- 对于纯虚函数,如果要创建对象,必须要在派生类中定义。
对象切片
在继承的过程中,通常派生类不仅具有基类的特征,也具有自身的一些特征。当派生类向上进行类型转换称为基类时,就会发生那些自身的特征被切除,只保留继承了基类的特征,这种现象就是对象切片。
虚函数和构造函数
- 由于基类构造函数总是在继承类构造函数中被调用,这就确保了在派生类中,基类的所有成员都是有效的,即所有成员都已经建立。
- 虚机制在构造函数中不工作。有两种理由:
- A. 在任何构造函数中,我们只能知道基类已被初始化,但不能知道哪个类是从这个基类继承来的。但是,虚函数在继承层次上是向前和向后调用。它可以调用派生类中的函数。
- B. 构造函数的vptr的状态是由最后调用的构造函数确定的,这就意味着当最后调用的构造函数还没有完成之前,当前的构造函数完全不知道这个对象是否是基于其他类的。但是,当这一系列的构造函数调用正发生时,每个构造函数都已经设置vptr指向它自己的vtbl,如果函数调用使用虚机制,它将只产生通过它自己的vtbl的调用,而不是最后派生的vtbl。
虚析构函数和析构函数
- 析构函数自最晚派生的类开始,并向上到基类。这就意味着每个析构函数知道它所在类从哪一个类派生而来,但不知道从它派生出哪些类。
- 析构函数可以为虚函数,因为这个对象已经知道它是什么类型,但是在构造期间就不知道了。一旦对象已被构造,它的vptr就已经被初始化,所以能发生虚函数调用。
- C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
虚函数、纯虚函数、抽象类的作用
- 虚函数的作用:每个类必须提供一个可以被调用的虚函数,但每个类可以按它们认为合适的任何方式处理。如果某个类不想做什么特别的事,可以借助于基类中提供的缺省处理函数。也就是说,虚函数的声明是在告诉子类的设计者,“你必须支持虚函数,但如果你不想写自己的版本,可以借助基类中的缺省版本”。
- 纯虚函数的作用:让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
- 抽象类的作用:抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。