C++多态
前言
本节内容,将会用到前面类的相关知识以及C++基础教程以及C++类&对象,需要你具备前面的知识,如果你还有不会的或者说不熟悉的地方,请重新温习一下
C++ 多态:让对象具备“多种形态”的能力
多态,顾名思义,就是“多种形态”。在 C++ 中,多态是面向对象编程(OOP)的三大特性之一(另两个是封装和继承)。多态使得程序可以使用统一的接口来操纵不同类型的对象,从而实现代码的灵活性和可扩展性。
目录
1. 什么是多态?
多态是指程序中对象的多种表现形态,具体来说,就是使用基类指针或引用,在运行时根据对象的实际类型,调用对应的重写方法。这意味着同一个函数调用,根据对象类型的不同,会表现出不同的行为。
举个生活中的例子:
假设有一个“动物”这个概念,动物会“发出声音”。不同的动物,发出的声音不同。猫会“喵喵叫”,狗会“汪汪叫”。在程序中,我们可以通过一个统一的接口(如 speak() 方法)来让不同的动物发出各自的声音。
2. 虚函数的概念
2.1 什么是虚函数?
虚函数(virtual function)是使用 virtual 关键字声明的成员函数,目的是为了允许子类重写该函数,并在运行时通过基类指针或引用调用子类的实现。
2.2 为什么需要虚函数?
在基类中,如果某个函数可能会被子类重写,并且你希望通过基类指针调用子类的实现,就需要将该函数声明为虚函数。
2.3 示例
1 | class Animal { |
2.4 如何使用
1 | void makeAnimalSpeak(Animal* animal) { |
在上述代码中,尽管我们使用的是 Animal* 类型的指针,但实际调用的是 Dog 和 Cat 各自的 speak() 方法。
3. 动态绑定 vs 静态绑定
3.1 静态绑定
- 定义:函数调用在编译时就已经确定,称为静态绑定或早期绑定。
- 特点:编译器根据对象的静态类型决定调用哪个函数。
3.2 动态绑定
- 定义:函数调用在运行时根据对象的实际类型来决定,称为动态绑定或晚期绑定。
- 特点:需要通过基类指针或引用调用虚函数。
3.3 示例对比
静态绑定
1 | Dog dog; |
动态绑定
1 | Animal* animal = new Dog(); |
如果 speak() 不是虚函数,那么即使使用基类指针,也会调用 Animal::speak(),这是因为没有动态绑定,函数调用在编译时已经确定。
4. 纯虚函数与抽象类
4.1 纯虚函数
- 定义:没有实现的虚函数,声明时在函数签名后加
= 0。 - 目的:要求派生类必须实现该函数,否则派生类也将是抽象类,无法实例化。
4.2 抽象类
- 定义:包含纯虚函数的类称为抽象类。
- 特点:
- 不能创建抽象类的实例。
- 可以包含成员变量和已经实现的函数。
- 用于定义接口,供子类继承和实现。
4.3 示例
1 | class Shape { |
4.4 使用抽象类
1 | void renderShape(Shape* shape) { |
5. 多态的实现原理
5.1 虚函数表(V-Table)
- 概念:编译器为每个包含虚函数的类创建一个虚函数表,记录该类的虚函数地址。
- 作用:用于在运行时动态决定调用哪个函数。
5.2 虚函数指针(V-Ptr)
- 概念:每个对象都有一个隐藏的指针,指向所属类的虚函数表。
- 作用:通过虚函数指针找到虚函数表,从而调用正确的函数实现。
5.3 调用过程
当通过基类指针或引用调用虚函数时:
- 程序根据对象的虚函数指针找到对应的虚函数表。
- 在虚函数表中查找对应函数的地址。
- 调用该地址对应的函数实现。
5.4 示例图解
类结构
1
2
3
4
5Animal
├── speak() [virtual]
↓
Dog : public Animal
└── speak() [override]对象内存布局
Animal 对象
1
2[ V-Ptr ] ---> [ Animal V-Table ]
├── speak() --> Animal::speak()Dog 对象
1
2[ V-Ptr ] ---> [ Dog V-Table ]
├── speak() --> Dog::speak()
6. 为什么要使用多态?
6.1 代码复用性
- 统一接口:通过基类指针或引用,可以使用同一个函数操作不同类型的对象。
- 减少重复代码:避免为每个子类编写重复的函数调用代码。
6.2 扩展性
- 易于维护和扩展:添加新的子类无需修改现有代码,只需确保新子类实现了必要的虚函数。
- 降低耦合度:模块之间依赖于抽象的接口,而非具体的实现。
6.3 灵活性
- 运行时决定行为:程序可以在运行时根据实际对象类型执行不同的操作。
- 支持多态容器:可以创建包含基类指针的容器,存储不同类型的对象。
7. 使用多态需要注意的问题
7.1 析构函数应为虚函数
原因:当通过基类指针删除子类对象时,如果基类析构函数不是虚函数,可能导致资源未正确释放。
示例
1
2
3
4
5
6class Animal {
public:
virtual ~Animal() {
std::cout << "Animal destroyed." << std::endl;
}
};
7.2 对象切片
- 概念:当将子类对象赋值给基类对象时,子类特有的数据会被“切掉”。
- 避免方法:尽量使用指针或引用来处理对象。
7.3 性能开销
- 动态绑定的成本:多态机制需要在运行时查找函数地址,略微增加了执行时间和内存开销。
- 权衡:通常,这些开销是可以接受的,不会对性能产生显著影响。
7.4 虚函数不能是内联函数
- 原因:虚函数在运行时决定调用哪个版本,无法在编译时内联展开。
8. 总结
多态是 C++ 面向对象编程的核心特性之一,它通过虚函数和动态绑定机制,使得程序可以在运行时根据对象的实际类型执行相应的操作。
多态的优点:
- 提高代码的复用性和可维护性。
- 增强程序的扩展性和灵活性。
- 使代码更符合面向对象的设计思想。
使用多态的关键:
- 使用虚函数来允许子类重写基类方法。
- 通过基类指针或引用来调用虚函数。
- 当需要定义统一接口时,使用纯虚函数和抽象类。
希望以上内容能够帮助您更深入地理解 C++ 中的多态机制,并在实际编程中有效地应用它。多态虽然概念简单,但在大型项目中能够大大简化代码结构,提高开发效率。
如果您有任何疑问,或者需要进一步的解释,请随时提问!








