《C++ 多态》三大面向对象编程——多态:虚函数机制、重写规范与现代C++多态控制全概要

🔥个人主页:Cx330🌸
❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》
《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔
🌟心向往之行必能至
🎥Cx330🌸的简介:

目录
2.7.4 override / final在大型项目中的价值
前言:
在面向对象编程的三大核心特性中,多态是继继承之后的 “进阶能力”,它完美诠释了 “同一接口,不同实现” 的设计哲学。就像生活中 “播放” 这一行为,音乐播放器播放歌曲、视频播放器播放影片、幻灯片播放器展示画面 —— 接口一致但表现形式各异;在 C++ 中,多态让我们通过基类的统一接口,调用不同派生类的专属实现,无需修改调用逻辑就能适配多样化的业务需求。作为继承机制的延伸与升华,多态不仅解决了 “代码复用” 后的 “灵活扩展” 问题,更让面向对象系统具备了可维护性与可扩展性,成为构建复杂软件架构的核心技术之一

一、认识多态:面向对象编程的灵魂

1.1 多态的核心概念解析

实质:做同一个行为(调同一个函数),不同的对象完成不同的行为
1.2 联系实际:现实世界中的多态类比


二、多态的实现机制深度探索
2.1 多态的本质与构成必要条件

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

以【买票】为例

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

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

