
🔥艾莉丝努力练剑:个人主页
❄专栏传送门:《C语言》、《数据结构与算法》、C/C++干货分享&学习过程记录、Linux操作系统编程详解、笔试/面试常见算法:从基础到进阶
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬艾莉丝的简介:

🎬艾莉丝的C++专栏简介:

目录
C++的两个参考文档
1 ~> 认识多态:面向对象编程的灵魂
1.1 多态的核心概念解析
1.2 联系实际:现实世界中的多态类比
2 ~> 多态的实现机制深度探索
2.1 多态的本质与构成必要条件
2.1.1 多态的科学定义
2.1.2 实现多态的双重关键条件
2.2 虚函数:多态的基石
2.3 虚函数重写(覆盖)详解
2.4 虚函数重写的最佳实践
2.5 实战演练:腾讯多态笔试题精解
2.5.1 场景A:基础多态调用
2.5.2 内存模型与调用链路图解
2.5.3 场景B:复杂多态场景分析
2.5.4 多态调用的决定性因素
2.6 虚函数重写进阶专题
2.6.1 虚函数重写的常见问题
2.6.2 协变(Covariant)返回类型详解
2.6.3 派生类virtual关键字可选性分析
2.6.4 析构函数重写的必要性论证
2.6.5 虚函数定义权限全解析:哪些函数可以被定义成虚函数,哪些不能?
2.6.6 多态虚函数重写知识体系思维导图
2.7 现代C++多态控制:override和final
2.7.1 override/final的设计哲学
2.7.2 override/final核心功能剖析
2.7.3 最佳实践指南
2.7.4 override / final在大型项目中的价值
2.8 三大概念终极对比:重载vs重写vs隐藏
完整代码示例与实践演示
Test.cpp:
结尾
老朋友(非官方文档):cplusplus 官方文档(同步更新):cppreference



做同一个行为(调同一个函数),不同的对象完成不同的行为。




多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票。
(1)必须是基类的指针或者引用调用虚函数; (2)被调用的函数必须是虚函数,并且完成了虚函数重写 / 覆盖。
其中,虚函数重写这里注意一下:派生类中有一个跟基类完全相同的虚函数,两者有“三同”——

以上面的【买票】为例——

说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。
类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。
这里需要注意两点:
(1)第一,非成员函数不能加virtual修饰,全局、静态加了会报错; (2)第二,虚函数前面加的也是virtual,但是和虚继承的virtual没有任何关系!!!
下面这个是多继承的virtual——

class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};虚函数的重写(覆盖)的基本概念——
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意挖这样一个坑,让你判断是否构成多态。
重写:重写实现。

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; }
};
void Func(Person* ptr)
{
// 多态调用
ptr->BuyTicket();
}
class Animal
{
public:
virtual void talk() const
{
std::cout << "吱吱吱" << std::endl;
}
};
class Dog :public Animal
{
public:
//重写实现
void talk() const
{
std::cout << "汪汪汪" << std::endl;
}
};
class Cat : public Animal
{
public:
// 重写实现
void talk() const
{
std::cout << "(>^ω^<)喵呜" << std::endl;
}
};
void letsHear(Animal& animal)
{
animal.talk();
}
//void letsHear(Animal animal)
//{
// animal.talk();
//}
//// 如果没有加&,打印结果如下:
//// 买票-全价
//// 买票 - 打折
//// 吱吱吱
//// 吱吱吱
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
Cat cat;
Dog dog;
letsHear(cat);
letsHear(dog);
// 打印结果
//买票 - 全价
//买票 - 打折
//(>^ω^<)喵呜
//汪汪汪
return 0;
}
这里可以看到虽然都是Person指针Ptr在调用BuyTicket,但是跟ptr没关系,而是由ptr指向的对象决定的。
运行一下——

以下程序输出结果是什么()
A. A->0 B. B->1 C:A->1
D. B->0 E:编译出错 F. 以上都不正确
// 腾讯笔试题目
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。您选对了吗?
很多人可能会在C、D之间犹豫,这里答案其实是B。
如下图所示,这里是基类的声明+派生类实现构成这个虚函数,所以是基类A的缺省参数,B->1。

上图中艾莉丝还展示了另一种考法,代码是这样的——
// 腾讯笔试题目
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->func();
return 0;
}其实就是在考察你,在这个时候选什么——

正确答案:D。这个的答案就是D了,
这里的func函数只有在多态的时候才会走重写的这个机制。
即派生类是由多态调用的时候,父类虚函数声明+子类实现构成这个虚函数。
而这里的这个——

是个普通调用,所以是子类虚函数声明+子类实现构成的这个虚函数,因此选择D。



派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解一下即可。
一句话总结:返回值类型可以有差异。
// 协变
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;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}运行一下——


基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。
destructor这个话题博主之前在【继承】就已经聊过了,直接放图,大家再回顾一下——

