当前位置: 首页 > news >正文

多态(上)

多态(上)

文章目录

  • 多态(上)
    • 1. 多态的概念
    • 2. 多态的定义及实现
      • 2.1 多态的构成条件
      • 2.1.1 实现多态还有两个必须重要条件:
      • 结合动物例子串起来看
      • 2.1.2 虚函数
      • 2.1.3 虚函数的重写/覆盖
        • 坑点总结
      • 2.1.4 多态场景的⼀个选择题
      • 2.1.5 虚函数重写的⼀些其他问题
        • 1.协变(Covariance)
          • 1.1 结合协变代码讲解:
          • 1.2 协变完整带注释代码
        • 2. 析构函数的虚函数重写(面试重点)
      • 最重要实际意义(面试核心)
          • 1. 析构代码逐行讲解
          • 反面场景(坑)
          • 2. 虚析构完整带注释代码
          • 3. 考试 / 面试小结
      • 2.1.6 override 和final关键字
        • 1、override(检查是否成功重写虚函数)
          • 问题背景
          • override 的作用
        • 2. final(两个用途)
          • 用途 1:修饰虚函数 —— 禁止派生类重写这个虚函数
          • 用途 2:修饰类 —— 禁止这个类被继承
        • 总结
        • 补充小对比
      • 2.1.7 重载/重写/隐藏的对比
        • 1. 重载(同一作用域)
        • 2. 重写 / 覆盖(实现多态,继承体系 + virtual 虚函数)
        • 3. 隐藏(又叫遮蔽,继承体系,不满足重写条件)
        • 总结对照表(方便记忆)

1. 多态的概念

同一个动作,交给不同对象去做,表现出不一样的效果,这就是多态(多种形态)

多态分成两大类:静态多态(编译时)、动态多态(运行时)

  1. 静态多态【编译阶段就确定好调用哪个函数】

代表:函数重载、模板

同一个函数名字,根据你传入不同参数,直接确定执行哪段代码

程序还没跑起来(编译的时候),编译器一看参数,就知道要用哪个方法。

举例子:

add(1,2)两个数字相加

add("a","b")两个字符串拼接

函数名都是 add,参数不一样,功能不一样。

代码编译的时候就分清调用哪个,所以叫静态 / 编译时多态

  1. 动态多态【程序跑起来之后,才确定执行什么代码】(面向对象重点)

规则:同一个行为,传入不同对象,产生不同结果,程序运行的时候才决定用哪段逻辑

原文两个例子通俗翻译:

例子 1:买票

动作:【买票】

  • 普通人对象执行买票 → 付全款
  • 学生对象执行买票 → 半价优惠
  • 军人对象执行买票 → 优先购票

动作名称不变都是 “买票”,对象不一样,行为结果不一样。

例子 2:动物叫唤

统一动作:【叫】

  • 猫对象调用【叫】 → 喵喵喵
  • 狗对象调用【叫】 → 汪汪汪

静态多态 vs 动态多态最简单区分

✅ 静态多态:看参数区分逻辑(编译时定好)

✅ 动态多态:看对象区分逻辑(程序运行时才确定)

2. 多态的定义及实现

2.1 多态的构成条件

多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了 Person。Person对象买票全价,Student对象优惠买票。

2.1.1 实现多态还有两个必须重要条件:

  1. 条件 1:要用基类指针 / 基类引用去调用函数

基类相当于通用模板(比如:人、动物),派生类是具体种类(学生、猫、军人、狗)。

普通指针只能固定指向一种对象;

基类指针 / 引用很灵活:既能指向父类对象,也能指向子类对象。

举例子:

基类 = 动物

派生类 = 猫、狗

动物* p; // 基类指针 p = new 猫(); // 可以指向猫 p = new 狗(); // 也可以指向狗

如果不用基类指针,直接用猫指针只能指向猫,狗指针只能指向狗,没法统一用一套代码接收多种子类,自然谈不上多态。

简单理解:基类指针就是一个 “通用容器”,能装下所有子类。

  1. 条件 2:调用的函数必须是虚函数,并且子类完成重写(覆盖)

  2. 基类里的函数前面加上virtual,它就变成虚函数

  3. 派生类写一个一模一样的函数(函数名、参数、返回值完全匹配),这就叫重写 / 覆盖。

没有虚函数会发生什么?

就算基类指针指向猫、指向狗,调用方法时只会死板执行基类里面的代码,不会自动调用猫、狗自己实现的版本,出不来多态效果!

虚函数作用:相当于给程序开了一个开关。程序运行的时候,不去死板看指针是什么类型,而是看指针真正指向的是什么对象,调用这个对象自己重写的函数。

结合动物例子串起来看

基类【动物】,里面写虚函数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) 重点注意点

  1. 子类可以不写virtual,依然能构成重写

    基类函数已经是虚函数,继承到子类后,这个函数天生自带虚函数身份。

    哪怕子类函数前面省略virtual,依旧算是重写,多态照样生效。

  2. ❗但是不推荐省略 virtual,代码可读性很差,规范开发都建议写上。

  3. 💡考试高频坑!

    选择题经常故意把子类的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; } };

基类PersonBuyTicket()带有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