2.4 虚函数重写的最佳实践
class Animal { public: virtual void talk() const { } }; class Dog : public Animal { public: //重写实现,可以不加virtual virtual void talk() const { std::cout << "汪汪" << std::endl; } }; class Cat : public Animal { public: virtual void talk() const { std::cout << "(>^ω^<)喵" << std::endl; } }; //必须是指针或者引用 void letsHear(const Animal& animal) { animal.talk(); } int main() { Cat cat; Dog dog; letsHear(cat); letsHear(dog); return 0; }运行结果:

2.5 实战演练:腾讯多态笔试题精解
2.5.1 场景A:基础多态调用
以下程序输出结果是什么?()
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
2.5.2 内存模型与调用链路图解

2.5.3 场景B:复杂多态场景分析
上图中艾莉丝还展示了另一种考法,代码是这样的
class A { public: virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; } virtual void test() { func(); } }; class B : public A { public: //多态是:不加virtual重写是重写虚函数的实现部分 //相当于是基类的函数声明部分+派生类的函数实现部分 //即 virtual void func(int val = 1)+{ std::cout << "B->" << val << std::endl; } void func(int val = 0) { std::cout << "B->" << val << std::endl; } }; int main(int argc, char* argv[]) { B* p = new B; //多态调用 //p->test(); //普通调用 p->func(); return 0; }正确答案:D。这个的答案就是D了,
这里的func函数只有在多态的时候才会走重写的这个机制。
即派生类是由多态调用的时候,父类虚函数声明+子类实现构成这个虚函数。
而这里的这个——是个普通调用,所以是子类虚函数声明+子类实现构成的这个虚函数,因此选择D

2.5.4 多态调用的决定性因素

2.6 虚函数重写进阶专题

2.6.1 虚函数重写的常见问题

2.6.2 协变(Covariant)返回类型详解
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解一下即可
一句话总结:返回值类型可以有差异
// 协变 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; }运行结果:

2.6.3 派生类virtual关键字可选性分析

2.6.4 析构函数重写的必要性论证
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加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 //virtual ~B() ~B() { cout << "~B()->delete:" << _p << endl; delete _p; } protected: int* _p = new int[10]; }; // 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能 //构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。 //基类只要保证析构函数是虚函数,下面这下些场景就不会存在内存泄露 int main() { A* ptr1 = new B; delete ptr1; A* ptr2 = new A; delete ptr2; return 0; }只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能 构成多态,才能保证p1和p2指向的对象正确的调用析构函数
基类只要保障了析构函数是虚函数,下面场景就不会存在内存泄漏
运行结果:

解决方案:这里必须是多态调用,因为基类的析构函数是虚函数。
派生类的析构函数没有调用,就有可能造成内存泄漏
2.7 现代C++多态控制:override和final
2.7.1 override/final的设计哲学
从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写,override在派生类里面。如果不想让派生类重写这个虚函数,那么可以用final去修饰,final在基类里面,作用就是不让虚函数重写(父类不想被重写,可能是已经被重写过了)
2.7.2 override/final核心功能剖析

2.7.3 最佳实践指南
class Car { public: //virtual void Dirve()//函数名写错、参数写错等导致⽆法构成重写 virtual void Drive() { } //不想让派⽣类重写这个虚函数,那么可以⽤final去修饰 virtual void Drive() final { } }; class Benz :public Car { public: virtual void Drive() override { cout << "Benz-舒适" << endl; } }; int main() { return 0; }运行结果:

我们把override去掉
class Car { public: //virtual void Dirve()//函数名写错、参数写错等导致⽆法构成重写 virtual void Drive() { } //不想让派⽣类重写这个虚函数,那么可以⽤final去修饰 virtual void Drive() { } }; class Benz :public Car { public: virtual void Drive(){ cout << "Benz-舒适" << endl; } }; int main() { return 0; }运行结果:

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

完整代码示例
Test.cpp:
#include<iostream> using namespace std; //class Student; // //class Person //{ //public: // //友元关系不能被子类继承 // friend void Display(const Person& p, const Student& s); //protected: // string _name; // 姓名 //}; // //class Student : public Person //{ // friend void Display(const Person& p, const Student& s); //protected: // int _stuNum; // 学号 //}; //void Display(const Person& p, const Student& s) //{ // cout << p._name << endl; // cout << s._stuNum << endl; //} // //int main() //{ // Person p; // Student s; // // 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员 // // 解决⽅案:Display也变成Student 的友元即可 // Display(p, s); // // return 0; //} //class Person //{ //public: // string _name; // static int _count;//存放在静态区 //}; // //int Person::_count = 0; // //class Student : public Person //{ //protected: // int _stuNum; //}; // //int main() //{ // Person p; // Student s; // // 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的 // // 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份 // cout << &p._name << endl; // cout << &s._name << endl; // // 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的 // // 说明派⽣类和基类共⽤同⼀份静态成员 // cout << &p._count << endl; // cout << &s._count << endl; // // 公有的情况下,⽗派⽣类指定类域都可以访问静态成员 // cout << Person::_count << endl; // cout << Student::_count << endl; // // return 0; //} //菱形继承 //菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 //看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 //⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议 //设计出菱形继承这样的模型的 //class Person //{ //public: // string _name; // 姓名 //}; //// //class Student : virtual public Person //virtual虚拟继承在腰部 //{ //protected: // int _num; //学号 //}; //class Teacher : virtual public Person //{ //protected: // int _id; // 职⼯编号 //}; // //class Assistant : public Student, public Teacher //{ //protected: // string _majorCourse; // 主修课程 //}; // //int main() //{ // // 编译报错:error C2385: 对“_name”的访问不明确 // Assistant a; // a._name = "peter"; // // 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决 // a.Student::_name = "xxx"; // a.Teacher::_name = "yyy"; // // return 0; //} //class Person //{ //public: // Person(const char* name) // :_name(name) // { } // string _name; // 姓名 //}; // //class Student : virtual public Person //{ //public: // Student(const char* name, int num) // :Person(name) // , _num(num) // { // } //protected: // int _num; //学号 //}; // //class Teacher : virtual public Person //{ //public: // Teacher(const char* name, int id) // :Person(name) // , _id(id) // { // } //protected: // int _id; // 职⼯编号 //}; // //// 不要去玩菱形继承 //class Assistant : public Student, public Teacher //{ //public: // Assistant(const char* name1, const char* name2, const char* name3) // :Person(name3) // ,Student(name1, 1) // ,Teacher(name2, 2) // { // } //protected: // string _majorCourse; // 主修课程 //}; // //int main() //{ // // 思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个? // Assistant a("张三", "李四", "王五"); // return 0; //} // 继承和组合 // Tire(轮胎)和Car(⻋)更符合has-a的关系 // 轮胎类 //class Tire { //protected: // string _brand = "Michelin"; // size_t _size = 17; //}; // //// 汽车基类 //class Car { //protected: // string _colour = "白色"; // string _num = "陕ABIT00"; // Tire _tires[4]; // 使用数组更合适 //}; // //// 派生类 //class BMW : public Car { //public: // void Drive() { cout << "好开-操控" << endl; } //}; // //class Benz : public Car { //public: // void Drive() { cout << "好坐-舒适" << endl; } //}; // //// 正确的stack实现 //template<class T> //class Stack { //private: // vector<T> _v; // 组合关系 //public: // void push(const T& x) { _v.push_back(x); } // void pop() { // if (!_v.empty()) // _v.pop_back(); // } // T& top() { // if (!_v.empty()) // return _v.back(); // throw std::out_of_range("Stack is empty"); // } // bool empty() const { return _v.empty(); } // size_t size() const { return _v.size(); } //}; // //int main() { // BMW bmw; // bmw.Drive(); // // Stack<int> s; // s.push(1); // s.push(2); // cout << s.top() << endl; // 输出2 // // return 0; //} //实现多态的两个重要条件 // 必须是基类的指针或者引⽤调⽤虚函数 // 被调⽤的函数必须是虚函数,并且完成了虚函数重写 / 覆盖。 // 虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数 // (即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同), // 称派⽣类的虚函数重写了基类的虚函数 //class Person //{ //public: // virtual void BuyTicket()//虚函数 // { // cout << "买票-全价" << endl; // } //}; // //class Student :public Person //{ //public: // virtual void BuyTicket() // { // cout << "买票-打折" << endl; // } //}; // //void Func(Person* ptr) //{ // // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。 // ptr->BuyTicket(); //} //int main() //{ // Person ps; // Student st; // // Func(&ps); // Func(&st); // // return 0; //} //class Animal //{ //public: // virtual void talk() const // { } //}; // //class Dog : public Animal //{ //public: // //重写实现,可以不加virtual // virtual void talk() const // { // std::cout << "汪汪" << std::endl; // } //}; // //class Cat : public Animal //{ //public: // virtual void talk() const // { // std::cout << "(>^ω^<)喵" << std::endl; // } //}; // ////必须是指针或者引用 //void letsHear(const Animal& animal) //{ // animal.talk(); //} // //int main() //{ // Cat cat; // Dog dog; // // letsHear(cat); // letsHear(dog); // // return 0; //} //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; //} //class A //{ //public: // virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; } // virtual void test() { func(); } //}; //class B : public A //{ //public: // //多态是:不加virtual重写是重写虚函数的实现部分 // //相当于是基类的函数声明部分+派生类的函数实现部分 // //即 virtual void func(int val = 1)+{ std::cout << "B->" << val << std::endl; } // void func(int val = 0) { std::cout << "B->" << val << std::endl; } //}; //int main(int argc, char* argv[]) //{ // B* p = new B; // //多态调用 // //p->test(); // // //普通调用 // p->func(); // // return 0; //} //class A //{ //public: // virtual ~A() // { // cout << "~A()" << endl; // } //}; // //class B : public A { //public: // //建议加上virtual // //virtual ~B() // ~B() // { // cout << "~B()->delete:" << _p << endl; // delete _p; // } //protected: // int* _p = new int[10]; //}; // //// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能 ////构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。 // ////基类只要保证析构函数是虚函数,下面这下些场景就不会存在内存泄露 //int main() //{ // A* ptr1 = new B; // delete ptr1; // // A* ptr2 = new A; // delete ptr2; // // return 0; //} //override检查虚函数 class Car { public: //virtual void Dirve()//函数名写错、参数写错等导致⽆法构成重写 virtual void Drive() { } //不想让派⽣类重写这个虚函数,那么可以⽤final去修饰 //virtual void Drive() //{ } }; class Benz :public Car { public: virtual void Drive(){ cout << "Benz-舒适" << endl; } }; int main() { return 0; } //设计一个不能被继承的类 //class Base //class Base final //{ //public: // void func5() { cout << "Base::func5" << endl; } //protected: // int a = 1; //private: // //构造函数私有的类不能被继承 // Base() // {} //}; // //class Derive :Base //{ // //}; //int main() //{ // Derive d; // // return 0; //}结尾
往期回顾:
《C++ 继承》三大面向对象编程——继承:派生类构造、多继承、菱形虚拟继承概要
结语:多态是 C++ 面向对象编程的 “终极特性” 之一,它以继承为基础,通过虚函数实现 “同一接口,不同实现”,让代码兼具灵活性、扩展性与可读性。静态多态满足编译时的类型适配,无运行时开销;动态多态支持运行时的行为切换,契合开闭原则。掌握多态的实现原理、使用场景与避坑要点,不仅能解决实际开发中的复杂需求,更能帮助我们理解面向对象设计的核心思想 —— 将 “不变的接口” 与 “可变的实现” 分离,构建出高内聚、低耦合的软件系统