2. 多态的概念
同一操作作用于不同对象,可以产生不同的行为。 我们以 "乐器演奏会"为例,指挥家在台上挥动指挥棒,此时:
- 钢琴演奏者听到后,开始弹奏钢琴。
- 小提琴演奏者听到后,开始拉小提琴。
- 鼓手听到后,开始击鼓。
在这个场景中,"指挥棒挥动" 就是统一的接口,而不同乐器的演奏者则是不同的对象,他们对这一统一指令做出了不同的响应,这就解释了什么是多态。

多态的核心价值:
- 接口与实现分离: 基类定义接口,派生类提供实现
- **可扩展性:**新增派生类不影响现有代码
- **代码复用:**通用逻辑放在基类,派生类专注差异
- **设计灵活性:**支持运行时对象替换(工厂模式、策略模式等)
2.1 多态的定义和实现
在C++中实现多态主要通过方法重写 和继承实现。在继承中要构成多态还有两个条件(缺一不可):
- 必须通过基类的指针 或者引用调用虚函数
- 被调用的函数必须是虚函数 ,且子类必须对基类的虚函数进行重写
注: 使用父类指针指向子类对象是实现多态性的核心机制

2.2 虚函数
虚函数:即被virtual
关键字修饰的类成员函数称为虚函数。
cpp
class Person {
public:
// 虚函数(用于多态)
virtual void introduce() {
cout << "我是普通人。" << endl;
}
2.3 虚函数重写
子类中定义与父类虚函数 同名、同参数列表、同返回类型 的函数,重写(覆盖)父类的实现。
注意: 在重写父类虚函数时,子类的虚函数在不加virtual
关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
cpp
class Person {
public:
virtual void introduce() {
cout << "我是普通人。" << endl; }
};
class Student : public Person {
public:
void introduce() override {// 重写父类的虚函数,override检查是否重写
cout << "我是一个学生。" << endl; }
};
虚函数的重写有两个例外:协变 和析构函数的重写。
- 协变(父类与子类虚函数返回值类型不同)
子类重写父类虚函数时,与父类虚函数返回值类型不同。即父类虚函数返回基类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。
cpp
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;}
};
- 虚析构函数(父类与子类析构函数的名字不同)
当通过父类指针删除子类对象,而父类的析构函数不是虚函数时,只会调用父类的析构函数,而不会调用子类的析构函数。这会导致子类对象的资源无法被正确释放,造成内存泄漏。父类析构函数为虚函数,子类只需定义即可构成重写。

此处因为Person
的析构函数不是虚函数,delete p2
只会根据指针类型(Person*
)调用父类析构函数,导致子类Student
析构函数未被调用。

将父类的析构函数声明为虚函数,此时通过父类指针删除子类对象时,会先调用子类的析构函数,再调用父类的析构函数,确保资源正确释放。
2.4 final和override
final
:修饰虚函数,表示该虚函数不能再被重写
override
: 检查派子类虚函数是否重写了父类某个虚函数,如果没有重写编译报错(重写检查)。
2.5 重载、重写、重定义
- 重载: 指在同一个类中定义多个同名方法,但这些方法的参数列表不同(参数类型、数量或顺序)。编译器根据调用时的实参类型和数量来决定执行哪个方法。
- 重写: 也叫做覆盖,一般发生在子类和父类继承关系之间。子类重新定义父类中有相同名称和参数的虚函数。
- 隐藏: 也叫做重定义,子类重新定义父类中有相同名称的非虚函数(参数列表可以不同),指父类的函数屏蔽了与其同名的基类函数。可以理解成发生在继承中的重载。

重定义:
cpp
class Base {
public:
void show() { cout << "Base show" << endl; } // 非虚函数
void print(int x) { cout << "Base: " << x << endl; }
};
class Derived : public Base {
public:
// 重定义show(隐藏基类show)
void show() { cout << "Derived show" << endl; }
// 重定义print(参数不同)
void print(double x) { cout << "Derived: " << x << endl; }
};
2.6 抽象类
本质是强制子类完成基类虚函数的重写来实现特定方法。通过父类指针 或引用 操作子类类对象。override
只在语法上检查是否完成重写。
定义:
- 抽象类是包含至少一个纯虚函数的类,无法被实例化,只能作为父类被继承。
- 纯虚函数 的声明方式为:
virtual 返回类型 函数名(参数列表) = 0;
,体现出了接口的继承。
2.7 接口继承与实现继承
2.7.1 接口继承
指子类仅继承父类的方法签名(接口),不继承具体实现。目的是为了重写,实现多态。
2.7.2 实现继承
实现继承是指派生类继承基类的方法实现,可直接使用或重写。通过普通虚函数 或非虚函数实现。
2.8 多态的原理

我们定义一个没有成员的空类,只有一个虚函数。但是它的大小为4,可以推测其中暗含一个指向虚函数表的指针,依靠虚函数表指针+虚表
实现多态?下面进行验证
虚函数表:
- 每个包含虚函数的类(或继承自包含虚函数的类)都有一个虚函数表(由编译器自动生成)。
- 虚函数表是一个函数指针数组(存放虚函数的地址),每个条目指向该类的某个虚函数的具体实现。当通过
基类指针
或基类引用
调用虚函数时,程序会使用对象的虚函数表来查找正确的函数地址并调用。 - 如果子类重写了父类的虚函数,则子类的虚函数表中对应的条目会被替换为子类的实现;未重写的条目保留父类的实现。
虚函数指针:
- 每个包含虚函数的类的对象都会有一个
vptr
,指向该对象类的vtable
。 - 当对象被创建时,
vptr
才会被初始化为指向其类的虚函数表。当通过父类指针
或引用
调用虚函数时,会通过vptr
访问虚函数表,从而实现多态。
在单继承中:
- 子类会继承父类的虚函数表,并
覆盖
(重写)的虚函数,如:vfunc1()
- 未重写的虚函数仍然指向父类的实现,如:
vfunc2()
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1() { cout << "Base::func1\n"; }
void func3() { cout << "Base::func3 (non-virtual)\n"; } // 非虚函数
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1\n"; } // 重写 func1
};
int main() {
Base* obj = new Derived();
obj->func1(); // 输出 "Derived::func1"(动态绑定)
obj->func3(); // 输出 "Base::func3"(静态绑定)
Derived d;
Base& obj2 = d;//父类引用绑定子类对象
obj2.func1();
obj2.func3();
delete obj;
return 0;
}
-编译阶段:
- 编译器发现
func1
是虚函数,不会直接生成调用指令,而是生成查表调用代码。 func3
是非虚函数,直接静态绑定Base::func3
。
执行obj->func1():
- 获取 vptr: 编译器知道
func1
是虚函数,不会调用Base::func1
,而是通过obj
的vptr
找到虚函数表。 - 定位虚函数表:
obj
实际指向Derived 对象,因此vptr
指向 Derived 的虚函数表。 - 查找 func1 的条目: 虚函数条目按声明顺序排列,
func1
索引是0
,因此直接访问vtable[0]。 - 调用正确的函数: 调用vtable[0]实际储存的
Derived::func1
。

