C++多态详解
1. 多态的概念
多态性允许我们通过基类的指针或引用访问派生类的成员函数,使得同一个函数调用可以有不同的行为。
多态 = “一个名字,多种行为”。
例如:
票价的售卖,同样是售卖门票,但是针对不同的人群有不同的票价。
语言的种类,同样是说话,不同国家的人有不同的语言。
多态分为两种类型:
- 静态多态(编译时多态):通过函数重载和运算符重载实现
- 动态多态(运行时多态):通过虚函数实现
1.1静态多态(编译时多态)
编译器在编译时直接根据参数类型挑选对应的函数,然后将函数调用“替换成”具体的机器指令。运行时没有任何判断、跳转、查表的过程,效率极高。
常见实现方式:
-
函数重载(Function Overload)
-
运算符重载(Operator Overload)
-
类模板
1.2动态多态(运行时多态)
动态多态发生在程序运行时,即程序执行过程中才根据对象的实际类型决定要调用哪个函数。
关键要素:
-
基类中的函数必须是
virtual
(虚函数) -
派生类中要重写这个虚函数
-
使用基类的指针或引用调用这个函数
底层原理:
当类中出现虚函数时,编译器会为这个类创建一张虚函数表(vtable),每个对象中还会有一个指针 vptr
指向对应的表。但是一般父类创建虚函数表后,子类无需额外创建,直接继承父类的虚函数表,若在子类中对虚函数进行重写后,会在子类虚函数表中覆盖原来父类该函数的地址。
运行时,程序会:
-
通过对象的
vptr
找到 vtable -
在表中查找到对应的函数地址
-
跳转执行这个地址的函数(比如 Chinese::Speak)
所以 p->Speak()
虽然 p是 Person*
类型,但它内部的 vptr
指向的是 Chinese的函数表,所以调用的是Chinese::Speak()
。
2. 实现多态的条件
2.
2. 实现多态的条件
实现动态多态需要满足以下两个条件,缺一不可:
-
虚函数的重写:派生类重写基类的虚函数
-
基类指针或引用调用虚函数:使用基类的指针或引用指向派生类对象,并通过该指针或引用调用虚函数
#include <iostream>
using namespace std;class Person {
public:virtual void BuyTicket() {cout << "全价购票" << endl;}
};class Student : public Person {
public:virtual void BuyTicket() { // 重写基类的虚函数cout << "半价购票" << endl;}
};void Show(Person& p) { // 通过基类引用调用虚函数p.BuyTicket();// 这里发生了隐式向上类型转换:将派生类对象转换为基类引用// 原因是派生类继承了基类的所有公共和受保护成员,因此派生类对象可以被安全地视为基类对象的一种
}int main() {Person p;Student s;Show(p); // 输出: 全价购票Show(s); // 输出: 半价购票return 0;
}
在这段代码中:Person
类中定义了一个虚函数BuyTicket()
,Student
类继承自Person
并重写了BuyTicket()
函数,函数Show()
接受一个Person
类型的引用,并调用其BuyTicket()
方法,当我们传递Student
对象时,会调用Student
类的BuyTicket()
方法,这就是多态的表现。
3.虚函数的重写规则
虚函数重写必须满足以下条件:
- 函数名相同
- 参数列表相同(参数类型和数量)
- 返回值类型相同(有特例:协变)
- 访问修饰符可以不同,但不能降低访问权限
3.1协变
C++允许在派生类中重写的虚函数返回类型与基类虚函数返回类型不同,这种特殊情况称为"协变返回类型"。协变要求:
-
基类虚函数返回基类对象的指针或引用
-
派生类虚函数返回派生类对象的指针或引用(派生类是基类的子类)
class Person {
public:virtual Person* BuyTicket() {cout << "全价购票" << endl;return this;}
};class Student : public Person {
public:virtual Student* BuyTicket() { // 协变返回类型cout << "半价购票" << endl;return this;}
};
注意:在重写基类虚函数时,派生类的虚函数前面的
virtual
关键字可以不加,编译器会自动处理这种情况。但为了代码可读性,建议显式添加virtual
关键字。
4. 虚函数表和多态的实现原理
每个包含虚函数的类都有一个虚函数表,这个表是一个指针数组,存储了该类所有虚函数的地址:
-
虚函数表:每个类拥有一个虚函数表,存储类中所有虚函数的地址
-
虚函数表指针(vptr):每个对象都有一个虚表指针,指向该对象所属类的虚函数表
-
动态绑定:当通过基类指针或引用调用虚函数时,会根据对象的实际类型查找对应的虚函数表,找到并调用正确的函数
重要细节:子类本身没有独立的虚表指针,而是包含在从父类继承下来的对象中。当子类重写父类的虚函数时,子类的虚表会覆盖原来函数地址,而父类的虚表保持不变,仍存储父类的函数地址。
我们可以通过以下代码查看虚函数表的内容:
typedef void(*FuncPtr)(); // 定义函数指针类型// 打印虚函数表的函数
void PrintVFTable(FuncPtr* pVTable) {for (size_t i = 0; pVTable[i] != 0; i++) {printf("pVTable[%d]:%p->", i, pVTable[i]);FuncPtr f = pVTable[i];f(); // 调用函数}cout << endl;
}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; } // 新增
private:int _b;
};void test() {Base b;Derive d;// 获取并打印虚函数表PrintVFTable((FuncPtr*)(*(size_t*)&b)); //取出对象的地址后强转为整数指针进行解引用后得到对PrintVFTable((FuncPtr*)(*(size_t*)&d)); //象内部虚函数表的地址后强转为(FuncPtr*)}
5. 虚析构函数
当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,则只会调用基类的析构函数,而不会调用派生类的析构函数,这可致内能导存泄漏。因此,当类设计用作基类时,通常应该将其析构函数声明为虚函数:
class Person {
public:virtual ~Person() {cout << "~Person()" << endl;}
};class Student : public Person {
public:virtual ~Student() { // 析构函数的函数名会被编译器内部处理成destructor//无需显式调用基类的析构函数,派生类析构函数结束后会自动调用基类析构函数cout << "~Student()" << endl;}
};int main() {Person* p = new Student();delete p; // 会先调用Student的析构函数,再调用Person的析构函数return 0;
}
6.纯虚函数和抽象类
6.1 纯虚函数
纯虚函数是一种特殊的虚函数,它在基类中没有实现,要求派生类必须提供实现:
virtual 返回类型 函数名(参数列表) = 0;
6.2 抽象类
包含至少一个纯虚函数的类称为抽象类。抽象类有以下特点:
-
不能实例化对象,但可以声明指针和引用
-
必须在派生类中实现所有纯虚函数,否则派生类也是抽象类
class Car {
public:virtual void Run() = 0; // 纯虚函数virtual void Stop() = 0; // 纯虚函数
};class Benz : public Car {
public:virtual void Run() {cout << "Benz Run" << endl;}virtual void Stop() {cout << "Benz Stop" << endl;}
};void test() {// Car c; // 错误:不能创建抽象类的对象Car* p = new Benz(); // 正确:可以创建抽象类的指针p->Run(); // 输出: Benz Runp->Stop(); // 输出: Benz Stopdelete p;
}
7. 虚函数的默认参数
虚函数可以有默认参数,但默认参数是静态绑定的,即根据指针或引用的类型决定使用哪个默认参数,而不是根据对象的实际类型:
class Base {
public:virtual void show(int x = 10) {cout << "Base::show " << x << endl;}
};class Derived : public Base {
public:void show(int x = 20) override {cout << "Derived::show " << x << endl;}
};int main() {Derived d;Base* p = &d;p->show(); // 输出:Derived::show 10 <- 默认值是Base的d.show(); // 输出:Derived::show 20 <- 默认值是Derived的
}
p的类型是Base*,当编译器在编译这一行时会查Base::show(int x = 10)
,看到默认值是 10
,就把它“写死”成了 p->show(10);
总的来说:
默认参数“看你用什么类型的指针”,而不是“你指向的对象是谁”
8. final 和 override 关键字
8.1 final 关键字
final
关键字可以防止类被继承或虚函数被重写:
class Base {
public:virtual void func() final { // 禁止重写此函数cout << "Base::func" << endl;}
};class Derive final : public Base { // 禁止继承此类// virtual void func() { } // 错误:不能重写被final修饰的函数
};// class Further : public Derive { }; // 错误:不能继承被final修饰的类
8.2 override 关键字
override
关键字用于明确表示函数是对基类虚函数的重写,如果不满足重写条件,编译器会报错:
class Base {
public:virtual void func() {cout << "Base::func" << endl;}
};class Derive : public Base {
public:virtual void func() override { // 明确表示重写基类的func函数cout << "Derive::func" << endl;}// virtual void funk() override { } // 错误:基类没有名为funk的虚函数
};
9. 重载、重写和重定义对比
特性 | 重载(Overload) | 重写(Override) | 重定义 / 隐藏(Hide) |
---|---|---|---|
是否发生继承关系 | 否,通常在同一个类中 | 是,派生类对基类虚函数的重新实现 | 是,发生在派生类中 |
是否函数名相同 | ✅ 是 | ✅ 是 | ✅ 是 |
参数列表 | ❗ 不同(个数或类型不同) | ✅ 相同 | ❗ 不同也可触发隐藏 |
返回值类型 | 可不同 | 通常相同(支持协变返回类型) | 可不同 |
是否必须是虚函数 | 否 | ✅ 是(基类中必须是 virtual ) | 否(可是虚函数,也可以不是) |
与虚函数表关系 | 无关 | ✅ 修改虚函数表(vtable),支持运行时多态 | ❌ 不修改虚函数表,隐藏基类版本 |
编译时 or 运行时 | 编译时绑定 | 运行时绑定(通过虚函数表) | 编译时绑定(隐藏而非替换) |
使用方式 | 同类中多个同名函数(重载) | 派生类中重写基类的虚函数(覆盖) | 派生类中定义新函数,与基类同名但参数不同或非虚函数 |
class Base {
public:void func() {cout << "Base::func()" << endl;}virtual void vfunc() {cout << "Base::vfunc()" << endl;}
};class Derive : public Base {
public:void func() { // 重定义(隐藏)cout << "Derive::func()" << endl;}virtual void vfunc() { // 重写cout << "Derive::vfunc()" << endl;}
};void test() {Base* pb = new Derive();pb->func(); // 输出: Base::func() - 静态绑定pb->vfunc(); // 输出: Derive::vfunc() - 动态绑定delete pb;
}
10.总结
C++的多态是面向对象编程的重要特性,通过虚函数实现:
- 多态的实现:虚函数 + 继承 + 基类指针/引用
- 虚函数表:多态的实现机制,每个含有虚函数的类都有一个虚函数表,子类重写虚函数会覆盖虚表中的函数地址
- 纯虚函数与抽象类:定义接口,强制派生类实现特定功能
- 虚析构函数:防止内存泄漏的重要手段
- 关键字:
virtual
在多态中用于定义虚函数,在菱形继承中用于虚继承;override
和final
帮助管理和控制多态