什么是多态?
所谓的多态说简单来讲就是不同的对象去完成相同的工作时,会产生不同的状态。
多态分为两种:一种是编译时的多态(静态多态),另一种是运行时的多态(动态多态)。
编译时的多态
编译时的多态的实现与静态连编有关。
(1)那么什么是静态连编呢?
所谓的连编就是将函数名和函数体的代码联系在一起的过程。而所谓的静态连编就是在编译时进行的连编。程序在编译期间,编译器通过对实参与形参的比较,对于同名的重载函数便根据参数上的差异进行区别,然后进行连编,如此就实现了编译时的多态。
(2)编译时的多态可以用两种方式来实现:
- 函数重载
- 泛型编程
运行时的多态
运行时的多态则是通过动态连编实现的,所谓的动态连编就是在运行阶段完成的连编。即当程序调用到某一个函数时,才去寻找和连接到与之对应的程序代码。
(1)构成多态的条件
- 必须通过基类的指针或者引用去调用函数。
- 基类中必须包含虚函数,且在派生类中必须完成了虚函数的重写。
问题:什么是虚函数?
在成员函数前面加上关键字virtual就是虚函数。
问题:什么是虚函数的重写?
派生类中存在的虚函数与其基类中的虚函数函数名、参数、返回值都相同(协变例外),那么我们就说派生类中的虚函数重写了基类中的虚函数。而且虚函数重写也叫做虚函数的覆盖。
多态的原理
结合下面这段代码进行分析:
class Person
{
public:
......
virtual void show_name();
virtual void show_age();
......
};
class Student:public Person
{
public:
......
virtual void show_age(); //重新定义
virtual void show_all(); //新增的虚函数
......
}:
虚函数表(虚表):
所谓的虚函数表本质上是一个指针数组,这个数组中的每一个元素都是一个指针,而这个指针指向的是就是虚函数的地址。换句话说,虚函数表中存放的就是虚函数的地址。
虚表指针:
编译器如果发现这个类中有虚函数,那么它就会给在个类的对象中添加一个隐藏的成员,而这个隐藏成员本质上其实就是一个二级指针(vfptr),也就是说,它指向的是函数地址数组的指针,我们把这个指针叫做虚表指针。虚表指针指向的就是虚表的首地址。
问题:派生类是如何继承基类的虚函数?
对于每一个类,编译器都会为其创建一个虚函数表。也就是说,基类对象有一个虚表指针,该指针指向了基类中的虚函数表。派生类中同样也有一个虚表指针,该指针指向了派生类的虚函数表。如果派生类中提供了虚函数的新定义,那么就会在派生类中的虚函数表中添加该新虚函数的地址;如果派生类中没有新定义某个虚函数,那么就会直接在虚函数表中保存原始虚函数的地址;如果在派生类中新定义了一个虚函数,那么就会在虚函数表中直接添加该新虚函数的地址。如下图所示:
因为在Student类的虚函数表中没有重新定义show_name
函数,所以该函数的地址和基类中的地址一致;而show_age
函数在Student类中重新定义,所以地址与原来不一样;在Student类中新增加了一个虚函数show_all
,所以就直接在虚函数表的后面增加了该函数的地址。
原理:
调用虚函数时,程序将首先找到存储在对象中的虚表指针,然后根据虚表指针再找到虚函数表的首地址。如果要使用类的声明中的第一个虚函数,那么就使用这个数组中的第一个函数地址,然后到该地址去执行这个函数。如果要使用类的声明中的第二个虚函数,那么就使用这个数组中的第二个函数地址,然后到该地址去执行这个函数。
一、虚函数相比于非虚函数的缺点:
1. 内存会增加
(1)每个对象都会增大,因为会多一个虚表指针
(2)编译器会为每一个类都创建一个虚表
2. 执行效率减低
(1)每一次虚函数调用时,都需要执行一个额外的操作:到虚表中查找地址
二、不能作为虚函数的函数类型:
1. 构造函数
(1)因为派生类不继承基类的构造函数,所以没意义。
(2)虚表指针是在构造函数成员初始化列表阶段才初始化的
2. 友元
因为友元不是类的成员,而虚函数必须是成员函数。
3. 全局函数
4. 静态成员函数,它没有this指针,也不是成员函数
5. 拷贝构造函数,以及赋值运算符重载(可以但是不建议作为虚函数)
三、如果基类中有虚函数,析构函数为什么最好也要声明为虚函数?
假如,有这样一句语句:Base* p=Deriver d;
那么如果没有将析构函数声明为虚函数会出现什么情况呢?当我们delete p
时,因为此时p
是Base
类型,所以它只会调用基类的析构函数,也就是说只能销毁派生类d
中属于基类成员的那一部分,而在派生类d
中新增的成员而无法被销毁,这就造成了一个诡异的“局部销毁”的局面,从而造成了内存泄漏。因此我们将析构函数声明为虚函数,当我们delete p
时,首先调用派生类的析构函数,销毁派生类d
中新增的成员,然后再调用基类的析构函数,销毁从基类中继承的成员。