前言
在上一篇文章中,我们介绍了c++中类与对象的继承,继承可以根据一个或多个类来定义一个新的类,减少代码量,使得开发和维护一个应用程序变得更加的容易。本文将介绍c++继承的重要应用 —— 多态。
一、多态
Q:什么是多态?
A:多态是同一个事物在不同场景下的多种形式,具体讲就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
多态是c++面向对象的三大特性之一,有着动态改变程序的功能,一般分为两类:静态多态和动态多态。
1、静态多态
Q:什么是静态多态?
A:静态多态,又称为编译期多态,它的特点就是函数地址早绑定,即在系统编译期间就可以确定程序将要执行哪个函数。函数重载和运算符重载,都是静态多态。
#include<iostream> using namespace std; class base1 { public: void show() { cout << "父类中的show函数" << endl; } }; class son1 : public base1 { public: void show() { cout << "子类son1中的show函数" << endl; } }; class son2 : public base1 { public: void show() { cout << "子类son2中的show函数" << endl; } }; int main() { base1 *a,*b; a = new son1; b = new son2; a -> show(); b -> show(); return 0; }
在这个例子中,我们在父类base1与两个子类son1 son2中都实现一个show方法,我们分别调用两个子类的show方法,最终输出的都是 父类中的show函数。这说明在静态多态中,在系统编译期间就可以确定程序将要执行哪个函数,函数地址绑定父类中的show函数地址。
2、动态多态
Q:什么是动态多态?
A:动态多态的各种实现方法与静态多态一致,只不过多了一个virtual关键字来修饰,动态多态通过派生类和虚函数在运行时实现。
#include<iostream> using namespace std; class base1 { public: virtual void show() { cout << "父类中的show函数" << endl; } }; class son1 : public base1 { public: void show() { cout << "子类son1中的show函数" << endl; } }; class son2 : public base1 { public: void show() { cout << "子类son2中的show函数" << endl; } }; int main() { base1 *a,*b; a = new son1; b = new son2; a -> show(); b -> show(); return 0; }
在这个例子中,我们使用了动态多态,可以成功调用两个子类中的show函数。
二、虚函数与纯虚函数
1、虚函数
Q:什么是虚函数?
A:我们将被virtual修饰过的函数称为虚函数.
从上面案例的运行结果来看,用基类的指针指向一个派生类时,如果调用了虚函数,则会调用派生类对应的虚函数而不是基类本身所拥有的虚函数。
2、纯虚函数
Q:什么是纯虚函数?
A:纯虚函数与虚函数相同,就是一个被virtual修饰过的函数,但是没有函数体,直接等于 0。当一个类中有了纯虚函数,这个类就称为抽象类。抽象类无法实例化对象,子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
三、多态的实现原理
1、虚函数表
Q:什么是虚函数表?
A:虚函数表中存储着虚函数的地址。当派生类重新定义虚函数时,则将该函数的地址添加到虚函数表中。
2、虚函数指针
Q:什么是虚函数指针?
A:当使用虚函数的时候,类空间对象会占用更大的内存空间。编译器会给每一个对象添加一个隐藏变量:指向虚函数表的指针。
注意:无论类的对象中定义了多少个虚函数,虚函数指针只有一个。
3、多态的实现原理
当调用虚函数时,由于派生类对象重写了派生类对应的虚函数表项,基类在调用时会调用派生类的虚函数,从而产生多态。
四、虚析构函数
当通过delete关键字删除有派生类对象的基类指针时,只会调用基类的析构函数,派生类的空间并没有被释放,会造成内存泄漏。为了避免内存泄露,需要使用虚析构函数在删除基类指针时可以调用子类的析构函数释放子类中堆内存,防止内存泄露。
我们来看看下面这个例子:
#include<iostream> using namespace std; class base1 { public: base1() {} ~base1() { cout<<"delete base1"<<endl; } virtual void show() { cout << "show base1" << endl; } }; class son1 : public base1 { public: son1() {} ~son1() { cout << "delete son1" << endl; } void show() { cout << "show son1" << endl; } }; int main() { base1 *p = new son1; p -> show(); delete p; return 0; }
在这个例子中,我们通过delete关键字删除指针时,没有调用派生类的析构函数。我们试试将基类中的析构函数改为虚析构函数。
#include<iostream> using namespace std; class base1 { public: base1() {} virtual ~base1() { cout<<"delete base1"<<endl; } virtual void show() { cout << "show base1" << endl; } }; class son1 : public base1 { public: son1() {} ~son1() { cout << "delete son1" << endl; } void show() { cout << "show son1" << endl; } }; int main() { base1 *p = new son1; p -> show(); delete p; return 0; }
可以发现,成功调用了派生类的析构函数,没有造成内存泄漏。
总结一下:
如果父类的析构函数不加上virtual关键字
当父类的析构函数不为虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。
如果父类的析构函数加上virtual关键字
当父类的析构函数为虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。