目录
- 前言
- 一、多态的概念
- 二、多态的定义及实现
-
- [2.1 多态的构成条件](#2.1 多态的构成条件)
-
- [2.1.1 虚函数](#2.1.1 虚函数)
- [2.1.2 实现多态还有两个必须重要条件](#2.1.2 实现多态还有两个必须重要条件)
- [2.1.3 虚函数的重写/覆盖](#2.1.3 虚函数的重写/覆盖)
- [2.1.4 多态场景的一个选择题(腾讯面试题)](#2.1.4 多态场景的一个选择题(腾讯面试题))
- [2.1.5 虚函数重写的一些其他问题](#2.1.5 虚函数重写的一些其他问题)
-
- [2.1.5.1 协变(了解)](#2.1.5.1 协变(了解))
- [2.1.5.2 析构函数的重写(笔试常见)](#2.1.5.2 析构函数的重写(笔试常见))
- [2.1.6 override 和 final 关键字](#2.1.6 override 和 final 关键字)
- [2.1.7 重载/重写/隐藏的对比(面试考点)](#2.1.7 重载/重写/隐藏的对比(面试考点))
- 结语


🎬 云泽Q :个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》
⛺️遇见安然遇见你,不负代码不负卿~
前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
一、多态的概念
多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲运行时多态,编译时多态(静态多态)和运行时多态(动态多态)。编译时多态(静态多态)主要就是我前面文章讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是"(> ^ w ^ <)喵",传狗对象过去,就是"汪汪"。
cpp
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket()
{
cout << "买票 - 全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "买票 - 打折" << endl;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
这串代码如果交给没有学过多态的新手来看,新手会看到Func函数的形参定义是Person* ptr,因此会认为:无论传入的是Person对象的地址(&ps)还是Student对象的地址(&st),ptr的类型永远是Person*(编译器在编译阶段就固定的,不会因为传入的实际对象不同而改变)
既然ptr的类型是Person*,那么ptr->BuyTicket()就只能调用Person类中定义的BuyTicket()函数。
基于以上理解,新手会预判这段代码的运行结果是:
cpp
买票 - 全价
买票 - 全价
实则不然,不同的对象区调用会产生不同的结果


上面第一个是基类的指针调用虚函数,第二个是基类的引用调用虚函数
二、多态的定义及实现
2.1 多态的构成条件
多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票
2.1.1 虚函数
类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰
注意:虚函数和虚继承虽然用的是同一个关键字virtual,但是并没有什么关联关系
2.1.2 实现多态还有两个必须重要条件
-
必须是基类的指针或者引用调用虚函数(如下图用基类对象直接调用就无法达成多态了)

-
被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖

如图若被调用的函数不是虚函数,也不构成多态
2.1.3 虚函数的重写/覆盖
虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为C++ 规定:基类中被virtual修饰的成员函数,其 "虚函数" 的特性会被派生类继承。只要派生类定义的函数与基类虚函数的签名完全一致(函数名、参数列表、const/volatile修饰符、返回类型都匹配),那么这个派生类函数会自动成为虚函数,无需显式加virtual关键字,自然也能构成对基类虚函数的重写),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意留这个坑,让你判断是否构成多态,主包之前在腾讯的笔试题中见过,非常恶心,只能说相关出题人用心险恶。

2.1.4 多态场景的一个选择题(腾讯面试题)
上面提及的非常变态的面试题就是下面这一道,兄弟们可以看一下大厂的笔试题恶心到什么程度,我个人感觉这就属于专为考试而设计的题目了,公司内如果真的有人写出这种代码的话,第二天应该直接就要走人了

以上程序输出结果是什么()
cpp
A:A->0
B:B->1
C:A->1
D:B->0
E:编译出错
F:以上都不正确
题目解析:
首先要清楚4个核心概念

上面静态类型 和动态类型的绑定规则是 C++ 语言标准明确规定 的,不是编译器随意实现的,所有符合标准的 C++ 编译器都必须遵守
第一步:拆解题目完整代码(标注继承关系)
cpp
#include <iostream>
using namespace std;
// 父类(基类)A
class A {
public:
// 虚函数func:允许子类重写,默认参数val=1(静态绑定的关键)
virtual void func(int val = 1) {
cout << "A->" << val << endl;
}
// 虚函数test:内部调用func(),B未重写此方法(继承复用的关键)
virtual void test() {
func(); // 等价于 this->func(),this是隐藏指针
}
};
// 子类(派生类)B:public继承A(继承的核心)
class B : public A {
public:
// 重写(override)A的虚函数func:函数签名和父类一致(多态的前提)
// 默认参数改为val=0,但默认参数不参与多态
void func(int val = 0) {
cout << "B->" << val << endl;
}
// 关键:B没有重写test() → 完全继承A的test()逻辑
};
int main() {
B* p = new B; // 静态类型:B*;动态类型:B*
p->test(); // 核心调用1:重点分析
p->func(); // 核心调用2:对比分析
delete p; // 补充:避免内存泄漏
return 0;
}
第二步:核心调用 1 → p->test ()(4 个概念全联动)
步骤 1:执行 p->test () → 继承 + 静态 / 动态类型
- 继承:B 没有重写 test(),因此继承并复用 A 的 test() 方法;
- 静态类型:p 的静态类型是
B*(声明时的类型); - 动态类型:p 的动态类型是
B*(实际指向 B 对象); - 多态:此步骤暂未触发多态(test () 未被重写,调用的是确定的 A::test ())。
步骤 2:进入 A::test () → this 指针的静态 / 动态类型
A::test () 中的 func() 等价于 this->func(),this 是隐藏的成员函数参数,此时:
- 继承:this 是 A 类成员函数的隐藏指针,因此静态类型被编译器固定为
A*(继承链中 "父类方法的 this 天然认为自己指向父类"); - 静态类型:this 的静态类型 =
A*(编译期确定,和 A 类绑定); - 动态类型:this 的动态类型 =
B*(运行期传递,实际指向 p 所指的 B 对象); - 多态:即将触发多态(因为 func () 是虚函数)。
步骤 3:执行 this->func () → 多态 + 静态 / 动态类型
这是整个题目的核心,多态和静态绑定在此分道扬镳:
- 多态(函数体):func () 是虚函数,按动态类型(
B*) 绑定 → 执行 B::func () 的函数体(输出 "B->"); - 静态类型(默认参数):默认参数是编译期决议,按静态类型(
A*) 绑定 → 取 A::func () 的默认参数 1; - 继承:B 重写了 A 的 func (),才让多态有了 "不同实现" 的基础;
- 动态类型:保证了 "执行的是子类的函数体"。
步骤 4:最终结果
执行 B::func (1) → 输出 B->1。
第三步:核心调用 2 → p->func ()
步骤 1:执行 p->func () → 继承 + 静态 / 动态类型
- 继承:B 重写了 A 的 func (),因此优先调用自身的 func ();
- 静态类型:p 的静态类型 = B*(声明时的类型);
- 动态类型:p 的动态类型 = B*(实际指向 B 对象);
- 多态:即将触发多态,但静态 / 动态类型一致,结果更直观。
步骤 2:执行 p->func () → 多态 + 静态 / 动态类型
- 多态(函数体):按动态类型(B*)绑定 → 执行 B::func () 的函数体(输出 "
B->"); - 静态类型(默认参数):按静态类型(B*)绑定 → 取 B::func () 的默认参数 0;
- 继承:B 重写 func () 是多态的前提;
- 动态类型:和静态类型一致,无 "矛盾"。
步骤 3:最终结果
执行 B::func (0) → 输出 B->0。

2.1.5 虚函数重写的一些其他问题
2.1.5.1 协变(了解)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。协变的实际意义并不大,所以了解一下即可

但是若A,B类没有继承关系,就会有报错,就是因为没有满足协变的要求,如下图

返回值并不局限于其他类的类型,用自己的类型也可以,只要这两个类型是继承关系就可以

2.1.5.2 析构函数的重写(笔试常见)
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor ,所以基类的析构函数加了 virtual 修饰,派生类的析构函数就构成重写。
下面结合代码说明一下某些情况下若析构函数不加 virtual 会出现的问题(为什么基类中的析构函数建议设计为虚函数),这个问题在面试中经常考察,一定要结合样例才可以讲清楚,主包写的示例代码也可以参考



情况 1:栈上对象(A a; B b;)
两种析构版本分析(无虚表 / 虚指针)

情况 2:基类指针指向基类堆对象(A* ptr = new A; delete ptr;)
两种析构版本分析(无虚表 / 虚指针)

情况 3:基类指针指向派生类堆对象(A* ptr = new B; delete ptr;)

情况 4:基类指针管理派生类 + 基类堆对象(A* ptr1 = new B; delete ptr1; A* ptr2 = new A; delete ptr2;)

上面派生类重写的虚函数可以不加 virtual 某种程度上来说原因就在这里,避免派生类的析构函数没写 virtual 不构成多态进而造成内存泄漏
补充一点:内存泄漏是非常严重的问题,也是因为C++的特性最容易出现的问题,泄露一点点没有问题,但是如果用一次泄露一点就是慢性死亡了,等到项目上线运行十天半个月后,内存就会逐渐捉襟见肘,越用越卡
2.1.6 override 和 final 关键字
从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错,参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug灰得不偿失,因此C++11提供了 override ,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final修饰
如图加了 override 可以检查出基类的函数名写错,未加就检查不出


加了 final 后,基类中有函数重写了就会报错

不重写就没事

注意两个关键字的位置,override 是放在派生类的虚函数处,final 是放在基类的虚函数处
2.1.7 重载/重写/隐藏的对比(面试考点)

结语