解释:

  1. 先看能不能触发多态
  • 基类 A:virtual void func(int val = 1)

  • 派生类 B:

    void func(int val = 0)

    函数名、返回值、参数列表完全相同,

    构成虚函数重写

    (子类不写 virtual 依然有效)。

  1. 调用流程梳理
B* p = new B; p->test();
  1. test()这个函数定义在 A 类里面virtual void test(){ func(); }
  2. 虽然 test 写在 A 中,但是 test 内部调用func()。由于 func 是虚函数,会触发多态,执行 B 类重写的 func

重点:最终跑的是B::func(),不是 A::func ()。

  1. 最容易踩坑:默认参数编译期确定,不参与多态!

✅ 记住铁律:

虚函数是运行时确定调用哪个函数;函数默认参数值,是编译阶段就确定好的,不会跟着子类改变!

test()属于 A 类的成员函数,编译的时候,编译器看到func();(无传参),直接拿A 类 func 的默认参数 val=1

运行时:执行的是 B 的 func 函数,但传入的参数值是编译期敲定的1

  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. 只允许指针、引用;普通对象不能协变
  2. 必须是「基类 ← 派生类」的继承关系
  3. 日常开发极少使用,了解概念即可。
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. 析构函数的虚函数重写(面试重点)

正常成员函数重写:函数名、参数必须完全相同。

析构函数是编译器特殊处理的例外!

  1. 源代码里基类析构~A(),派生类析构~B()名字看起来不同
  2. 编译阶段,编译器把所有析构函数统一改名destructor
  3. 只要基类析构加 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. 考试 / 面试小结
  1. 协变仅支持指针 / 引用,日常开发很少用,认识即可
  2. 析构函数名称源代码不同,编译器统一改名,满足虚函数重写条件
  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不能被继承
总结
  1. override:告诉编译器「我打算重写虚函数,帮我检查有没有写错」,防止隐性 bug。
  2. 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 << "打折票"; } // 参数不一样!无法构成重写,属于【隐藏】 };
总结对照表(方便记忆)
  1. 重载:同一个房间,同名不同参
  2. 重写:父子房间、带 virtual、签名一致,实现多态
  3. 隐藏:父子房间、名字相同,但达不到重写标准;父类同名成员被遮蔽

又是美好的一天,各位观众姥爷们,若有什么不懂的地方,欢迎在评论区留言哦,我会给出相应解释的。如有什么疏忽的细节,请点出,我会及时纠正。感谢观看。喵喵

http://www.jsqmd.com/news/1295681/

相关文章:

  • 游乐园检票板厂家:破解验票痛点的6S全域验票优化方法论 - 汇聚至此
  • 1000+ Claude技能库:终极AI工作流自动化工具集
  • AhabAssistantLimbusCompany:边狱巴士PC端自动化助手完整指南
  • 5个理由告诉你为什么Gopeed是现代下载管理的终极解决方案
  • 3步掌握OBS Studio专业色彩校正:从基础调色到电影级LUT应用完整指南
  • 新手怎么卖包包不亏?2026 济南奢侈品变现渠道避坑技巧 - 全国二奢机构参考
  • 【AI预算编制辅助终极指南】:20年财务系统架构师亲授,3步实现预算准确率提升47%的实战方法
  • 跨境电商实战:选品、物流与运营避坑指南
  • GEO服务商怎么选?从4个维度判断专业能力 - 生活动态圈
  • 直流与交流UPS电源:技术原理与应用场景解析
  • 半透明物体排序与顺序无关透明:工程上的取舍
  • 单片机计算机毕设之基于 STM32F103 的人体心率血氧检测装置开发 基于嵌入式单片机的健康预警终端设计与实现(013201)
  • 深度解析检票板厂家材质区别:不同防护材质的技术特点与应用场景 - 汇聚至此
  • SpringBoot 整合 Spring Retry——优雅实现接口重试
  • Go容器化部署优化:Go Practical Tips中的GOMAXPROCS调整与性能调优
  • SpringBoot+Vue汽车销售管理系统开发实践
  • 5分钟快速上手:免费跨平台图表工具draw.io桌面版完整指南
  • 高产率生物反应器价格解析与工艺装备一体化价值 - 生活动态圈
  • Pixelle-Video:基于ComfyUI架构的AI短视频生成解决方案
  • 苹果开学返校季装备焕新全攻略!2026 国补叠加京东学生认证教育优惠,手机、平板、电脑三大件折上折,学生党一套操作最高多省上千元! - 生活动态圈
  • 2026年中学生单词学习软件选型指南:3步避开三大坑,用对工具效率翻倍
  • PersonaLive终极指南:5分钟打造你的实时人像动画虚拟主播
  • 2026年 乙烯基/乙烯基重防腐厂家推荐榜单:环保耐蚀与高性能防腐蚀涂装源头工厂精选 - 优企名品
  • 区分流通梯队!广州香奈儿鞋服高价变现实操指南 - 全国二奢机构参考
  • SpringBoot集成Kafka实战:从核心配置到生产级调优
  • 潮玩抽奖小程序架构设计与高并发优化实践
  • 高性能容器化Android系统架构揭秘:Waydroid实现原理深度解析
  • 从流量收割到用户价值经营:营销推广的本质演变
  • 算法面试——二叉树:最大深度、验证 BST、层序遍历
  • 海关AEO高级认证实战信息系统安全要求达标全路径