一、多态的概念
多态是面向对象编程(OOP)的核心特性之一,核心定义为:同一行为(调用同一方法)作用于不同对象时,表现出不同的执行结果。
1.1 生活类比
以 “动物发声” 为例:
- 行为统一:所有动物都有 “发出叫声” 的行为;
- 结果不同:小狗执行 “叫” 时输出 “汪汪”,小猫执行 “叫” 时输出 “喵喵”;
- 本质:通过统一接口(“叫”),触发不同对象的专属实现。
1.2 编程场景类比
以 “买票” 为例:
- 行为统一:所有用户都有 “买票” 的行为;
- 结果不同:普通人买票全价,学生买票半价;
- 实现依赖:通过继承关系 + 虚函数,让 “买票” 接口表现出多态性。
二、多态的定义及实现
多态的实现需满足两个强制条件,核心依赖 “虚函数” 机制,以下结合代码详细说明。
2.1 多态实现的两个强制条件
- 通过基类的指针或引用调用目标函数;
- 被调用的函数是虚函数,且派生类重写了该虚函数。
2.2 核心组件:虚函数(Virtual Function)
2.2.1 虚函数的定义
被virtual
关键字修饰的类成员函数,称为虚函数。其作用是允许派生类重写该函数,为多态提供基础。
cpp
运行
// 基类:Person(普通人)
class Person {
public:
// 定义虚函数:买票(全价)
virtual void BuyTicket() {
cout << "买票-全价" << endl;
}
};
2.2.2 虚函数的重写(覆盖)
派生类中定义与基类完全一致的虚函数(返回值类型、函数名、参数列表均相同),称为 “重写”(语法层面),底层本质是 “覆盖”(虚表中函数地址的替换)。
cpp
运行
// 派生类:Student(学生,继承自Person)
class Student : public Person {
public:
// 重写基类的虚函数BuyTicket(半价)
// 注:即使不加virtual,因继承基类虚函数的“虚属性”,仍构成重写;
// 但规范要求显式加virtual,提升代码可读性
virtual void BuyTicket() {
cout << "买票-半价" << endl;
}
};
2.2.3 多态调用的完整代码示例
cpp
运行
// 统一接口:通过基类引用调用虚函数
void Func(Person& people) {
// 关键:调用时不依赖“引用的声明类型(Person)”,而依赖“实际指向的对象类型”
people.BuyTicket();
}
int main() {
Person Mike; // 基类对象(普通人)
Student Johnson;// 派生类对象(学生)
Func(Mike); // 实际对象:Person → 执行Person::BuyTicket → 输出“买票-全价”
Func(Johnson); // 实际对象:Student → 执行Student::BuyTicket → 输出“买票-半价”
return 0;
}
2.3 多态与非多态的调用区别
调用场景 | 绑定方式 | 执行逻辑 | 示例结果 |
---|---|---|---|
满足多态条件(基类指针 / 引用 + 虚函数重写) | 动态绑定(运行时) | 看 “实际指向的对象类型”,执行对应虚函数 | Base* p = new Derive; p->Func () → 执行 Derive::Func |
不满足多态条件(基类对象直接调用) | 静态绑定(编译时) | 看 “变量的声明类型”,执行对应函数 | Base b = Derive (); b.Func () → 执行 Base::Func |
三、虚函数重写的特殊例外
虚函数重写的默认规则是 “返回值、函数名、参数列表完全一致”,但以下两种场景为合法例外,需单独掌握。
3.1 例外 1:协变(返回值类型不同)
3.1.1 定义
基类虚函数返回基类对象的指针 / 引用,派生类虚函数返回派生类对象的指针 / 引用(派生类与基类需有继承关系),仍构成重写,称为 “协变”。
3.1.2 代码示例
cpp
运行
// 辅助类:A(基类)、B(派生类)
class A {};
class B : public A {}; // B是A的子类
// 基类:Person
class Person {
public:
// 虚函数返回A*(基类指针)
virtual A* f() {
return new A;
}
};
// 派生类:Student
class Student : public Person {
public:
// 重写虚函数,返回B*(派生类指针)→ 符合协变规则,构成重写
virtual B* f() {
return new B;
}
};
3.2 例外 2:析构函数的重写(函数名不同)
3.2.1 核心原因
编译器会将所有类的析构函数名称统一处理为destructor
,因此即使基类与派生类析构函数的表面名称不同(如~Person()
和~Student()
),仍可构成重写。
3.2.2 关键问题:基类析构函数未设为虚函数的风险
若基类析构函数非虚函数,通过 “基类指针删除派生类对象” 时,仅会调用基类析构函数,导致派生类的资源(如堆内存)无法释放,引发资源泄漏。
cpp
运行
// 错误示例:基类析构非虚函数
class Person {
public:
// 非虚析构:风险点
~Person() {
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
~Student() {
cout << "~Student()" << endl;
}
};
int main() {
Person* p2 = new Student; // 基类指针指向派生类对象
delete p2; // 仅调用~Person(),未调用~Student() → 派生类资源泄漏
return 0;
}
3.2.3 修正方案:基类析构函数设为虚函数
cpp
运行
// 正确示例:基类析构设为虚函数
class Person {
public:
// 虚析构:确保多态调用
virtual ~Person() {
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
// 派生类析构:即使不加virtual,仍构成重写
~Student() {
cout << "~Student()" << endl;
}
};
int main() {
Person* p2 = new Student;
delete p2; // 先调用~Student(),再调用~Person() → 资源完全释放
return 0;
}
3.3 补充:C++11 重写校验关键字(override/final)
虚函数重写规则严格,若手动写错(如函数名、参数),编译器可能不报错,仅在运行时出问题。C++11 新增override
和final
,实现编译期校验,提前规避错误。
3.3.1 override:强制校验重写合法性
加在派生类虚函数后,明确声明 “该函数需重写基类虚函数”。若不满足重写规则(如基类无对应虚函数、参数不匹配),编译器直接报错。
cpp
运行
class Person {
public:
virtual void BuyTicket(int n = 1) { cout << "全价票x" << n << endl; }
};
class Student : public Person {
public:
// 校验:若基类BuyTicket参数不是int,或非虚函数,编译器报错
virtual void BuyTicket(int n = 1) override {
cout << "半价票x" << n << endl;
}
};
3.3.2 final:限制重写或继承
- 作用于基类虚函数:禁止派生类重写该函数;
- 作用于类名:禁止该类被继承。
cpp
运行
// 场景1:限制虚函数重写
class Person {
public:
// final:Person的派生类无法重写BuyTicket
virtual void BuyTicket() final {
cout << "全价" << endl;
}
};
class Student : public Person {
public:
// 错误:编译器报错,无法重写final修饰的虚函数
virtual void BuyTicket() {
cout << "半价" << endl;
}
};
// 场景2:限制类继承
class Person final { // final:禁止任何类继承Person
public:
virtual void BuyTicket() { cout << "全价" << endl; }
};
// 错误:编译器报错,无法继承final类
class Student : public Person {
};
四、易混淆概念:重载、重写、重定义
三者均涉及 “同名函数”,但作用域、规则、效果完全不同,需精准区分。
特性 | 重载(Overload) | 重写(Override) | 重定义(Redefine) |
---|---|---|---|
作用域 | 同一作用域(如同一类内) | 不同作用域(基类 + 派生类) | 不同作用域(基类 + 派生类) |
函数名 | 必须相同 | 必须相同 | 必须相同 |
参数列表 | 必须不同(个数 / 类型 / 顺序) | 必须相同 | 可相同、可不同 |
返回值类型 | 可相同、可不同 | 必须相同(协变例外) | 可相同、可不同 |
虚函数要求 | 无(与虚函数无关) | 必须均为虚函数(基类需加 virtual) | 无(若基类是虚函数但参数不同,仍算重定义) |
核心效果 | 编译期根据参数区分调用 | 运行期根据对象类型区分调用(多态) | 派生类函数 “隐藏” 基类同名函数,直接调用时优先用派生类 |
4.1 示例对比
4.1.1 重载(同一类内)
cpp
运行
class Math {
public:
// 重载:同一类内,函数名相同,参数不同
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
};
4.1.2 重写(基类 + 派生类,虚函数 + 参数相同)
cpp
运行
class Person {
public:
virtual void BuyTicket() { cout << "全价" << endl; } // 基类虚函数
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "半价" << endl; } // 重写:参数/返回值相同,均为虚函数
};
4.1.3 重定义(基类 + 派生类,不满足重写)
cpp
运行
class Person {
public:
virtual void func(int a) { cout << "Person::func(" << a << ")" << endl; }
};
class Student : public Person {
public:
// 重定义:参数不同(int→double),即使基类是虚函数,也不构成重写
virtual void func(double a) { cout << "Student::func(" << a << ")" << endl; }
};
int main() {
Student s;
s.func(10); // 调用Student::func(double),基类func(int)被隐藏 → 输出“Student::func(10.0)”
return 0;
}
五、抽象类(接口类)
抽象类是包含 “纯虚函数” 的类,核心作用是定义接口规范,强制派生类实现特定行为,无法直接实例化。
5.1 核心组件:纯虚函数
纯虚函数是在虚函数后加=0
的函数,仅声明接口,不提供实现(也可提供实现,但派生类仍需重写才能实例化)。
cpp
运行
class Animal {
public:
// 纯虚函数:声明“发声”接口,无具体实现
virtual void MakeSound() = 0;
};
5.2 抽象类的特性
- 无法直接实例化:抽象类不能创建对象,如
Animal a;
会编译报错; - 派生类需重写所有纯虚函数:若派生类未重写抽象类的全部纯虚函数,则该派生类仍为抽象类,无法实例化;
- 可定义抽象类的指针 / 引用:用于实现多态调用(指向派生类对象)。
5.3 代码示例
cpp
运行
// 抽象类:Animal(含纯虚函数MakeSound)
class Animal {
public:
virtual void MakeSound() = 0; // 纯虚函数
};
// 派生类1:Dog(重写纯虚函数)
class Dog : public Animal {
public:
void MakeSound() override { // 重写纯虚函数
cout << "汪汪汪" << endl;
}
};
// 派生类2:Cat(重写纯虚函数)
class Cat : public Animal {
public:
void MakeSound() override { // 重写纯虚函数
cout << "喵喵喵" << endl;
}
};
// 派生类3:Bird(未重写纯虚函数 → 仍为抽象类)
class Bird : public Animal {
// 未实现MakeSound() → Bird是抽象类,无法实例化
};
int main() {
// Animal a; // 错误:抽象类无法实例化
// Bird b; // 错误:Bird未重写纯虚函数,仍是抽象类
Animal* p1 = new Dog; // 抽象类指针指向派生类对象
Animal* p2 = new Cat; // 多态调用前提
p1->MakeSound(); // 输出“汪汪汪”
p2->MakeSound(); // 输出“喵喵喵”
delete p1;
delete p2;
return 0;
}
5.4 补充:接口继承 vs 实现继承
多态依赖 “接口继承”,需与普通函数的 “实现继承” 区分:
- 接口继承:派生类继承基类虚函数的 “接口规范”(函数名、参数、返回值),需自行实现逻辑(如 Animal 的 MakeSound);核心目的是实现多态。
- 实现继承:派生类直接继承基类普通函数的 “完整实现”,无需重写即可使用(如 Person 的 Eat 函数);核心目的是代码复用。
六、多态的底层实现原理(虚表与虚表指针)
多态的底层依赖 “虚表(Virtual Function Table)” 和 “虚表指针(Virtual Table Pointer, _vftptr)” 机制,以下从内存结构、生成逻辑、调用流程三方面解析。
6.1 核心概念定义
组件 | 本质 | 存在位置 | 核心作用 |
---|---|---|---|
虚表指针(_vftptr) | 指向虚表的指针(4 字节 / 32 位,8 字节 / 64 位) | 类对象的内存头部(隐藏成员) | 关联对象与对应的虚表 |
虚表(vftable) | 存储虚函数地址的指针数组(结尾含 nullptr) | 代码段的常量区(编译时确定) | 记录类的所有虚函数地址,供调用查询 |
虚函数 | 可重写的成员函数(二进制指令) | 代码段(与普通函数存储位置一致) | 实际执行的业务逻辑 |
6.2 虚表与虚表指针的生成规则
6.2.1 虚表指针的生成
- 仅当类中包含虚函数(或继承自含虚函数的类)时,该类的对象才会自动添加
_vftptr
(隐藏成员,不占用类的显式成员内存); - 同一类的所有对象共享同一张虚表,因此每个对象仅需一个
_vftptr
指向该虚表(节省内存)。
6.2.2 派生类虚表的生成步骤(以单继承为例)
- 拷贝基类虚表:派生类先将基类的虚表完整拷贝到自己的虚表中;
- 替换重写的虚函数:若派生类重写了基类的某虚函数,用派生类虚函数的地址替换虚表中对应基类虚函数的地址;
- 追加新增虚函数:若派生类有自己新增的虚函数,按函数声明顺序,将其地址追加到虚表末尾(nullptr 前)。
6.2.3 示例:单继承下的虚表结构
cpp
运行
// 基类:Base(含2个虚函数)
class Base {
public:
virtual void Func1() { cout << "Base::Func1" << endl; }
virtual void Func2() { cout << "Base::Func2" << endl; }
private:
int b; // 显式成员(4字节/32位)
};
// 派生类:Derive(重写Func1,新增Func3)
class Derive : public Base {
public:
virtual void Func1() override { cout << "Derive::Func1" << endl; } // 重写Func1
virtual void Func3() { cout << "Derive::Func3" << endl; } // 新增虚函数
private:
int d; // 新增显式成员(4字节/32位)
};
虚表结构对比:
- Base 的虚表:
[&Base::Func1, &Base::Func2, nullptr]
; - Derive 的虚表:
[&Derive::Func1(替换), &Base::Func2(保留), &Derive::Func3(追加), nullptr]
。
6.3 对象的内存布局(32 位系统)
以Derive d;
为例,其内存结构(共 12 字节)如下:
内存偏移 | 内容 | 大小(字节) | 说明 |
---|---|---|---|
0~3 | _vftptr(虚表指针) | 4 | 指向 Derive 的虚表 |
4~7 | 继承的 Base::b | 4 | 从基类继承的显式成员 |
8~11 | 自身的 Derive::d | 4 | 派生类新增的显式成员 |
6.4 多态调用的底层流程(以Base* p = new Derive; p->Func1();
为例)
- 获取虚表指针:通过指针
p
访问派生类对象Derive
的内存头部,拿到_vftptr
; - 查找虚表:通过
_vftptr
指向代码段常量区中的Derive
虚表; - 定位虚函数地址:在虚表中找到
Func1
对应的地址(已被替换为&Derive::Func1
); - 执行函数:跳转到该地址,执行
Derive::Func1
的二进制指令。
6.5 补充:关键细节澄清
- 虚表不存储在对象中:虚表在代码段常量区,对象仅通过
_vftptr
关联虚表,避免内存浪费; - 静态函数无法成为虚函数:静态函数无
this
指针,无法通过this->_vftptr
访问虚表,因此不能定义为虚函数; - 虚表指针的位置:随对象存储(对象在栈上,
_vftptr
在栈上;对象在堆上,_vftptr
在堆上)。
七、单继承与多继承的虚函数表差异
单继承和多继承下,虚表的数量、结构及子类虚函数的存储位置均不同,需分别分析。
7.1 单继承的虚函数表
7.1.1 核心特性
- 虚表数量:1 张(派生类仅需在基类虚表基础上修改 / 追加);
- 虚表指针:派生类对象仅含1 个
_vftptr
(继承自基类,指向派生类的虚表); - 共享特性:同一类的所有对象共享同一张虚表(如
Derive d1, d2;
的_vftptr
指向同一张虚表)。
7.1.2 内存布局示例(32 位系统)
cpp
运行
class Base {
public:
virtual void Func1() { cout << "Base::Func1" << endl; }
private:
int b = 1; // 4字节
};
class Derive : public Base {
public:
virtual void Func1() override { cout << "Derive::Func1" << endl; }
virtual void Func2() { cout << "Derive::Func2" << endl; }
private:
int d = 2; // 4字节
};
Derive d;
的内存布局(共 12 字节):
偏移 | 内容 | 大小 | 说明 |
---|---|---|---|
0~3 | _vftptr(指向 Derive 虚表) | 4 | 唯一虚表指针 |
4~7 | Base::b(值 = 1) | 4 | 继承自基类的成员 |
8~11 | Derive::d(值 = 2) | 4 | 派生类新增成员 |
7.2 多继承的虚函数表
多继承指派生类同时继承多个基类(均含虚函数),此时虚表数量与基类数量一致,虚函数存储位置需区分 “重写” 和 “新增”。
7.2.1 核心特性
- 虚表数量:与含虚函数的基类数量一致(每个基类对应 1 张虚表);
- 虚表指针:派生类对象含多个
_vftptr
(每个基类对应 1 个,按继承顺序排列); - 虚函数存储规则:
- 子类重写的虚函数:存储在对应基类的虚表中(如重写
Base1::Func1
,则替换Base1
虚表中的Func1
地址); - 子类新增的虚函数:默认存储在第一个基类的虚表末尾(按继承顺序,如先继承
Base1
,则新增函数追加到Base1
虚表)。
- 子类重写的虚函数:存储在对应基类的虚表中(如重写
7.2.2 代码示例与内存分析(32 位系统)
cpp
运行
// 基类1:Base1(含虚函数)
class Base1 {
public:
virtual void Func1() { cout << "Base1::Func1" << endl; }
virtual void Func2() { cout << "Base1::Func2" << endl; }
private:
int b1 = 1; // 4字节
};
// 基类2:Base2(含虚函数)
class Base2 {
public:
virtual void Func1() { cout << "Base2::Func1" << endl; }
virtual void Func2() { cout << "Base2::Func2" << endl; }
private:
int b2 = 2; // 4字节
};
// 派生类:Derive(多继承Base1、Base2,重写Func1,新增Func3)
class Derive : public Base1, public Base2 {
public:
// 重写Base1::Func1和Base2::Func1
virtual void Func1() override { cout << "Derive::Func1" << endl; }
// 新增虚函数Func3
virtual void Func3() { cout << "Derive::Func3" << endl; }
private:
int d1 = 3; // 4字节
};
7.2.3 虚表结构
- Base1 对应的虚表:
[&Derive::Func1(重写), &Base1::Func2(保留), &Derive::Func3(新增), nullptr]
; - Base2 对应的虚表:
[&Derive::Func1(重写), &Base2::Func2(保留), nullptr]
。
7.2.4 内存布局(共 20 字节)
Derive d;
的内存结构(32 位系统,按继承顺序排列):
偏移 | 内容 | 大小 | 说明 |
---|---|---|---|
0~3 | _vftptr1(指向 Base1 的虚表) | 4 | 对应 Base1 的虚表指针 |
4~7 | Base1::b1(值 = 1) | 4 | Base1 的成员 |
8~11 | _vftptr2(指向 Base2 的虚表) | 4 | 对应 Base2 的虚表指针 |
12~15 | Base2::b2(值 = 2) | 4 | Base2 的成员 |
16~19 | Derive::d1(值 = 3) | 4 | Derive 的新增成员 |
7.3 补充:多继承的内存对齐
多继承下,对象内存需满足 “内存对齐” 规则(通常与指针大小一致,32 位 4 字节,64 位 8 字节)。上述示例中,各部分均为 4 字节,无需额外补位,总大小为 20 字节;若存在非对齐成员(如 char),会自动补位至对齐大小。
八、静态绑定与动态绑定(多态的绑定机制)
多态的本质是 “动态绑定”,需与 “静态绑定” 对比,明确两者的适用场景。
8.1 静态绑定(前期绑定 / 早绑定)
- 定义:编译期间确定函数的调用关系,不依赖运行时对象类型;
- 适用场景:函数重载、普通函数调用、非虚函数的继承调用;
- 核心特点:效率高,编译时已确定调用地址;
- 示例:
cpp
运行
void f(int a) { cout << "int: " << a << endl; } void f(double a) { cout << "double: " << a << endl; } int main() { int i = 10; double d = 5.5; f(i); // 编译时确定调用f(int) → 静态绑定 f(d); // 编译时确定调用f(double) → 静态绑定 return 0; }
8.2 动态绑定(后期绑定 / 晚绑定)
- 定义:运行期间根据实际对象类型确定函数的调用关系,是多态的核心;
- 适用场景:通过基类指针 / 引用调用重写的虚函数;
- 核心特点:灵活性高,支持多态,但需通过虚表查询地址,效率略低;
- 示例:
cpp
运行
class Base { public: virtual void Func() { cout << "Base::Func" << endl; } }; class Derive : public Base { public: virtual void Func() override { cout << "Derive::Func" << endl; } }; int main() { Base* p = nullptr; // 运行时根据对象类型确定调用 p = new Base; p->Func(); // 指向Base对象 → 调用Base::Func p = new Derive;p->Func(); // 指向Derive对象 → 调用Derive::Func delete p; return 0; }
九、补充:常见疏漏点与易错点
-
纯虚函数可提供实现:纯虚函数的
=0
仅表示 “强制派生类重写”,不禁止基类提供实现,但派生类仍需重写后才能实例化;cpp
运行
class Animal { public: virtual void MakeSound() = 0; // 纯虚函数 }; // 基类提供纯虚函数的实现(合法) void Animal::MakeSound() { cout << "动物发声" << endl; } class Dog : public Animal { public: void MakeSound() override { Animal::MakeSound(); // 可调用基类的实现 cout << "汪汪汪" << endl; } };
-
虚函数的默认参数是静态绑定:虚函数的默认参数由 “指针 / 引用的声明类型” 决定(编译时绑定),而非实际对象类型;
cpp
运行
class Base { public: virtual void Func(int a = 1) { cout << "Base: " << a << endl; } }; class Derive : public Base { public: virtual void Func(int a = 2) override { cout << "Derive: " << a << endl; } }; int main() { Base* p = new Derive; p->Func(); // 默认参数用Base的a=1,函数体用Derive的实现 → 输出“Derive: 1” return 0; }
-
析构函数必须设为虚函数的场景:仅当 “需通过基类指针删除派生类对象” 时,基类析构才需设为虚函数;若仅用派生类对象直接释放,无需虚析构。