【本节要点】
- 多态的概念
- 多态的定义及实现
- 抽象类
- 多态的原理
- 单继承和多继承关系中的虚函数表
一、多态的概念
多态(Polymorphism)源自希腊语,意为“多种形态”。在面向对象编程(OOP)中,多态指同一操作作用于不同对象时,可以产生不同的行为。这种特性允许程序在运行时根据对象的实际类型动态调用相应的方法,从而增强代码的灵活性和扩展性。
例子:
绘图软件中的图形工具
用户选择“绘制”工具,但具体绘制的是圆形、矩形还是三角形,由当前选择的工具类型决定。不同人去买票
如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票 时是优先买票。
- 不同动物的叫声
同样都是动物类,狗叫是“汪汪汪”,猫叫是“喵喵喵”。
二、多态的定义及实现
2.1 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数。
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
![]()
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; }*/
};void Func(Person& p)
{ p.BuyTicket(); }int main()
{
Person ps;
Student st;
Func(ps); // 买票-全价
Func(st); // 买票-半价return 0;
}
示例二:动物类:狗、猫
#include <iostream>
using namespace std;// 基类:动物
class Animal {
public:virtual void speak() { cout << "动物发出声音" << endl; }virtual ~Animal() {} // 虚析构函数(防止内存泄漏)
};// 派生类:狗
class Dog : public Animal {
public:void speak() override { cout << "汪汪汪!" << endl; }
};// 派生类:猫
class Cat : public Animal {
public:void speak() override { cout << "喵喵喵!" << endl; }
};int main() {Animal* animals[] = {new Dog(), new Cat()};for (Animal* animal : animals) {animal->speak(); // 动态绑定delete animal;}return 0;
}
虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
2. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person { public:virtual ~Person() {cout << "~Person()" << endl;} };class Student : public Person { public:virtual ~Student() { cout << "~Student()" << endl; } };// 只有派生类Student的析构函数重写了Person的析构函数, // 下面的delete对象调用析构函数,才能构成多态, // 才能保证p1和p2指向的对象正确的调用析构函数。int main() {Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0; }
2.4 C++11 override(覆盖) 和 final(最终)
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
1. final:修饰虚函数,表示该虚函数不能再被重写
class Car { public:virtual void Drive() final {} };class Benz :public Car { public:virtual void Drive() {cout << "Benz-舒适" << endl;} };
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car { public:virtual void Drive(){} };class Benz :public Car { public:virtual void Drive() override {cout << "Benz-舒适" << endl;} };
2.5 重载、重写(覆盖)、重定义(隐藏)的对比
三、抽象类
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* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive(); }
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
四、多态的原理
4.1虚函数表
// 这里常考一道笔试题:sizeof(Base)是多少? class Base { public:virtual void Func1(){cout << "Func1()" << endl;} private:int _b = 1; };
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
4.2 多态的原理
还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket(示例一)
1. 观察箭头我们看到,people是指向mike对象时,people->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。2. 观察箭头我们看到,people是指向johnson对象时,people->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
内存模型示例(示例二):
Animal 对象内存布局: +----------------+ | vptr | --> 指向 Animal 的虚函数表 | 其他成员变量 | +----------------+Animal 的虚函数表: +---------------------+ | &Animal::speak() | | &Animal::~Animal() | +---------------------+Dog 的虚函数表: +---------------------+ | &Dog::speak() | // 重写后的函数地址 | &Animal::~Animal() | +---------------------+
4.3 动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
五、单继承与多继承关系的虚函数表
5.1 单继承中的虚函数表
派生类继承基类的虚函数表,并替换重写的虚函数地址。
Dog 对象: +----------------+ | vptr | --> Dog 的虚函数表 | Animal 成员变量 | | Dog 的成员变量 | +----------------+
5.2 多继承的虚函数表
派生类包含多个虚函数表(每个基类对应一个)。
菱形继承问题:通过虚继承(
virtual
)解决重复基类的问题class Base1 { public:virtual void func1() {} };class Base2 { public:virtual void func2() {} };class Derived : public Base1, public Base2 { public:void func1() override {}void func2() override {}virtual void func3() {} // 新增虚函数 };
内存布局分析:
Derived 对象: +----------------+ | vptr (Base1) | --> 指向 Base1 的虚函数表 | Base1 成员变量 | +----------------+ | vptr (Base2) | --> 指向 Base2 的虚函数表 | Base2 成员变量 | | Derived 成员变量| +----------------+
5.3 虚函数表的调试与查看
使用调试工具(如GDB)或内存查看器观察
vptr
和虚函数表。示例代码:
Derived d; Base1* b1 = &d; Base2* b2 = &d;// 查看 b1 和 b2 的虚函数表地址是否不同
六、总结与对比
特性 | 单继承 | 多继承 |
---|---|---|
虚函数表数量 | 1个 | 多个(每个基类一个) |
内存布局复杂度 | 简单 | 复杂(需处理多个基类成员) |
菱形继承问题 | 无 | 需使用虚继承解决 |
虚函数表覆盖逻辑 | 直接替换基类函数地址 | 每个基类虚函数表独立替换 |
以上就是关于继承的知识总结,如果有发现问题的小伙伴,请在评论区说出来哦。后面还会持续更新C++相关知识,感兴趣请持续关注我哦!!