文章目录
- 前言
- 一、多态的定义
- 二、多态的实现条件
- 三、虚函数
-
- 1.展示示例
- 2.虚函数的覆盖/实现
- 3.虚函数析构函数的重写
- 4.协变
- [5. override 和final关键字](#5. override 和final关键字)
- 经典例题
- 四、纯虚函数和抽象类
- 五、多态的原理
前言
例如
一、多态的定义
多态 (polymorphism) 的概念:通俗来说,就是多种形态。其分为两种形态。
- 静态多态
静态多态,也称为编译时多态。编译时多态 (静态多态) 主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。核心:在编译阶段就确定调用哪个函数,程序运行时不需要额外查表开销。 - 动态多态
运行时多态,具体点就是去完成某个行为 (函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票 (5 折或 75 折);军人买票时是优先买票。再比如,同样是动物叫的一个行为 (函数),传猫对象过去,就是"喵喵",传狗对象过去,就是 "汪汪"。
二、多态的实现条件
-
必须是具有继承关系的类对象。
实现多态,语法上私有 / 保护继承也能写,但没有实际意义;工程上多态必须用公有继承 public。
-
基类声明虚函数,派生类重写该虚函数
基类中需要被多态调用的函数必须用 virtual 关键字声明为虚函数。
派生类必须对该虚函数进行重写(Override):函数名、参数列表、const 属性必须完全一致
派生类必须对基类的虚函数完成重写 / 覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。
-
基类指针 / 引用指向派生类对象
必须通过基类的指针或引用调用虚函数,才会触发动态绑定。如果直接通过对象调用虚函数,编译期就会确定调用版本,属于静态绑定。同时因为只有基类的指针或引用才能既指向基类对象又指向派生类对象
示例:
cpp
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);
}
int main()
{
Test();
return 0;
}
运行结果:

三、虚函数
当然大家听到这肯定云里雾里的,因为你们对多态的大体结构并不认识,且虚函数也没有了解。这里先来介绍虚函数。
首先明确一点的是,这里的虚函数与与我们为了解决菱形继承的虚继承,没有关系,它们只是都用了同一个关键字。
1.展示示例
类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。
cpp
class Person
{
public:
virtual void buy()
{
cout << "我买普通票" << endl;
}
};
2.虚函数的覆盖/实现
虚函数的重写 / 覆盖:派生类中有一个除了函数内容以外其他与基类完全相同的虚函数 (即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
注意 :
在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写 (因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意埋这个坑,让你判断是否构成多态。

这里没有编译报错,你要知道虚函数被派生类继承下来了。

这里可以看到虽然都是 Person指针Ptr在调用BuyTicket
但是跟ptr没关系,而是由ptr指向的对象决定的。
3.虚函数析构函数的重写
我们之前在继承的那个篇章里写过,C++在某些地方因为前面设计产生的问题而不得不设计一个新的功能来解决,这里就是这样。
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor,所以基类的析构函数加了 virtual 修饰,派生类的析构函数就构成重写。

像这边的处理成destructor,就是C++为了解决虚函数重写而新建的语法。
cpp
class Base {
public:
~Base() { cout << "Base析构" << endl; }
};
class Derive : public Base {
public:
int* ptr;
Derive() { ptr = new int(100); }
~Derive() {
delete ptr; // 这一步不会执行!
cout << "Derive析构,释放ptr" << endl;
}
};
int main() {
Base* p = new Derive();
delete p;
// 输出:Base析构
// Derive的ptr没有被delete → 内存泄漏!
}
基类析构函数必须写成 virtual,否则通过基类指针 / 引用释放派生类对象时,只会调用基类析构,派生类析构不会执行 → 内存泄漏!
原因:
-
当析构不是虚函数:
delete p 时,编译器做静态绑定,看指针的类型(Base*),而不是指针实际指向的对象类型。
指针类型是 Base* → 直接调用 Base::~Base(),完全不看 p 实际指向 Derive 对象,不会去找虚表,不会调用派生析构。
-
如果基类析构加 virtual(动态绑定)看 p 指向对象的虚表,拿到实际对象(Derive)的析构函数地址。
调用 Derive::~Derive()(派生析构)。
派生析构执行完,编译器自动调用基类析构。
顺序:派生析构 → 基类析构。
4.协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
规则:
- 必须是虚函数重写场景(普通函数不存在协变概念)
- 基类必须返回 Base* / Base&;派生必须返回 Derive* / Derive&
- 派生类返回类型,必须是基类返回类型对应的派生类(有继承关系)
- 参数列表必须完全一致,函数名相同
cpp
class Base {
public:
virtual Base* getObj() {
cout << "Base::getObj" << endl;
return this;
}
virtual ~Base(){}
};
class Derive : public Base {
public:
// 重写,返回Derive*,而不是Base*,协变合法
Derive* getObj() override {
cout << "Derive::getObj" << endl;
return this;
}
};
int main() {
Base* p = new Derive();
auto ret = p->getObj(); // 动态绑定,调用Derive::getObj
delete p;
return 0;
}
输出:Derive::getObj
协变就是让派生类重写虚函数时,可以返回更具体(派生类)的指针 / 引用,保留多态的同时,让派生类调用该函数拿到派生类类型,不需要强制类型转换。
5. override 和final关键字
从上面可以看出,C++ 对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来 debug 会得不偿失,因此 C++11 提供了 override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用 final 去修饰
- override
显式告诉编译器:这个函数是用来重写(覆盖)基类的虚函数,让编译器帮你检查重写是否合法。不加 override 也能实现多态重写,但容易写错签名,变成新建一个同名函数(隐藏,不是重写),bug 很难找。
cpp
class Base {
public:
virtual void func(int a) {
cout << "Base::func(int)" << endl;
}
};
class Derive : public Base {
public:
// 正确重写,override校验通过
void func(int a) override {
cout << "Derive::func(int)" << endl;
}
// 错误!参数不一样,加override会直接编译报错
// void func(double a) override {}
};
int main() {
Base* p = new Derive();
p->func(10); // 多态,调用Derive版本
delete p;
return 0;
}
重写的位置在派生类的重写函数后面。
- final
final 修饰虚函数:禁止派生类继续重写这个虚函数。
基类 / 派生类的虚函数加 final,后续继承它的子类不能再 override 这个函数,编译报错。
cpp
class Base {
public:
virtual void func() {
cout << "Base" << endl;
}
};
class Derive : public Base {
public:
void func() final override { // 先override重写,再加final锁住,不能继续重写
cout << "Derive" << endl;
}
};
class Son : public Derive {
public:
// 编译报错!Derive的func被final修饰,禁止重写
// void func() override {}
};
经典例题
结果为多少:
cpp
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
答案为 b->1。
-
B 是公有继承 A,test() 没有在 B 中重新实现,所以B 直接继承 A 的 test 成员函数。所以 p->test() 调用的就是 A::test()。
-
默认参数是编译期静态绑定:编译器看这个调用语句func()写在 A 类的 test 函数内部,就拿 A 的func默认参数 val=1。
-
func是虚函数,运行时动态绑定到 B 的func函数体。
场景 2:直接通过 B 指针调用 func
cpp
B* p = new B;
p->func();
// 调用点静态类型是B,编译器看B的func默认参数0 → B::func(0),输出 B->0
场景 3:A 指针指向 B 对象,直接调用 func
cpp
A* p = new B;
p->func();
// 调用点静态类型A,编译器补A的默认值1 → B::func(1),输出 B->1
特别注意
注意重载,重写,隐藏的关系。

四、纯虚函数和抽象类
在虚函数的后面写上 = 0,则这个函数为纯虚函数,纯虚函数不需要定义实现 (实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
cpp
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;
}
};
int main()
{
// 编译报错: error C2259: "Car": 无法实例化抽象类
// Car car;
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
delete pBenz;
delete pBMW;
return 0;
}

注意:
-
抽象类不允许实例化。
-
抽象类的作用主要为继承的子类强制重写其的成员函数。
五、多态的原理
1.虚函数表
首先让我们用一个题目来引入。
cpp
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}

