C++之多态
多态
- 什么是多态?
- 多态的定义及实现
- 多态的构成条件
- 虚函数
- 虚函数的重写/覆盖
- 关键技术原理
- 最佳实践指南
- 虚函数重写
- 协变
- 析构函数的重写
- override和final关键字
- 纯虚函数和抽象类
- 多态的原理
- 多态是如何实现的
- 1. 虚函数表(vtable)
- 虚函数表知识要点
- 2. 虚函数的声明
- 3. 多态的实现过程
- 动态绑定与静态绑定

什么是多态?
多态(Polymorphism)是面向对象编程的三大核心特性之一(封装、继承、多态),源于希腊语"多种形态"。在C++中,它允许我们使用统一的接口处理不同类型的对象,显著提高了代码的灵活性和可扩展性。
核心概念
- 同一接口,多种形态
不同的对象可以通过相同的方法名调用,但实际执行的逻辑由对象自身的类决定。 - 解耦调用与实现
调用者只需关注接口(方法名和参数),无需关心具体实现,提高代码的可扩展性和可维护性。
多态的定义及实现
多态的构成条件
多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。
实现多态还有两个必须重要条件:
-
必须是基类的指针或者引⽤调⽤虚函数
-
被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。
说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类 对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派 ⽣类之间才能有不同的函数,多态的不同形态效果才能达到。
虚函数
类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。
class A
{public:virtual void() {}
};
虚函数的重写/覆盖
虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性)
关键技术原理
-
虚函数表(vtable)
- 每个包含虚函数的类自动生成虚函数表
- 存储该类所有虚函数的地址
- 创建对象时隐式添加
vptr
指针指向vtable
-
动态绑定过程
最佳实践指南
-
使用
override
关键字明确重写意图 -
基类析构函数必须声明为
virtual
-
接口类使用纯虚函数
-
性能考量:虚函数调用比普通函数多一次指针解引用
虚函数重写
协变
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。
核心概念
- 基类虚函数返回基类类型指针 / 引用。
- 派生类重写函数返回派生类类型指针 / 引用。
- 编译器会自动处理类型转换,确保调用的一致性。
协变的条件
- 基类函数必须为虚函数。
- 返回类型必须是指针或引用(不能是值类型)。
- 派生类返回类型必须是基类返回类型的公有派生类。
- 函数签名的其他部分(参数、常量性)必须完全相同。
析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。
class A{public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A {public:~B(){ cout << "~B()->delete:"<<_p<< endl;delete _p;}protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为 ~B()中在释放资源。
只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
override和final关键字
- override 关键字
作用
明确标记一个函数是重写基类的虚函数,编译器会检查以下条件:
基类中是否存在同名虚函数。
基类和派生类的函数签名(参数、常量性、引用限定符)是否严格一致。
返回类型是否符合协变规则(如果适用)。 - final 关键字
作用
final 有两种用法:
修饰虚函数:禁止派生类重写该虚函数。
修饰类:禁止该类被继承。
纯虚函数和抽象类
在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象
- 纯虚函数:在基类中声明为
virtual
,并且没有实现的函数,形式为virtual void func() = 0;
。 - 抽象类:包含纯虚函数的类称为抽象类,不能实例化对象,只能作为基类被派生。
- 示例:
class Shape { public:virtual void draw() = 0; // 纯虚函数 };class Circle : public Shape { public:void draw() override {cout << "Drawing Circle" << endl;} };
Shape
是一个抽象类,Circle
是派生类,重写了draw()
函数。- 多态的使用:
Shape* shape = new Circle(); shape->draw(); // 输出:Drawing Circle
多态的原理
多态是如何实现的
在C++中,多态主要通过虚函数(virtual functions)和继承机制来实现。以下是多态实现的详细过程和原理:
1. 虚函数表(vtable)
- 概念:每个包含虚函数的类都会有一个虚函数表(vtable),它是一个存储虚函数地址的数组。当创建一个类的对象时,对象会包含一个指向其类的虚函数表的指针(vptr)。
- 作用:vtable是实现多态的关键。当通过基类指针或引用调用虚函数时,程序会通过vptr找到对应的vtable,然后通过vtable找到实际的虚函数地址。
虚函数表知识要点
- 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。
- 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣对象中的基类对象成员也独⽴的。
- 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
- 派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分。
- 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(此条取决于编译器)
- 虚函数存储的位置(vs下为常量区)
可以自行验证:
class Base {public:virtual void func1() {}protected:int a = 1;
};class Derive : public Base
{
public:virtual void func1() {}
protected:int b = 2;
};int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Base* p3 = &b;printf("虚表地址:%p\n", *(int*)p3);return 0;
}
//通过比较虚表地址,找到其最近的内存片段来确定
2. 虚函数的声明
- 在类中使用
virtual
关键字声明函数为虚函数。
Base为基类,Derived为继承类。class Base { public:virtual void show() {cout << "Base show()" << endl;} };class Derived : public Base { public:void show() override { // override表示重写基类的虚函数cout << "Derived show()" << endl;} };
Base
类的show()
函数被声明为虚函数,Derived
类重写了这个虚函数。
3. 多态的实现过程
- 创建对象:
Derived d;
- 当创建
Derived
类的对象d
时,对象中会包含一个vptr,指向Derived
类的vtable。 Derived
类的vtable中存储了Derived
类重写的虚函数show()
的地址。
- 当创建
- 通过基类指针或引用调用虚函数:
Base* ptr = &d; ptr->show();
ptr
是一个指向Base
类的指针,但它指向的是Derived
类的对象。- 当调用
ptr->show()
时,程序会通过ptr
找到对象的vptr,然后通过vptr找到Derived
类的vtable,最终调用Derived
类的show()
函数。 - 这就是多态的运行时绑定(动态绑定)过程。
动态绑定与静态绑定
• 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
• 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。