目录
在 C++ 中,多态是面向对象编程的核心特性之一,指 “同一接口,不同实现”—— 通过基类的接口调用时,能根据对象的实际类型执行不同的派生类实现。多态让代码更灵活、可扩展,是实现 “开闭原则”(对扩展开放,对修改关闭)的重要手段。
说白话,多态就是C++的一种特点,那它具体怎么提现,我简单举一个例子,让大家能理解多态的。
还是那王者**来说,王者**有那么多英雄,我们假设想要构建游戏中的角色,在C++这样以面向对象的编程中是怎样来实现的呢?直接说思路,首先创建一个基类叫英雄类,所有的英雄都会继承这个基类,这个类中有英雄最基本的属性,血量、蓝条、攻击范围等。然后再去实现不同的派生类。派生类就是对应的具体英雄,如齐天大圣、诸葛亮等。
那这里什么地方提现了多态呢?现在解释会点空,还是先看例程。
一、多态的实现
#include <iostream>
#include <string>
using namespace std;
// 英雄基类
class Hero {
protected:
string name; // 英雄名称
public:
Hero(string n) : name(n) {}
// 纯虚函数:强制子类重写语音方法
virtual void sayVoice() = 0;
};
// 子类1:亚瑟
class Arthur : public Hero {
public:
Arthur() : Hero("亚瑟") {}
// 重写语音方法(亚瑟专属语音)
void sayVoice() override {
cout << name << ":为了父王的荣耀!" << endl;
}
};
// 子类2:后羿
class HouYi : public Hero {
public:
HouYi() : Hero("后羿") {}
// 重写语音方法(后羿专属语音)
void sayVoice() override {
cout << name << ":太阳,终将照耀大地!" << endl;
}
};
// 子类3:安琪拉
class AnQiLa : public Hero {
public:
AnQiLa() : Hero("安琪拉") {}
// 重写语音方法(安琪拉专属语音)
void sayVoice() override {
cout << name << ":玩火,是会烧到自己的哦~" << endl;
}
};
// 多态调用
void play(Hero* hero) {
hero->sayVoice(); // 同一接口,执行不同英雄的语音
}
int main() {
Hero* hero1 = new Arthur();
Hero* hero2 = new HouYi();
Hero* hero3 = new AnQiLa();
// 自动匹配对应英雄的语音
play(hero1);
play(hero2);
play(hero3);
delete hero1;
delete hero2;
delete hero3;
return 0;
}
结果如下:

