【C++】多态与虚函数
目录
- 🚀前言
- 🤔多态的定义和分类
- 💯静态多态(编译期多态)
- 💯动态多态(运行期多态)
- 🌟动态多态的实现条件
- 🦜虚函数实现多态的机制
- 💯虚函数表是什么?
- 💯虚函数表的生成与继承规则
- 💯动态绑定的过程(多态的实际执行逻辑)
- 💯关于虚函数表的关键细节
- 🎋纯虚函数与抽象类
- 💯纯虚函数的语法
- 💯抽象类的特点
- 💯代码示例
- ☘️虚析构函数
- 💯虚析构的写法
- 🐍override和final
- 💯override
- 💯final
- ⚙️总结
🚀前言

大家好!我是 EnigmaCoder。
- 本文介绍C++中的多态,从静态/动态多态、虚函数表、纯虚函数/抽象类到虚析构函数。
🤔多态的定义和分类
多态(Polymorphism)是面向对象三大特性之一,核心是“一个接口,多种形态”,比如同样是“发出叫声”,猫是“喵喵喵”,狗是“汪汪汪”。多态的核心魅力在于让同一行为在不同对象上有不同实现,既提升了代码复用性,又增强了扩展性。
多态可以分为两类:
💯静态多态(编译期多态)
- 实现方式:函数重载、运算符重载
- 特点:编译阶段就确定函数的调用地址(静态绑定)
- 注意:如果是多继承中出现同名成员,需要用
作用域::区分(比如Father1::func())
💯动态多态(运行期多态)
- 实现方式:虚函数(
virtual)+ 继承 + 重写 - 特点:运行阶段才确定函数的调用地址(动态绑定)
- 这是C++多态的核心,其底层依赖「虚函数表」实现
🌟动态多态的实现条件
想要实现动态多态,必须同时满足3个条件:
- 有继承关系:子类继承父类
- 子类重写父类的虚函数:重写要求:函数名、返回值类型、参数列表完全一致
- 父类指针/引用指向子类对象:通过父类的指针或引用调用重写的函数
🦜虚函数实现多态的机制
动态多态能“运行时选函数”,全靠**虚函数表(Virtual Table,简称vtable)和虚指针(Virtual Pointer,简称vptr)**的配合,这是C++多态的底层核心机制。
💯虚函数表是什么?
- 本质:一个全局只读的函数地址数组,属于类(而非对象),每个包含虚函数的类都会生成唯一的虚函数表。
- 内容:存储该类所有虚函数的地址,顺序与虚函数在类中的声明顺序一致。
- 虚指针(vptr):每个包含虚函数的类的对象,都会隐含一个
vptr成员(位于对象内存的最前端),用来指向所属类的虚函数表。
💯虚函数表的生成与继承规则
当类之间存在继承+虚函数重写时,虚函数表会发生如下变化:
- 父类有虚函数:父类会生成自己的虚函数表,比如
Animal类的虚函数表中存Animal::speak()的地址。 - 子类继承父类:
- 若子类未重写父类虚函数:子类的虚函数表会直接继承父类的虚函数地址。
- 若子类重写了父类虚函数:子类的虚函数表中,对应位置的函数地址会被替换为子类重写后的函数地址(比如
Cat类的虚函数表中,speak()的地址会变成Cat::speak())。
💯动态绑定的过程(多态的实际执行逻辑)
以“动物叫”为例,当执行Animal* p = new Cat(); p->speak();时:
- 程序通过
p(父类指针)找到所指对象的vptr(此时vptr是Cat对象的,指向Cat类的虚函数表)。 - 从
Cat的虚函数表中,找到speak()对应的地址(即Cat::speak())。 - 调用该地址对应的函数,最终输出“小猫喵喵喵”。
举个直观的例子:虚函数表的变化
// 父类:Animal(含虚函数)
class Animal {
public:virtual void speak() { cout << "动物叫" << endl; }virtual void eat() { cout << "动物进食" << endl; }
};// 子类:Cat(重写speak,继承eat)
class Cat : public Animal {
public:void speak() override { cout << "小猫喵喵喵" << endl; }
};
-
Animal类的虚函数表:
位置 函数地址 0 Animal::speak() 1 Animal::eat() -
Cat类的虚函数表:
位置 函数地址 0 Cat::speak() 1 Animal::eat()
💯关于虚函数表的关键细节
- 每个类的vtable全局唯一:不管创建多少个对象,同一个类的所有对象共享同一个虚函数表。
- vptr在构造函数中初始化:对象创建时,构造函数会把
vptr指向当前类的虚函数表。 - 对象的内存开销:包含虚函数的对象会多一个
vptr的大小(32位系统占4字节,64位占8字节)。
🎋纯虚函数与抽象类
如果父类的虚函数不需要具体实现(只是给子类留接口),可以声明为纯虚函数,包含纯虚函数的类称为抽象类。
💯纯虚函数的语法
virtual 返回值类型 函数名(参数列表) = 0;
比如动物类的speak()可以写成纯虚函数:
class Animal {
public:// 纯虚函数:没有函数体,强制子类实现virtual void speak() = 0;
};
💯抽象类的特点
- 不能实例化对象(比如
Animal a;会报错) - 子类必须全部实现父类的纯虚函数,否则子类也会变成抽象类
- 可以定义抽象类的指针/引用,用来指向子类对象(这也是多态的常用方式)
💯代码示例
#include <iostream>
using namespace std;// 抽象类:Animal(包含纯虚函数,无法实例化)
class Animal {
public:// 纯虚函数1:动物叫声(仅定义接口,无实现)virtual void speak() = 0;// 纯虚函数2:动物进食(仅定义接口,无实现)virtual void eat() = 0;// 虚析构:确保子类析构被调用(避免内存泄漏)virtual ~Animal() {}
};// 子类1:Cat(必须实现Animal的所有纯虚函数,否则是抽象类)
class Cat : public Animal {
public:// 实现纯虚函数speak:猫的叫声void speak() override {cout << "小猫:喵喵喵~" << endl;}// 实现纯虚函数eat:猫的进食行为void eat() override {cout << "小猫:爱吃小鱼干~" << endl;}// 子类析构(会被虚析构触发)~Cat() override {cout << "Cat对象被销毁" << endl;}
};// 子类2:Dog(同样实现所有纯虚函数)
class Dog : public Animal {
public:void speak() override {cout << "小狗:汪汪汪~" << endl;}void eat() override {cout << "小狗:爱吃肉骨头~" << endl;}~Dog() override {cout << "Dog对象被销毁" << endl;}
};// 错误示例:Bird类(仅实现了一个纯虚函数,因此是抽象类,无法实例化)
class Bird : public Animal {
public:void speak() override {cout << "小鸟:叽叽叽~" << endl;}// 未实现eat(),因此Bird是抽象类
};int main() {// 1. 抽象类不能实例化(以下代码会报错)// Animal animal; // 编译错误:Animal是抽象类// 2. 用抽象类指针指向子类对象(实现多态)Animal* animal1 = new Cat();Animal* animal2 = new Dog();// 3. 调用函数:运行时绑定到子类实现animal1->speak(); // 输出:小猫:喵喵喵~animal1->eat(); // 输出:小猫:爱吃小鱼干~animal2->speak(); // 输出:小狗:汪汪汪~animal2->eat(); // 输出:小狗:爱吃肉骨头~// 4. 释放资源:虚析构会触发子类析构delete animal1; // 输出:Cat对象被销毁delete animal2; // 输出:Dog对象被销毁// 5. 抽象类Bird无法实例化(以下代码会报错)// Bird bird; // 编译错误:Bird是抽象类return 0;
}
☘️虚析构函数
当用父类指针指向子类对象时,如果父类的析构函数不是虚函数,delete父类指针时只会调用父类的析构函数,不会调用子类的析构函数,导致子类的资源无法释放(内存泄漏)。
💯虚析构的写法
class Animal {
public:virtual ~Animal() { // 虚析构cout << "Animal析构" << endl;}
};class Cat : public Animal {
public:~Cat() override {cout << "Cat析构" << endl;}
};// 调用时:
Animal* p = new Cat();
delete p; // 会先调用Cat析构,再调用Animal析构(通过虚函数表实现)
🐍override和final
C++11提供了override和final两个关键字,用来增强多态的安全性。
💯override
在子类重写的函数后加override,编译器会检查是否真的重写了父类的虚函数(比如函数名/参数写错了会报错):
class Animal {
public:virtual void speak(int a) {}
};class Cat : public Animal {
public:// 错误:父类speak是带int参数的,这里没参数,override会触发编译错误void speak() override {}
};
💯final
- 修饰类:该类不能被继承(比如
class Animal final {};) - 修饰虚函数:该函数不能被子类重写(比如
virtual void speak() final {})
⚙️总结
多态是C++面向对象的灵魂,其底层逻辑可以概括为:
- 静态多态:编译期绑定,依赖函数重载
- 动态多态:运行期绑定,依赖「虚函数表+虚指针」,核心是“子类虚函数表替换重写函数地址”
- 抽象类+纯虚函数:定义接口规范,强制子类实现
- 虚析构:通过虚函数表保证子类析构被调用,避免内存泄漏
