【C++进阶】深入理解C++多态:从概念到底层实现全面解析
1. 多态的概念
1.1 什么是多态?
多态(Polymorphism)是面向对象编程的三大特性之一(另外两个是封装和继承)。通俗来说,多态就是多种形态,具体来说就是完成某个行为时,不同的对象去完成会产生不同的状态。
1.2 生活中的多态例子
例子1:买票行为
- 普通人买票:全价
- 学生买票:半价
- 军人买票:优先买票
例子2:支付宝扫码红包
- 新用户:红包金额较大(8块、10块)
- 老用户:红包金额较小(1毛、5毛)
同样是扫码动作,不同的用户得到不同的结果,这就是多态行为。
2. 多态的定义及实现
2.1 多态的构成条件
要构成多态,必须满足两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
class Person {
public:
virtual void BuyTicket() {
cout << "买票全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() {
cout << "买票半价" << endl;
}
};
void Func(Person& people) {
people.BuyTicket(); // 多态调用
}
void Test() {
Person Mike;
Func(Mike); // 输出:买票全价
Student Johnson;
Func(Johnson); // 输出:买票半价
}
2.2 虚函数
虚函数:被virtual修饰的类成员函数称为虚函数。
class Person {
public:
virtual void BuyTicket() {
cout << "买票-全价" << endl;
}
};
2.3 虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:
virtual void BuyTicket() {
cout << "买票-全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() {
cout << "买票-半价" << endl;
}
// 或者省略virtual(但不建议)
// void BuyTicket() { cout << "买票-半价" << endl; }
};
2.4 虚函数重写的两个例外
2.4.1 协变(Covariant)
基类与派生类虚函数返回值类型不同,但必须是父子类关系的指针或引用。
class A {};
class B : public A {};
class Person {
public:
virtual A* f() {
return new A;
}
};
class Student : public Person {
public:
virtual B* f() {
return new B;
}
};
2.4.2 析构函数的重写
如果基类的析构函数为虚函数,派生类析构函数与基类析构函数构成重写。
class Person {
public:
virtual ~Person() {
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
virtual ~Student() {
cout << "~Student()" << endl;
}
};
int main() {
Person* p1 = new Person;
Person* p2 = new Student;
delete p1; // 输出:~Person()
delete p2; // 输出:~Student() -> ~Person()
return 0;
}
重要:只有将基类析构函数声明为虚函数,才能通过基类指针正确调用派生类的析构函数。
2.5 C++11 override 和 final
2.5.1 final
修饰虚函数,表示该虚函数不能再被重写。
class Car {
public:
virtual void Drive() final {}
};
class Benz : public Car {
public:
virtual void Drive() { // 编译错误:不能重写final函数
cout << "Benz-舒适" << endl;
}
};
2.5.2 override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则编译报错。
class Car {
public:
virtual void Drive() {}
};
class Benz : public Car {
public:
virtual void Drive() override { // 正确:重写了基类虚函数
cout << "Benz-舒适" << endl;
}
};
2.6 重载、覆盖(重写)、隐藏(重定义)的对比
| 特性 | 重载(Overload) | 覆盖(Override) | 隐藏(Hiding) |
|---|---|---|---|
| 作用域 | 同一作用域 | 基类和派生类 | 基类和派生类 |
| 函数名 | 相同 | 相同 | 相同 |
| 参数列表 | 必须不同 | 必须相同 | 可以不同 |
| 返回值 | 可以不同 | 相同(协变例外) | 可以不同 |
| 虚函数 | 无关 | 必须是虚函数 | 无关 |
3. 抽象类
3.1 概念
在虚函数的后面写上 = 0,这个函数就成为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)。
class Car {
public:
virtual void Drive() = 0; // 纯虚函数
};
class Benz : public Car {
public:
virtual void Drive() {
cout << "Benz-舒适" << endl;
}
};
class BMW : public Car {
public:
virtual void Drive() {
cout << "BMW-操控" << endl;
}
};
void Test() {
// Car car; // 错误:抽象类不能实例化对象
Car* pBenz = new Benz;
pBenz->Drive(); // 输出:Benz-舒适
Car* pBMW = new BMW;
pBMW->Drive(); // 输出:BMW-操控
}
抽象类特点:
- 不能实例化对象
- 派生类必须重写纯虚函数才能实例化对象
- 体现了接口继承的概念
3.2 接口继承和实现继承
- 普通函数继承:实现继承,派生类继承函数的实现
- 虚函数继承:接口继承,派生类继承函数接口以便重写
建议:如果不实现多态,不要把函数定义成虚函数。
4. 多态的原理
4.1 虚函数表(虚表)
先看一个关键问题:sizeof(Base) 是多少?
class Base {
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main() {
Base b;
cout << sizeof(b) << endl; // 输出:8(32位系统)
return 0;
}
为什么是8字节?因为对象中除了_b成员,还有一个虚函数表指针(_vfptr)。
4.2 虚函数表详解
class Base {
public:
virtual void Func1() {
cout << "Base::Func1()" << endl;
}
virtual void Func2() {
cout << "Base::Func2()" << endl;
}
void Func3() {
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base {
public:
virtual void Func1() { // 重写基类Func1
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main() {
Base b;
Derive d;
return 0;
}
虚表分析:
| 对象 | 虚表内容 |
|---|---|
| Base对象 | Base::Func1, Base::Func2 |
| Derive对象 | Derive::Func1, Base::Func2 |
重要结论:
- 含有虚函数的类都有虚表指针
- 派生类虚表生成规则:
- 拷贝基类虚表内容
- 替换重写的虚函数
- 添加新增的虚函数
4.3 多态的实现原理
void Func(Person* p) {
p->BuyTicket(); // 多态调用
}
对应的汇编代码:
void Func(Person* p) {
p->BuyTicket();
// 将p移动到eax中
001940DE mov eax, dword ptr [p]
// 取虚表指针到edx
001940E1 mov edx, dword ptr [eax]
// 取虚函数地址到eax
008823EE mov eax, dword ptr [edx]
// 调用虚函数
001940EA call eax
}
多态调用过程:
- 通过对象找到虚表指针
- 通过虚表指针找到虚表
- 在虚表中找到对应的虚函数地址
- 调用虚函数
4.4 静态绑定 vs 动态绑定
- 静态绑定(早绑定):编译期间确定函数地址(如:函数重载、普通函数调用)
- 动态绑定(晚绑定):运行期间确定函数地址(多态调用)
5. 单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive : public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
虚表打印代码:
typedef void(*VFPTR)(); // 虚函数指针类型
void PrintVTable(VFPTR vTable[]) {
cout << "虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i) {
printf("第%d个虚函数地址:0x%x -> ", i, vTable[i]);
VFPTR f = vTable[i];
f(); // 调用验证
}
cout << endl;
}
int main() {
Base b;
Derive d;
// 取Base对象的虚表
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
// 取Derive对象的虚表
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
输出结果:
虚表地址>0x2009F564
第0个虚函数地址:0x3100a -> Base::func1
第1个虚函数地址:0x31108 -> Base::func2
虚表地址>0x2009F594
第0个虚函数地址:0x31104 -> Derive::func1
第1个虚函数地址:0x31108 -> Base::func2
第2个虚函数地址:0x31104 -> Derive::func3
第3个虚函数地址:0x3100a -> Derive::func4
5.2 多继承中的虚函数表
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; } // 重写两个基类的func1
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
多继承虚表特点:
- 派生类有多个虚表(每个直接基类一个)
- 重写的虚函数在每个虚表中都被替换
- 新增的虚函数放在第一个基类的虚表中
5.3 菱形继承和菱形虚拟继承
不建议使用菱形继承和菱形虚拟继承,因为:
- 模型复杂容易出错
- 访问基类成员有性能损耗
- 虚表结构更加复杂
6. 继承和多态常见面试问题
6.1 概念选择题
-
下面哪种面向对象的方法可以让你变得富有()
A: 继承 ✓ B: 封装 C: 多态 D: 抽象 -
()是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定 ✓ -
关于虚函数的描述正确的是()
A: 派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B: 内联函数不能是虚函数 ✓
C: 派生类必须重新定义基类的虚函数
D: 虚函数可以是一个static型的函数
6.2 问答题
1. inline函数可以是虚函数吗?
答:可以,不过编译器会忽略inline属性,因为虚函数要放到虚表中去。
2. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,无法访问虚函数表。
3. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数,这样才能通过基类指针正确调用派生类的析构函数。
5. 对象访问普通函数快还是虚函数快?
答:普通对象一样快;指针或引用对象调用普通函数更快,因为虚函数需要在运行时查表。
6. 虚函数表是在什么阶段生成的,存在哪部分?
答:编译阶段生成,一般存在代码段(常量区)。
7. 总结
7.1 多态的核心要点
- 构成条件:虚函数 + 重写 + 基类指针/引用调用
- 实现原理:虚函数表 + 动态绑定
- 关键字:virtual、override、final
7.2 使用建议
- 析构函数:基类析构函数应该声明为虚函数
- 接口设计:使用抽象类定义接口规范
- 继承关系:明确is-a关系才使用public继承
- 性能考虑:非多态场景不要使用虚函数
7.3 代码实践
// 好的多态设计示例
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual void draw() const = 0; // 纯虚函数
virtual ~Shape() = default; // 虚析构函数
};
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
virtual double area() const override {
return 3.14159 * radius * radius;
}
virtual void draw() const override {
cout << "绘制圆形" << endl;
}
private:
double radius;
};
void printArea(const Shape& shape) {
cout << "面积: " << shape.area() << endl; // 多态调用
}

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



