一、什么是多态
多态是类的三大特性之一(封装,继承,多态)。多态是指由继承而产生的相关不同的类,其派生类对象对同一个消息会作出不同响应。
二、引入多态的目的
能增加程序的灵活性,可以减轻系统升级和维护,调试的工作量和复杂度就大大降低,程序可拓展性好。
三、怎么实现多态
多态其实可以分为静态多态(静态编译,编译时的多态性)和 动态多态(动态绑定,运行时的多态性)。静态多态主要是函数重载,动态多态主要是抽象类的多态。以下篇幅讲动态多态。
1、实现多态的前提是必须有继承,并且基类中要有虚函数
2、派生类中要有虚函数重写
3、派生类对象的地址赋值给基类的指针,通过基类的指针或者基类的引用
四、多态的其他名词解释
1、虚函数
虚函数是用关键字(virtual)修饰的函数,通常出现在基类中,要实现多态,在派生类中必须重写基类的虚函数。基本语法如下:
// 虚函数 virtual 数据类型 函数名(数据类型 形参...) { // 函数体 }
2、纯虚函数
虚函数的升级版,使用0去给虚函数赋值。纯虚函数是没有函数体的,所以,它就不能被调用,为了防止它被调用,将有纯虚函数的类变为抽象类,语法如下:
virtual 数据类型 函数名() = 0; // 后面必须是0,不能是1或者其它
3、抽象类
拥有纯虚函数的类为抽象类。抽象类的特点是不能被实例化,但是,还是可以声明指针或者引用来使用。
4、虚析构函数
将基类的析构函数设置为虚函数,就是虚析构函数。多态的实现是采用基类指针或者基类引用,在销毁对象时,采用delete方式,此时由于指针是基类的,所以只会释放基类对象,不会释放派生类对象,造成内存泄漏。因此,引入虚析构函数来解决这个问题。基本语法如下:
class Base { public: // 虚析构函数,前面直接加关键字virtual virtual ~Base() { } }
五、多态与虚继承的对比
多态原理实现过程:
1、在类设计时,使用关键字(virtual)来声明函数为虚函数,目的是告诉编译器该函数要进行晚捆绑(运行时多态)。
2、编译器在编译时,就准备好虚表(vtable)和 虚指针(vptr)。
3、执行晚捆绑(发生在程序运行时),通过实际的对象来使用虚指针去查虚表。
4、在虚表中,查找到该函数则执行。
虚继承实现过程:
1、在类进行继承时,使用关键字(virtual)来声明为虚继承,目的是告诉编译器要进行虚继承。
2、编译器在编译时,就将虚基类空间只保留一份,并且准备好虚表(vtable)和虚指针(vptr)。
3、在访问虚基类中成员时,也要查表,虚指针和虚表由编译器来进行维护。
4、对于虚基类中的成员,任何对象对它进行修改,数据都会发生改变。
六、案例
1、证明虚指针
#include <iostream> using namespace std; class Base { public: Base() { cout << "Base()" << endl; } virtual void func() = 0; // 纯虚函数 }; int main() { // Base类中没有任何成员变量,但是有一个纯虚函数,占一个指针空间 cout << "sizeof(Base): " << sizeof(Base) << endl; // 在64位空间中一个指针占8个字节,在32位空间中一个指针占4个字节 cout << "sizeof(int*): " << sizeof(int*) << endl; return 0; }
2、多态的简单使用
#include <iostream> using namespace std; class Animal { public: // 虚函数 virtual void eat() { cout << "Animal::eat()" << endl; } }; class Dog:public Animal { public: // 重写虚函数,virtual 关键字可加可不加,推荐不加,因为编译器最终都会自动加上 void eat() override // override关键字作用是用来说明该函数的特性,提高程序的可读性 { cout << "Dog::eat()" << endl; } }; class Cat:public Animal { public: void eat() override { cout << "Cat::eat()" << endl; } }; int main() { Animal* p[3]; // 使用基类指针或者基类引用来调用多态 p[0] = new Animal; p[1] = new Dog; p[2] = new Cat; p[0]->eat(); p[1]->eat(); p[2]->eat(); return 0; }
3、纯虚函数的使用
#include <iostream> using namespace std; // 抽象类 class Animal { public: // 纯虚函数 virtual void eat() = 0; }; class Dog:public Animal { public: // 重写虚函数,virtual 关键字可加可不加,推荐不加,因为编译器最终都会自动加上 void eat() override // override关键字作用是用来说明该函数的特性,提高程序的可读性 { cout << "Dog::eat()" << endl; } }; class Cat:public Animal { public: void eat() override { cout << "Cat::eat()" << endl; } }; int main() { // Animal animal; // 抽象类不能实例化 Dog dog; Cat cat; // 使用基类引用来调用纯虚函数 Animal &p1 = dog; Animal &p2 = cat; p1.eat(); p2.eat(); return 0; }
4、虚析构函数的使用
(1)没有声明虚析构函数
#include <iostream> using namespace std; class Base { public: Base() { cout << "Base()" << endl; } virtual void func() = 0; // 纯虚函数 ~Base() { cout << "~Base()" << endl; } }; class Test:public Base { private: int *p; public: Test(int len = 1) { cout << "Test()" << endl; p = new int[len]; } ~Test() { cout << "~Test()" << endl; delete []p; } void func() override { cout << "Test::func()" << endl; } }; int main() { Base *p = new Test(5); // 基类指针使用派生类对象 delete p; // 没有使用虚析构函数,只会调用基类的析构函数 return 0; }
(2)声明虚析构函数
#include <iostream> using namespace std; class Base { public: Base() { cout << "Base()" << endl; } virtual void func() = 0; // 纯虚函数 virtual ~Base() { cout << "~Base()" << endl; } }; class Test:public Base { private: int *p; public: Test(int len = 1) { cout << "Test()" << endl; p = new int[len]; } ~Test() { cout << "~Test()" << endl; delete []p; } void func() override { cout << "Test::func()" << endl; } }; int main() { Base *p = new Test(5); // 基类指针使用派生类对象 delete p; // 使用虚析构函数,会调用派生类的析构函数 return 0; }
七、总结
多态是C++类三大特性之一,多态可以分为编译时的多态和运行时的多态,实现运行时的多态需要有抽象类,并且抽象类用指针或者引用指向派生类对象。建设将析构函数改写成虚析构函数。注意区分虚继承和多态中的虚函数。