基类指针可以指向子类对象,假设基类中的函数并没有定义成虚函数,那么这个指针就无法调用子类的函数,思维习惯上,如果指针指向了子类对象就应该调用子类的成员函数,但是如果不声明该函数为虚函数,就无法访问子类的成员函数。此外想说一下,基类指针,指向派生类对象,也就只能访问虚函数对应的函数,其他成员函数也不能访问,派生类新定义的成员变量不可以访问,派生类从基类继承的成员变量可以访问。
C++中虚函数的唯一用处就是构成多态。C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问继承下来的成员变量。
下面是虚函数构成多态的条件:
- 必须存在继承关系;
- 继承关系中必须有同名的虚函数,并且它们是覆盖关系。
- 存在基类的指针,通过该指针调用虚函数。
1.虚函数表
每当创建一个含有虚函数的类,或者从包含有虚函数的类派生出一个类时,编译器就为这个类创建一个唯一的虚函数表,表中放置在类中,或者基类中已经声明为virtual的函数的地址,如果派生类没有为虚函数重新定义,那么它的虚函数表里面这一函数项存储的地址是基类里虚函数的地址。编译器在类中放置一个指针VPTR,指向类对应的虚函数表,如果这个类是简单继承的话,那么每一个对象只有一个VPTR,指针VPTR必须指向虚函数表的起始地址,这个过程在构造函数中完成。当指向子类对象的基类指针调用虚函数时,要特殊处理,并不是简单的函数调用。
必须保证对于虚函数的调用发生在VPTR之前,它会在构造函数中完成初始化,如果没有定义构造函数,那么编译器自动生成的默认构造函数,它只做VPTR的初始化工作。
2.抽象类和纯虚函数
- 什么是抽象类?
当一个类中有一个函数,在定义时,加上virtual,在函数声明的末尾加上=0;那么这个类就是一个抽象类,它不能实例化出对象。抽象类派生出的类如果不能全部实现纯虚函数的定义,那么这个派生类还是抽象类。可以对纯虚函数进行定义。 - 为什么要创建抽象类?
抽象类的唯一作用就是提供一个统一的接口,它建立一个基本的格式,来确定什么是对所有派生类是公共的。 - 抽象类不能实例化对象,因为在虚函数表中,会为纯虚函数保留一个位置,但是没有函数的地址。它的虚函数表就是不完全的,这样的类创建对象时,编译器会报错。
- 纯虚函数禁止对抽象类的函数以传值的方式调用,这也是防止对象切片的一种方法。通过抽象类可以保证在向上类型转换期间总是使用指针或者引用。
- 抽象类中定义纯虚函数,我们希望一些所有类的公共代码,能在基类中定义好,而不是每次都要在子类中实现一遍,在基类中对纯虚函数进行定义就是这个目的。在子类中调用抽象类的纯虚函数的方式是,抽象类类名::函数名。
- 当一个对象进行向上类型转换,而不使用地址或者引用,那么就会发生切片,只剩下来适合于目的的子对象。例如,一个函数的参数是一个基类的对象,子类对象传值过去调用这个函数,基类的拷贝构造函数被调用,该构造函数初始化VPTR指向基类的虚函数表,并且只拷贝子类对象与基类重叠的部分,因此这个对象在切片过程中真正变成一个基类对象。纯虚函数之所以能防止切片的产生,原因在于如果基类是一个抽象类,那么它就不允许创建基类对象,编译器会产生一堆错误来防止切片发生。