多态(上)

文章目录
- 多态(上)
-
- [1. 多态的概念](#1. 多态的概念)
- [2. 多态的定义及实现](#2. 多态的定义及实现)
-
- [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 虚函数重写的⼀些其他问题)
-
- 1.协变(Covariance)
-
- [1.1 结合协变代码讲解:](#1.1 结合协变代码讲解:)
- [1.2 协变完整带注释代码](#1.2 协变完整带注释代码)
- [2. 析构函数的虚函数重写(面试重点)](#2. 析构函数的虚函数重写(面试重点))
- 最重要实际意义(面试核心)
-
-
- [1. 析构代码逐行讲解](#1. 析构代码逐行讲解)
- 反面场景(坑)
- [2. 虚析构完整带注释代码](#2. 虚析构完整带注释代码)
- [3. 考试 / 面试小结](#3. 考试 / 面试小结)
-
- [2.1.6 override 和final关键字](#2.1.6 override 和final关键字)
-
- 1、override(检查是否成功重写虚函数)
-
- 问题背景
- [override 的作用](#override 的作用)
- [2. final(两个用途)](#2. final(两个用途))
-
- [用途 1:修饰虚函数 ------ 禁止派生类重写这个虚函数](#用途 1:修饰虚函数 —— 禁止派生类重写这个虚函数)
- [用途 2:修饰类 ------ 禁止这个类被继承](#用途 2:修饰类 —— 禁止这个类被继承)
- 总结
- 补充小对比
- [2.1.7 重载/重写/隐藏的对比](#2.1.7 重载/重写/隐藏的对比)
-
- [1. 重载(同一作用域)](#1. 重载(同一作用域))
- [2. 重写 / 覆盖(实现多态,继承体系 + virtual 虚函数)](#2. 重写 / 覆盖(实现多态,继承体系 + virtual 虚函数))
- [3. 隐藏(又叫遮蔽,继承体系,不满足重写条件)](#3. 隐藏(又叫遮蔽,继承体系,不满足重写条件))
- 总结对照表(方便记忆)
1. 多态的概念
同一个动作,交给不同对象去做,表现出不一样的效果,这就是多态(多种形态)
多态分成两大类:静态多态(编译时)、动态多态(运行时)
- 静态多态【编译阶段就确定好调用哪个函数】
代表:函数重载、模板
同一个函数名字,根据你传入不同参数,直接确定执行哪段代码
程序还没跑起来(编译的时候),编译器一看参数,就知道要用哪个方法。
举例子:
add(1,2) 两个数字相加
add("a","b") 两个字符串拼接
函数名都是 add,参数不一样,功能不一样。
代码编译的时候就分清调用哪个,所以叫静态 / 编译时多态。
- 动态多态【程序跑起来之后,才确定执行什么代码】(面向对象重点)
规则:同一个行为,传入不同对象,产生不同结果,程序运行的时候才决定用哪段逻辑
原文两个例子通俗翻译:
例子 1:买票
动作:【买票】
- 普通人对象执行买票 → 付全款
- 学生对象执行买票 → 半价优惠
- 军人对象执行买票 → 优先购票
动作名称不变都是 "买票",对象不一样,行为结果不一样。
例子 2:动物叫唤
统一动作:【叫】
- 猫对象调用【叫】 → 喵喵喵
- 狗对象调用【叫】 → 汪汪汪

静态多态 vs 动态多态最简单区分
✅ 静态多态:看参数区分逻辑(编译时定好)
✅ 动态多态:看对象区分逻辑(程序运行时才确定)
2. 多态的定义及实现
2.1 多态的构成条件
多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了 Person。Person对象买票全价,Student对象优惠买票。
2.1.1 实现多态还有两个必须重要条件:
- 条件 1:要用基类指针 / 基类引用去调用函数
基类相当于通用模板(比如:人、动物),派生类是具体种类(学生、猫、军人、狗)。
普通指针只能固定指向一种对象;
基类指针 / 引用很灵活:既能指向父类对象,也能指向子类对象。
举例子:
基类 = 动物
派生类 = 猫、狗
动物* p; // 基类指针
p = new 猫(); // 可以指向猫
p = new 狗(); // 也可以指向狗
如果不用基类指针,直接用猫指针只能指向猫,狗指针只能指向狗,没法统一用一套代码接收多种子类,自然谈不上多态。
简单理解:基类指针就是一个 "通用容器",能装下所有子类。
-
条件 2:调用的函数必须是虚函数 ,并且子类完成重写(覆盖)
-
基类里的函数前面加上
virtual,它就变成虚函数。 -
派生类写一个一模一样的函数(函数名、参数、返回值完全匹配),这就叫重写 / 覆盖。
没有虚函数会发生什么?
就算基类指针指向猫、指向狗,调用方法时只会死板执行基类里面的代码,不会自动调用猫、狗自己实现的版本,出不来多态效果!
虚函数作用:相当于给程序开了一个开关。程序运行的时候,不去死板看指针是什么类型,而是看指针真正指向的是什么对象,调用这个对象自己重写的函数。
结合动物例子串起来看
基类【动物】,里面写虚函数 virtual 叫()
子类【猫】重写 叫():喵喵喵
子类【狗】重写 叫():汪汪汪
动物* a = new 猫();
a->叫(); // 输出喵喵喵
a = new 狗();
a->叫(); // 输出汪汪汪
满足两条规则,多态生效!
以下是反面教材:
如果函数不加 virtual(不是虚函数)
基类指针哪怕指向猫,调用叫(),只会执行动物默认的叫声,不会调用猫的版本。
如果不用基类指针,直接用猫指针、狗指针各自调用
虽然也能正常叫唤,但是只是普通函数调用,不是多态,没法用统一的指针接收所有子类。

2.1.2 虚函数
类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。
class Person
{
public:
virtual void BuyTicket() { cout << "买票全价" << endl;}
};
2.1.3 虚函数的重写/覆盖
(1)定义
基类写了一个虚函数;
子类里面写一个函数:函数名一模一样、参数一模一样、返回值符合要求。
这就叫:子类重写(覆盖)了基类虚函数。
(2) 重点注意点
-
子类可以不写
virtual,依然能构成重写基类函数已经是虚函数,继承到子类后,这个函数天生自带虚函数身份。
哪怕子类函数前面省略
virtual,依旧算是重写,多态照样生效。 -
❗但是不推荐省略 virtual,代码可读性很差,规范开发都建议写上。
-
💡考试高频坑!
选择题经常故意把子类的
virtual删掉,迷惑你,很多人误以为没有 virtual 就不能重写、无法多态,这是易错点。
简单记住:虚属性是从基类继承下来的,子类可省略 virtual;但规范上一定要加上。
(3) 补充一个硬性前提
想要实现动态多态,必须满足重写;如果函数名字 / 参数不一样,那不叫重写,自然触发不了多态。
(4)结合两段代码讲解
案例 1:买票 Person & Student
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
基类Person:BuyTicket()带有virtual,是虚函数
子类Student:函数名BuyTicket、参数列表、返回值和基类完全一致 → 构成重写
void Func(Person* ptr) // 条件1:基类指针
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps); // 普通人类对象 → 全价买票
Func(&st); // 学生对象 → 打折买票
return 0;
}
✅ 两条动态多态条件全部满足:
① 使用基类指针调用函数
② 虚函数 + 子类完成重写
运行结果:
买票-全价
买票-打折
👉 关键点:指针永远都是Person*类型,但是程序运行时,看指针指向什么对象,就调用哪个类的函数,这就是多态。
拓展测试:把 Student 里面的 virtual 删掉
void BuyTicket() { cout << "买票-打折" << endl; }
依然构成重写,多态依旧生效!只是代码不规范,考试要认清这个情况。
案例 2:动物叫声 Animal Dog Cat
class Animal
{
public:
virtual void talk() const
{}
};
class Dog : public Animal
{
public:
virtual void talk() const
{
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal
{
public:
virtual void talk() const
{
std::cout << "(>^ω^<)喵" << std::endl;
}
};
基类Animal拥有虚函数talk();
Dog、Cat 中talk()函数名、参数、const 修饰、返回值完全匹配基类,构成重写。
void letsHear(const Animal& animal) // 条件1:基类引用(指针/引用二选一都可以)
{
animal.talk();
}
int main()
{
Cat cat;
Dog dog;
letsHear(cat);
letsHear(dog);
return 0;
}
✅ 使用基类引用(也是实现多态合法方式)+ 虚函数重写,满足条件。
运行结果:
(>^ω^<)喵
汪汪
再次区分:指针、引用都能实现多态;直接用父类对象接收子类(值传递切割)无法实现多态!
坑点总结
子类重写虚函数时,省略 virtual,依旧是重写,能够触发多态;
很多考生误以为子类必须写 virtual 才算重写,这是出题人最爱设置的陷阱;
但是工程开发规范:建议子类重写虚函数时主动写上 virtual(C++11 推荐再加 override 关键字);
如果函数名 / 参数不一样,不叫重写,叫函数隐藏,无法实现多态!
2.1.4 多态场景的⼀个选择题

各位大佬,请亮出您的答案!
答案:B: B->1
解释:
- 先看能不能触发多态
-
基类 A:
virtual void func(int val = 1) -
派生类 B:
void func(int val = 0)函数名、返回值、参数列表完全相同,
构成虚函数重写
(子类不写 virtual 依然有效)。
-
调用流程梳理
B* p = new B;
p->test(); -
test()这个函数定义在 A 类里面 :virtual void test(){ func(); } -
虽然 test 写在 A 中,但是 test 内部调用
func()。由于 func 是虚函数,会触发多态,执行 B 类重写的 func。
重点:最终跑的是
B::func(),不是 A::func ()。
- 最容易踩坑:默认参数编译期确定,不参与多态!
✅ 记住铁律:
虚函数是运行时确定调用哪个函数;函数默认参数值,是编译阶段就确定好的,不会跟着子类改变!
test() 属于 A 类的成员函数,编译的时候,编译器看到 func();(无传参),直接拿A 类 func 的默认参数 val=1。
运行时:执行的是 B 的 func 函数,但传入的参数值是编译期敲定的 1。
- 最终执行
运行 B::func(1)
打印输出:B->1
总结:
调用哪个函数(A::func 还是 B::func)运行时看虚函数;
参数默认值在编译时就定死,由调用处所在的类版本决定,不会动态跟随子类!
易错辨析
很多人会误以为:调用 B 的 func,就应该使用 B 的默认参数 0。❌错误!
默认参数不参与动态多态,编译时在 A 的 test 函数内部,func 不传参,直接绑定 A 的默认值 1。
拓展验证
如果代码写成:
A* p = new B();
p->func(); // 依然输出 B->1
同样原理!
对应选项: 选项 B: B->1 ✅
简易流程图
p->test() → 执行 A::test () → test 里面调用 func ()
→ 虚函数多态,运行找到 B::func
→ 参数编译阶段确定为 A 的默认值 1
→ 执行 B::func(1) → 输出 B->1
2.1.5 虚函数重写的⼀些其他问题
1.协变(Covariance)
正常虚函数重写要求:返回值必须一模一样。
协变是唯一的特例:
基类虚函数返回【基类指针 / 基类引用】
派生类重写的虚函数返回【派生类指针 / 派生类引用】
这种返回值不一样,但依然合法构成重写,就叫做协变。
⚠️ 硬性限制:
- 只允许指针、引用;普通对象不能协变
- 必须是「基类 ← 派生类」的继承关系
- 日常开发极少使用,了解概念即可。
1.1 结合协变代码讲解:
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()
{
cout << "买票-打折" << endl;
return nullptr;
}
};
基类虚函数返回 A*
子类重写函数返回 B*
B 公有继承 A,满足协变条件 → 合法重写,可以触发多态
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps); // 输出:买票-全价
Func(&st); // 输出:买票-打折
return 0;
}
运行逻辑:
Func 接收基类指针,调用 BuyTicket。因为构成合法重写,多态生效。
ptr 指向 Student 对象,自动执行 Student::BuyTicket。
1.2 协变完整带注释代码
#include <iostream>
using namespace std;
class A {};
// B公有继承A,满足协变的继承关系
class B : public A {};
class Person {
public:
// 基类虚函数:返回基类A的指针
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
// 协变:返回派生类B*,允许构成重写
virtual B* BuyTicket()
{
cout << "买票-打折" << endl;
return nullptr;
}
};
// 参数:基类指针,满足动态多态条件
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
2. 析构函数的虚函数重写(面试重点)
正常成员函数重写:函数名、参数必须完全相同。
析构函数是编译器特殊处理的例外!
- 源代码里基类析构
~A(),派生类析构~B()名字看起来不同 - 编译阶段,编译器把所有析构函数统一改名
destructor - 只要基类析构加 virtual,派生类析构自动构成重写,派生类析构写不写 virtual 都有效。
最重要实际意义(面试核心)
当用基类指针指向派生类对象 ,执行delete 指针;
- 如果基类析构不是虚函数 :只会调用基类析构,派生类析构不执行。派生类里面申请的内存无法释放 → 内存泄漏。
- 如果基类析构是虚函数:触发多态,先调用派生类析构,再调用基类析构,资源完整释放。
1. 析构代码逐行讲解
class A
{
public:
// 基类析构设置为virtual
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
// 自动重写基类虚析构,可省略virtual
~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; // 释放A对象,调用 ~A()
delete p2; // 基类析构是虚函数,触发多态
// 先调用 ~B() 释放_p数组,再调用 ~A()
return 0;
}
✅ 运行输出:
~A()
~B()->delete:0x...
~A()
反面场景(坑)
如果删掉virtual,变成~A(){}
delete p2; 只会执行~A(),不会执行~B ()
_p指向的堆数组没有释放,永久内存泄漏。
💡面试标准答案:
如果一个类可能被继承,最好把基类析构定义为虚析构函数,防止基类指针管理派生类对象时发生内存泄漏。
2. 虚析构完整带注释代码
#include <iostream>
using namespace std;
class A
{
public:
// 重点:基类析构加上virtual,成为虚析构函数
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
// 自动重写基类虚析构,virtual可写可不写,推荐写上增强可读性
~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;
}
3. 考试 / 面试小结
- 协变仅支持指针 / 引用,日常开发很少用,认识即可
- 析构函数名称源代码不同,编译器统一改名,满足虚函数重写条件
- 被继承的基类,析构函数建议加 virtual,避免基类指针 delete 派生类对象造成内存泄漏。
2.1.6 override 和final关键字
1、override(检查是否成功重写虚函数)
问题背景
虚函数重写要求很严:函数名、参数、返回值(协变除外)都要匹配。
一不小心写错函数名、参数,编译器不会报错 ,它只会认为:你在派生类新增了一个独立函数,没有完成重写。
等到运行程序,多态失效,结果不对,你还要花大量时间查 bug。
override 的作用
在派生类的虚函数后面写上 override
编译器会主动检查:这个函数能不能成功重写基类的虚函数。
- 满足重写规则:正常编译
- 写错名字 / 参数,无法构成重写:直接编译报错,提前发现错误。
示例:
class A
{
public:
virtual void func(){}
};
class B : public A
{
public:
// 正确:成功重写
void func() override{}
// void func(int x) override{} // 报错!参数不一样,无法重写
};
小建议:写重写的虚函数时,习惯性加上 override。
2. final(两个用途)
用途 1:修饰虚函数 ------ 禁止派生类重写这个虚函数
基类虚函数后面加 final,后续所有子类都不能重写它。
子类强行重写 → 编译报错。
class A
{
public:
virtual void func() final {}
};
class B : public A
{
public:
// void func(){} // 编译报错,func被final禁止重写
};
用途 2:修饰类 ------ 禁止这个类被继承
类名后面加 final,别的类不能继承它
class A final
{};
// class B:public A{}; // 报错,A不能被继承
总结
- override:告诉编译器「我打算重写虚函数,帮我检查有没有写错」,防止隐性 bug。
- final:上锁。修饰函数 = 不让子类重写;修饰类 = 不让别人继承这个类。
补充小对比
- override:只用在派生类的虚函数
- final:可以写在基类虚函数后,也可以写在类名后
2.1.7 重载/重写/隐藏的对比
如果追女生是每个少年(男生)心中第一件事喵,那么搞清楚这三个对比就是现在的当务之急,重中之重喵。(其实我想说这是第0件事,哈哈哈哈哈,因为计算机的世界是二进制的花园喵)

先一句话分清场景:
重载:同一个班级里;重写:父子班级 + 虚函数;隐藏:父子班级、但是达不到重写条件。
1. 重载(同一作用域)
条件:同一个范围,函数名字一样,参数不一样。
🌰例子:
班里同一个教室(同一个类 / 同一个作用域)有两位老师,都叫 "讲课"
-
讲课 (讲数学)
-
讲课 (讲语文)
名字都叫讲课,但是授课内容(参数)不同。
调用的时候看你传什么参数,自动匹配对应函数。
关键点:
全都在同一个教室(同一作用域),和继承没关系。
返回值随便,可以相同也可以不同,只看参数。
// 同一个类内部,属于重载
class Person
{
public:
void Talk(string subject);
void Talk(int age);
};
2. 重写 / 覆盖(实现多态,继承体系 + virtual 虚函数)
条件:父类、子类两个不同作用域;父类函数是 virtual 虚函数;函数名、参数一致(协变除外)。
🌰例子:
父亲开小店:virtual 买票 () → 全价票
儿子继承店铺,重新改造规则:买票 () → 学生打折票
规则名称一模一样,入参一样。
有人通过店铺招牌(父类指针)买票时,自动区分是谁经营店铺,执行对应的买票逻辑,这就是多态。
重点区别:
必须加 virtual!不加就不叫重写。
只有重写才能触发动态多态。
class Person
{
public:
virtual void BuyTicket() { cout << "全价票"; }
};
class Student : public Person
{
public:
void BuyTicket() override { cout << "打折票"; } // 重写
};
3. 隐藏(又叫遮蔽,继承体系,不满足重写条件)
规则:父子类,函数名相同,只要构不成重写,全部是隐藏。
两种常见情况:
1)父类没有写 virtual;
2)有 virtual,但是参数列表不一样;
🌰例子:
父亲店铺功能:买票 ()(无参数)
儿子店铺:买票 (int num)(带参数)
名字都叫买票,但是参数不一样。
如果你拿着儿子店铺的招牌调用买票 (),只会看到儿子自己的版本,父亲同名函数直接被藏起来了 。想要调用父类版本,必须手动指定 Person::BuyTicket()。
⚠️ 重要坑:
隐藏≠重写,不会触发多态!
额外补充:父子类同名成员变量,也属于隐藏!
父亲有个变量 int id;,子类也写 int id;,子类 id 会隐藏父类 id。
class Person
{
public:
virtual void BuyTicket() { cout << "全价票"; }
};
class Student : public Person
{
public:
void BuyTicket(int x) { cout << "打折票"; }
// 参数不一样!无法构成重写,属于【隐藏】
};
总结对照表(方便记忆)
- 重载:同一个房间,同名不同参
- 重写:父子房间、带 virtual、签名一致,实现多态
- 隐藏:父子房间、名字相同,但达不到重写标准;父类同名成员被遮蔽
又是美好的一天,各位观众姥爷们,若有什么不懂的地方,欢迎在评论区留言哦,我会给出相应解释的。如有什么疏忽的细节,请点出,我会及时纠正。感谢观看。喵喵