这段代码就是一个多态的实现,基类是英雄类,然后不同的英雄都会继承这个类。
多态首先需要创建子类的对象,然后将它的基类指针指向派生类的地址。即:
Hero* hero1 = new Arthur(); //创建亚瑟的对象,然后让基类指向它
为什么要这样写呢,我们来看代码中的play函数。
代码中的play函数实际就是一个打印每个角色的语音的函数,但是每个角色的派生类都不一样,那这个参数类型应该怎样去给呢?。。。。。这里就可以使用多态,直接将类型定为他它的一个基类指针,让这个子类的指针在创建的时候就直接指向它的派生类。那么后面调用直接调用这个HERO基类的指针,不同的英雄它的类型是一样的,但是内部的实现却不一样。这就是多态机制。
二、静态多态和动态多态
1. 静态多态(编译时多态)
- 定义:指在编译阶段就确定具体调用哪个函数的多态行为。
- 实现方式:主要通过函数重载(Overloading)和运算符重载实现。
- 函数重载:在同一作用域内,允许存在多个同名函数,但它们的参数列表(参数类型、个数或顺序)不同,编译器会根据实参类型在编译时匹配对应的函数。
- 运算符重载:对已有的运算符(如
+、<<等)重新定义,使其能作用于自定义类型,编译器在编译时根据操作数类型确定调用的重载版本。
- 特点:依赖编译器的静态绑定,效率较高,但灵活性较低,行为在编译时就已确定。
2. 动态多态(运行时多态)
- 定义:指在程序运行阶段才确定具体调用哪个函数的多态行为。
- 实现方式:主要通过虚函数和继承实现。
- 当子类重写(Override)父类的虚函数时,通过父类指针或引用指向子类对象,调用虚函数时,程序会在运行时根据对象的实际类型(而非指针 / 引用的声明类型)确定调用子类还是父类的函数。
- 特点:依赖动态绑定,灵活性高,能根据运行时对象的实际类型动态调整行为,但由于需要动态查找函数地址(如虚函数表),效率略低于静态多态。
三、虚函数
我们这个主要讲动态多态,因为动态和虚函数相关。
根据之前的代码中,我们看到了这样一段类中函数声明。
// 纯虚函数:强制子类重写语音方法
virtual void sayVoice() = 0;
这个函数声明的前面加了virtual,这是虚函数的关键词。为什么要加上这个呢,这是实现多态必要的,加上virtual才可以对该函数重写(不是重载),如果不加这个关键词,在派生类再写sayVoice()函数,这个叫做函数隐藏,派生类会将基类函数名相同的函数隐藏,这个在上一篇中继承讲过,不熟悉可以看一下上一篇。
再看注释那里还写了纯虚函数。这又要说明了,纯虚函数和虚函数是不一样的。纯虚函数需要在后面后加上 “= 0”。
并且他有几个特点:
- 声明纯虚函数后,该类会成为抽象类,无法被构建。
- 要求子类必须重写该函数,否则子类也会成为抽象类,无法构建。
相比之下,虚函数就比较自由,可以选择重写或者不重写,同时也可以实例化。
以上就是虚函数基本概念和特点,但有一个问题就是虚函数的底层原理有是怎样的呢?
四、虚函数表和虚函数指针
在 C++ 中,动态多态的实现依赖于虚函数表 和虚函数指针,这是编译器在底层自动实现的机制。
虚函数表每个包含虚函数的类,编译器都会为其生成一个全局唯一的虚函数表。这是一个函数指针数组,存储了该类所有虚函数的地址(包括继承自基类且未被重写的虚函数,以及自身重写的虚函数)。
虚函数指针每个包含虚函数的类的对象,都会隐含一个虚函数指针,它指向该对象所属类的虚函数表。虚函数指针 是对象内存布局的一部分,由编译器自动添加,在对象构造时初始化(指向当前类的 虚函数表)。
就是说,每个包含虚函数的类和对应的派生类都会创建一个虚函数表,在对象创建后,对象中会保存一个指向虚函数表的指针叫虚函数指针。
在虚函数表中保存了所有虚函数的函数指针。对于派生类的虚函数表,它的函数指针需要根据自己是否重写对应的虚函数来定,如果重写了,那对应的函数指针就会被更新为重写后的函数指针。没有重写,则还是存为原来基类的函数指针。
那这时再来看这个段代码,就应该能理解多态的底层原理了:
Hero* hero3 = new AnQiLa();
play(hero3);
当play中参数为hero3时,hero3保存的AnQiLa对象的指针,在内部程序调用的就会是调用派生类的虚函数表,派生类会对基类的虚函数进行重写,因而虚函数表中的函数指针会保存为重写后的函数指针,派生类play执行的时候就会调用这个重写后函数,从而实现多态。
五、虚析构函数
1、正常继承的析构顺序(非多态)
在正常的继承时,派生类继承基类在调用析构函数时,析构顺序是先派生类、再基类(与构造顺序相反)。
2.多态场景的问题(基类析构非虚函数)
当通过基类指针指向派生类对象,并删除指针时,此时编译器只根据指针的声明类型 调用析构函数,导致派生类的资源(如动态分配的内存等)无法释放,造成内存泄漏。
下面是一个简单示例:
#include <iostream>
#include <string>
using namespace std;
// 英雄基类(抽象类)
class Hero {
protected:
string name; // 英雄名称
public:
Hero(string n) : name(n) {
cout << "基类Hero构造:" << name << endl;
}
// 基类析构必须声明为虚函数,确保多态下派生类析构被调用
~Hero() {
cout << "基类Hero析构:" << name << endl;
}
// 纯虚函数:语音接口
virtual void sayVoice() = 0;
};
// 唯一派生类:亚瑟
class Arthur : public Hero {
public:
// 调用基类构造函数初始化名称
Arthur() : Hero("亚瑟") {
cout << "派生类Arthur构造" << endl;
}
// 派生类析构(自动成为虚函数)
~Arthur(){
cout << "派生类Arthur析构" << endl;
}
// 重写语音方法
void sayVoice() override {
cout << name << ":为了父王的荣耀!" << endl;
}
};
// 多态调用函数
void play(Hero* hero) {
hero->sayVoice();
}
int main() {
// 基类指针指向派生类对象
Hero* hero = new Arthur();
play(hero); // 调用派生类的语音实现
// 删除基类指针时,会先调用派生类析构,再调用基类析构
delete hero;
return 0;
}
结果如下:

我们发现,删除hero指针,仅仅只调用了基类的析构函数,并没有使用派生类的析构函数,这样就会导致内存的泄露。
解决的办法就是使用virtual来修饰基类的析构函数,这样编译器就会链式的的调用析构函数。
//修改点1,加上虚函数关键词
virtual ~Hero() {
cout << "基类Hero析构:" << name << endl;
}
//修改点2
virtual ~Arthur() override{ // 这里的override、virtual可加,可不加,它重写了基类的虚函数,
cout << "派生类Arthur析构" << endl; // 本身就是虚函数,派生类也可重写它的虚函数
} // 这里只是显示声明
执行结果如下:

那么这些就是一下关于多态和虚函数的一些内容了,有问题的可以在评论区交流,共同进步。

被折叠的 条评论
为什么被折叠?



