一、多态的核心概念
多态是 C++ 面向对象的三大特性(封装、继承、多态)之一,核心含义是:同一个接口,不同的实现 。简单说就是:调用同一个函数,根据调用的对象不同,执行的函数逻辑也不同。比如:定义一个动物基类,有叫()函数,猫和狗继承自动物并各自重写叫();当用动物类型的指针 / 引用指向猫或狗对象,调用叫()时,会执行对应子类的叫(),而不是基类的 ------ 这就是多态的直观体现。
二、多态的实现前提(3 个)
C++ 中运行时多态(核心) 必须满足以下 3 个条件,缺一不可:
- 存在继承关系(子类继承基类);
- 子类重写 (override)基类的虚函数 (基类函数加
virtual关键字); - 通过基类的指针 或基类的引用调用重写的虚函数(直接用对象调用不会触发多态)。
三、多态的分类
C++ 中的多态分两类,运行时多态(动态多态) 是工作中最常用的核心,编译时多态(静态多态) 是基础特性,先明确区别:
| 类型 | 实现方式 | 确定时机 | 核心特点 |
|---|---|---|---|
| 编译时多态 | 函数重载、运算符重载 | 程序编译阶段 | 编译器根据参数匹配确定执行哪个函数 |
| 运行时多态 | 虚函数 + 继承 + 重写 + 指针 / 引用 | 程序运行阶段 | 根据实际指向的对象确定执行哪个函数 |
补充:易混概念区分
- 函数重写(override) :子类重写基类的虚函数 ,要求函数名、参数列表、返回值完全一致(协变除外,新手暂不考虑),是运行时多态的核心;
- 函数重载(overload):同一作用域下,函数名相同、参数列表(个数 / 类型 / 顺序)不同,与返回值无关,是编译时多态的核心。
四、核心知识点:虚函数
虚函数是实现运行时多态的关键 ,基类中用virtual关键字修饰的函数就是虚函数,规则如下:
- 基类声明虚函数后,子类重写该函数时,可省略
virtual(编译器会自动识别为虚函数),但建议显式添加,增强代码可读性; - 虚函数可以有函数体(基类可提供默认实现);
- 析构函数建议声明为虚函数(避免子类对象析构不彻底,下文示例会讲);
- 静态成员函数、内联函数不能是虚函数(静态函数属于类,内联函数编译时展开,均无法满足运行时绑定)。
五、代码示例(从基础到进阶,可直接运行)
示例 1:基础运行时多态(核心)
实现动物基类,猫、狗子类重写叫()虚函数,通过基类指针调用,触发多态。
cpp
运行
#include <iostream>
using namespace std;
// 基类:动物
class Animal {
public:
// 虚函数:叫(加virtual,为多态做准备)
virtual void cry() {
cout << "动物发出叫声" << endl;
}
// 虚析构函数:避免子类析构不彻底(重点!)
virtual ~Animal() {
cout << "Animal析构" << endl;
}
};
// 子类:猫(继承Animal)
class Cat : public Animal {
public:
// 重写基类虚函数(可省略virtual,建议添加)
virtual void cry() override { // override关键字:显式声明重写,编译器会检查语法,避免写错
cout << "猫咪:喵喵喵~" << endl;
}
~Cat() {
cout << "Cat析构" << endl;
}
};
// 子类:狗(继承Animal)
class Dog : public Animal {
public:
virtual void cry() override {
cout << "狗狗:汪汪汪!" << endl;
}
~Dog() {
cout << "Dog析构" << endl;
}
};
// 测试函数:接收基类引用,调用cry()
void doCry(Animal &a) { // 基类引用,触发多态
a.cry();
}
int main() {
Cat cat;
Dog dog;
// 方式1:基类引用调用(推荐)
doCry(cat); // 输出:猫咪:喵喵喵~
doCry(dog); // 输出:狗狗:汪汪汪!
// 方式2:基类指针调用(常用,尤其动态分配对象时)
Animal *p1 = new Cat();
Animal *p2 = new Dog();
p1->cry(); // 输出:猫咪:喵喵喵~
p2->cry(); // 输出:狗狗:汪汪汪!
// 释放动态内存(虚析构的作用:会先析构子类,再析构基类)
delete p1;
delete p2;
return 0;
}
关键说明:
override关键字:C++11 引入,显式告诉编译器这是重写基类的虚函数,若函数名 / 参数写错,编译器会直接报错,避免手写错误;- 虚析构函数:如果基类析构函数不加
virtual,用基类指针delete子类对象时,只会调用基类析构,子类析构不会执行,造成内存泄漏 ;加virtual后,会先执行子类析构,再执行基类析构,保证析构彻底。
示例 2:编译时多态(函数重载)
最基础的多态,编译器编译时根据参数匹配函数,代码简单直观:
cpp
运行
#include <iostream>
using namespace std;
// 函数重载:同一作用域,函数名相同,参数列表不同
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
int main() {
cout << add(1,2) << endl; // 调用int版,输出3
cout << add(1.5,2.5) << endl; // 调用double版,输出4.0
cout << add(1,2,3) << endl; // 调用3个int版,输出6
return 0;
}
示例 3:纯虚函数与抽象类(工程常用)
如果基类的虚函数没有实际意义 ,不需要提供函数体,可声明为纯虚函数 ,格式:virtual 函数返回值 函数名(参数) = 0;。包含纯虚函数 的类称为抽象类,特点:
- 抽象类不能实例化对象 (不能直接
Animal a;); - 子类必须重写抽象类的所有纯虚函数,否则子类也会成为抽象类,无法实例化;
- 抽象类的核心作用:作为接口规范,约束子类必须实现指定的函数,是工程中设计基类的常用方式。
代码示例:
cpp
运行
#include <iostream>
using namespace std;
// 抽象类:包含纯虚函数
class Shape {
public:
// 纯虚函数:计算面积(基类无具体实现,由子类实现)
virtual double getArea() = 0;
// 纯虚函数:计算周长
virtual double getPeri() = 0;
virtual ~Shape() {}
};
// 子类:圆形(必须实现所有纯虚函数)
class Circle : public Shape {
private:
double r; // 半径
public:
Circle(double r) : r(r) {}
virtual double getArea() override {
return 3.14 * r * r;
}
virtual double getPeri() override {
return 2 * 3.14 * r;
}
};
// 子类:矩形
class Rect : public Shape {
private:
double w, h; // 宽、高
public:
Rect(double w, double h) : w(w), h(h) {}
virtual double getArea() override {
return w * h;
}
virtual double getPeri() override {
return 2 * (w + h);
}
};
// 测试函数:基类指针调用,触发多态
void show(Shape *s) {
cout << "面积:" << s->getArea() << endl;
cout << "周长:" << s->getPeri() << endl;
}
int main() {
// Shape s; // 错误:抽象类不能实例化
Shape *c = new Circle(2); // 圆形,半径2
Shape *r = new Rect(3,4); // 矩形,3*4
show(c); // 输出圆形的面积和周长
show(r); // 输出矩形的面积和周长
delete c;
delete r;
return 0;
}
运行结果:
plaintext
面积:12.56
周长:12.56
面积:12
周长:14
六、运行时多态的底层原理:虚函数表(vtable)
新手可以不用深入实现,但要知道核心逻辑,帮助理解多态的本质:
- 当类中有虚函数 时,编译器会为该类生成一个虚函数表(vtable) ------ 本质是一个指针数组,存储了该类所有虚函数的地址;
- 该类的每个对象,都会隐含一个虚表指针(vptr)(通常是对象的第一个成员),指向所属类的虚函数表;
- 子类继承基类时,会复制基类的虚函数表;如果子类重写了基类的虚函数,会替换虚表中对应函数的地址;
- 程序运行时,通过基类指针 / 引用调用虚函数时,会根据对象实际的虚表指针 ,找到对应类的虚函数表,执行表中存储的函数地址 ------ 这就是运行时绑定的本质。
简单说:虚表存函数地址,虚表指针指向实际的虚表,运行时通过虚表指针找真正要执行的函数。
七、多态的核心优势
- 提高代码可扩展性 :新增子类时,无需修改原有调用虚函数的代码,直接继承基类并重写虚函数即可,符合开闭原则(对扩展开放,对修改关闭);
- 提高代码可读性和维护性:通过基类指针 / 引用统一调用,代码更简洁,无需针对每个子类写单独的调用逻辑;
- 实现接口解耦:基类作为接口,调用方只需关注基类的接口,无需关注子类的具体实现,降低代码耦合度。
总结(核心关键点)
- C++ 多态分运行时(动态,核心) 和编译时(静态) ,运行时多态由虚函数 + 继承 + 重写 + 基类指针 / 引用实现;
- 虚函数是运行时多态的关键,基类析构函数建议声明为虚函数,避免子类析构不彻底;
- 纯虚函数无体,包含纯虚函数的类是抽象类,不能实例化,子类必须重写其所有纯虚函数;
- 多态的核心优势是高可扩展性、低耦合,是面向对象编程中实现 "接口统一,实现各异" 的核心手段;
- 底层通过虚函数表(vtable) 和虚表指针(vptr) 实现运行时绑定,根据对象实际类型执行对应函数。