根据我们的C的结构体知识可能算出来8,但实际上是12。
除了_b 和_ch 成员,还多一个__vfptr 放在对象的前面 (注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针 (v 代表 virtual,f 代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

2.多态的实现
从底层的角度 Func 函数中 ptr->BuyTicket (),是如何作为 ptr 指向 Person 对象调用 Person::BuyTicket,ptr 指向 Student 对象调用 Student::BuyTicket 的呢?
其实本质上就是指向哪个对象就通过虚函数指针找到该对象的虚函数表,从而找到对应的虚函数地址进行调用。
cpp
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
// 但是跟ptr没关系,而是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
// 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后
// 多态也会发生在多个派生类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。ptr 指向的 Person 对象,调用的是 Person 的虚函数;ptr 指向的 Student 对象,调用的是 Student 的虚函数。
3.动静态绑定
-
不满足多态条件 (指针或者引用 + 调用虚函数) 的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
-
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,叫做动态绑定。
4.虚函数表
虚函数表,简称虚表,本质上是一个函数指针数组。
-
基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
-
派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
-
派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。即如果没有重写的话,就继承基类的虚函数与基类保持一致
-
派生类的虚函数表中包含,(1) 基类的虚函数地址,(2) 派生类重写的虚函数地址完成覆盖,(3)派生类自己的虚函数地址,三个部分。
-
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 0x00000000 标记。(这个 C++ 并没有进行规定,各个编译器自行定义的,vs 系列编译器会在后面放个 0x00000000 标记,g++ 系列编译不会放)
-
虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
-
虚函数表存在哪的?这个问题严格说并没有标准答案 C++ 标准并没有规定,vs 下是存在代码段 (常量区)。
cpp
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base
{
public:
// 重写基类的func1
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func1" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
特别注意 :
相同类型的派生类它们的虚函数表也是相同的。比如Base定义了,a,b。


虚表的内容遵循以下规则:
派生类未重写基类虚函数:虚表对应位置保留基类虚函数的地址;
派生类重写(override)了基类虚函数:虚表对应位置替换为派生类自身的函数地址;
派生类新增自己的虚函数:按声明顺序追加在虚表的末尾。