多态(上)

发布时间:2026/7/30 19:43:02

多态(上) 多态上文章目录多态上1. 多态的概念2. 多态的定义及实现2.1 多态的构成条件2.1.1 实现多态还有两个必须重要条件结合动物例子串起来看2.1.2 虚函数2.1.3 虚函数的重写/覆盖坑点总结2.1.4 多态场景的⼀个选择题2.1.5 虚函数重写的⼀些其他问题1.协变Covariance1.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. 多态的概念同一个动作交给不同对象去做表现出不一样的效果这就是多态多种形态多态分成两大类静态多态编译时、动态多态运行时静态多态【编译阶段就确定好调用哪个函数】代表函数重载、模板同一个函数名字根据你传入不同参数直接确定执行哪段代码程序还没跑起来编译的时候编译器一看参数就知道要用哪个方法。举例子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 Studentclass 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 Catclass 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 才算重写这是出题人最爱设置的陷阱但是工程开发规范建议子类重写虚函数时主动写上 virtualC11 推荐再加 override 关键字如果函数名 / 参数不一样不叫重写叫函数隐藏无法实现多态2.1.4 多态场景的⼀个选择题各位大佬请亮出您的答案答案B: B-1解释先看能不能触发多态基类 Avirtual void func(int val 1)派生类 Bvoid 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 的默认参数 val1。运行时执行的是 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-12.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父类没有写 virtual2有 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、签名一致实现多态隐藏父子房间、名字相同但达不到重写标准父类同名成员被遮蔽又是美好的一天各位观众姥爷们若有什么不懂的地方欢迎在评论区留言哦我会给出相应解释的。如有什么疏忽的细节请点出我会及时纠正。感谢观看。喵喵

相关新闻