观察下面的代码,我们可以看到——如果~A( )不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B( )中在释放资源(注意:这个问题面试中经常考察,大家一定要结合类似下面的样例才能讲清楚,为什么基类中的析构函数建议设计为虚函数)——

// 析构函数
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
//virtual ~B();
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
// 基类只要保障了析构函数是虚函数,下面场景就不会存在内存泄漏
int main()
{
//A a;
//B b;
A* ptr1 = new B;
delete ptr1;
A* ptr2 = new A;
delete ptr2;
// 打印结果
//~B()->delete:000001E6AD3EB980
//~A()
//~A()
return 0;
}只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能 构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
基类只要保障了析构函数是虚函数,下面场景就不会存在内存泄漏。
运行一下——

大家可以结合下面的【博主手记】理解一下——

ptr->~B();
operator delete(ptr);解决方案:这里必须是多态调用,因为基类的析构函数是虚函数。
派生类的析构函数没有调用,就有可能造成内存泄漏。


在【继承】中,艾莉丝就介绍了final这个关键字,当时艾莉丝做了个小小的“预告”,今天要解密了!接下来我们就来介绍这两个关键字:override和final。

从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写,override在派生类里面。如果不想让派生类重写这个虚函数,那么可以用final去修饰,final在基类里面,作用就是不让虚函数重写(父类不想被重写,可能是已经被重写过了)。

// override和final
class Car
{
public:
/*virtual void Dirve()*/
virtual void Dirve() final // 不想被重写
{ }
};
class Benz : public Car // 奔驰和汽车
{
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}运行一下,报错了——

我们把override去掉,像这样——
// override和final
class Car
{
public:
/*virtual void Dirve()*/
virtual void Dirve() final // 不想被重写
{ }
};
class Benz : public Car // 奔驰和汽车
{
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}再运行一下——

去掉final,如下所示——
// override和final
class Car
{
public:
/*virtual void Dirve()*/
virtual void Dirve()
{ }
};
class Benz : public Car // 奔驰和汽车
{
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}再运行一下,没有任何报错——

正是因为C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,所以像上面的去掉final关键词或者去掉override这两个实验就验证了“这种错误在编译期间是不会报出的”,我们加这两个关键词的目的就在这里,通过这三个小实践,相信聪明的uu们一定了解了override和final的重要性。
重载 / 重写 / 隐藏三个概念的对比:一张图搞定——

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//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; }
//};
//
//void Func(Person* ptr)
//{
// // 多态调用
// ptr->BuyTicket();
//}
//
//class Animal
//{
//public:
// virtual void talk() const
// {
// std::cout << "吱吱吱" << std::endl;
// }
//};
//
//class Dog :public Animal
//{
//public:
// //重写实现
// void talk() const
// {
// std::cout << "汪汪汪" << std::endl;
// }
//};
//
//class Cat : public Animal
//{
//public:
// // 重写实现
// void talk() const
// {
// std::cout << "(>^ω^<)喵呜" << std::endl;
// }
//};
//
//void letsHear(Animal& animal)
//{
// animal.talk();
//}
//
////void letsHear(Animal animal)
////{
//// animal.talk();
////}
////// 如果没有加&,打印结果如下:
////// 买票-全价
////// 买票 - 打折
////// 吱吱吱
////// 吱吱吱
//
//int main()
//{
// Person ps;
// Student st;
// Func(&ps);
// Func(&st);
//
// Cat cat;
// Dog dog;
// letsHear(cat);
// letsHear(dog);
//
// // 打印结果
// //买票 - 全价
// //买票 - 打折
// //(>^ω^<)喵呜
// //汪汪汪
//
// return 0;
//}
//// 腾讯笔试题目
//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();
// // 结果:B->1
//
// B* p = new B;
// p->func();
// // 结果:B->0
//
// return 0;
//}
//// 析构
//class A
//{
//public:
// virtual ~A()
// {
// cout << "~A()" << endl;
// }
//};
//
//class B : public A
//{
//public:
// //virtual ~B();
// ~B()
// {
// cout << "~B()->delete:" << _p << endl;
// delete _p;
// }
//protected:
// int* _p = new int[10];
//};
//
//// 基类只要保障了析构函数是虚函数,下面场景就不会存在内存泄漏
//int main()
//{
// //A a;
// //B b;
// A* ptr1 = new B;
// delete ptr1;
//
// A* ptr2 = new A;
// delete ptr2;
// // 打印结果
// //~B()->delete:000001E6AD3EB980
// //~A()
// //~A()
//
// return 0;
//}
class Car
{
public:
/*virtual void Dirve()*/
virtual void Dirve() final // 不想被重写
{ }
};
class Benz : public Car // 奔驰和汽车
{
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}往期回顾:
【C++:继承】C++面向对象继承全面解析:派生类构造、多继承、菱形虚拟继承与设计模式实践
结语:都看到这里啦!那请大佬不要忘记给博主来个“一键四连”哦!
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა