1.多态的概念
多态按字面的意思就是多种形态。从系统实现的角度来看,多态性分为两类:静态多态和动态多态。
静态多态是通过函数重载和运算符重载来实现的,静态多态又称编译时的多态,要求在程序编译时系统就能决定要调用的是哪个函数。
动态多态是通过虚函数来实现的,动态多态又称运行时的多态,它是在程序运行过程中才动态地确定操作所针对的对象。
由于这里讲述的是类的多态性,因此不过多介绍静态多态的内容,而是重点介绍动态多态,即虚函数。
2.利用虚函数实现动态多态
(1)虚函数的概念
- 虚函数,就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。
- 虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
(2)实现动态多态的条件
- 需要有继承关系:即派生类从基类继承。
- 基类的成员函数需要被声明为虚函数。
- 派生类对虚函数进行重写,从而提供自己的实现。
- 使用基类的指针或引用来调用虚函数,从而达到动态绑定的效果。
(3)虚函数的使用方法
- 在基类中,用virtual关键字声明成员函数为虚函数,在类外定义虚函数时,不必再加virtual。
- 在派生类中,重新定义此函数,要求函数名、函数类型、函数参数完全相同,函数体可根据需要定义。
- 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
- 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
#include <iostream> using namespace std; class Base //基类 { public: virtual void show() // 基类虚函数 { cout << "This is the base class show function" << endl; } virtual ~Base() {} // 虚析构函数,防止内存泄漏 }; class Derived : public Base //派生类 { public: void show() override // 重写基类的虚函数 { cout << "This is the derived class show function" << endl; } }; int main() { Base *b; // 声明基类指针 Derived d; // 定义派生类对象 b = &d; // 将基类指针指向派生类对象 b->show(); // 调用派生类的 show(),而不是基类的 show() return 0; }
当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类中重新声明该函数时,可以加virtual也可以不加。
如果在派生类中,没有对基类中的虚函数重新定义,则派生类简单地继承该虚函数。
(4)override关键字的使用
可以看到,上述代码中,在派生类中对基类的虚函数进行重写时,使用到了override关键字。首先,这个关键字是可选的,即可以写或者不写,但是强烈推荐写。
其次,override的作用有:
-
明确意图:当使用
override
关键字时,表示有意去重写基类中的虚函数。其清晰表达了代码设计意图,提升了代码的可读性和可维护性。 -
编译器检查:编译器会检查基类中是否确实存在一个虚函数与派生类中声明的函数具有相同的签名(包括函数名、参数类型和返回类型)。如果没有这个虚函数,编译器将生成一个错误。这样可以防止由于拼写错误、参数类型不匹配等原因导致的错误重写。
(5)虚析构函数
可以看到,上述代码中,在基类中对析构函数声明时,使用到了virtual关键字,表示这是一个虚析构函数。 这么做的原因是防止内存泄漏的可能。那为什么会出现内存泄漏呢?
内存泄漏的潜在情况:
假如定义了一个基类Base和一个派生类Derived,如果定义一个基类的指针,指向派生类的对象,然后删除这个指针,会发生什么?
Base* p = new Derived();
delete p;
这段代码有一个潜在问题:析构函数的调用不完全。因为 p
是一个指向 Derived
对象的 Base
类指针,但 Base
类的析构函数不是虚函数,所以当调用 delete p
时,只有 Base
的析构函数会被调用,而不会调用 Derived
的析构函数。这就意味着 Derived
类特有的资源(例如动态分配的内存)可能不会被正确释放,从而导致内存泄漏。
那如何防止这种内存泄漏的情况呢?
将基类的析构函数用virtual关键字声明为虚析构函数。
此时,delete
操作会先调用 Derived
的析构函数,然后再调用 Base
的析构函数。这是因为 delete
操作在对象销毁时会依据虚函数表(V-Table)来正确地选择最合适的析构函数顺序。
- 专业人员一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。
(6)虚函数表
C++通过维护一个虚函数表(V-Table)来实现动态多态。每个包含虚函数的类都有一个虚函数表,该表存储了所有虚函数的指针。每个对象都有一个虚函数表指针(V-Table Pointer),用于指向其类的虚函数表。当使用基类指针调用虚函数时,程序会根据对象的虚表指针找到虚函数表,并在表中查找对应的虚函数实现,从而调用实际的函数。
3.纯虚函数与抽象类
(1)纯虚函数
纯虚函数是一种特殊的虚函数,它只在基类中声明而没有定义。纯虚函数的语法格式为:
virtual 函数类型 函数名(参数列表) = 0;
需要注意:
- 纯虚函数没有函数体。
- 最后面的“=0”不表示其返回值为0,只起一个形式上的作用,表示这是纯虚函数。
- 这是一个声明语句,后面应有分号。
- 基类并不需要这个函数,只是出于派生类的需要,为派生类保留一个函数的名字,以便派生类根据需要对它进行定义。
(2)抽象类
在面向对象程序设计中,往往有一些类,它们不用来生成对象。定义这些类的唯一目的是为了给其他类提供一个可以继承的适当的基类。它们作为一种基本类型提供给用户,用户在此基础上根据需要定义出各种派生类。这就是抽象类。
凡是包含纯虚函数的都是抽象类。抽象类不能实例化,即不能用来生成对象,它只能作为接口使用。相对的,可用于实例化对象的类被称为具体类。
接口描述了类的行为和功能,而不需要完成类的特定实现,C++ 接口是使用抽象类来实现的。