(上))
多態(tài)上文章目錄多態(tài)上1. 多態(tài)的概念2. 多態(tài)的定義及實(shí)現(xiàn)2.1 多態(tài)的構(gòu)成條件2.1.1 實(shí)現(xiàn)多態(tài)還有兩個必須重要條件結(jié)合動物例子串起來看2.1.2 虛函數(shù)2.1.3 虛函數(shù)的重寫/覆蓋坑點(diǎn)總結(jié)2.1.4 多態(tài)場景的?個選擇題2.1.5 虛函數(shù)重寫的?些其他問題1.協(xié)變Covariance1.1 結(jié)合協(xié)變代碼講解:1.2 協(xié)變完整帶注釋代碼2. 析構(gòu)函數(shù)的虛函數(shù)重寫面試重點(diǎn)最重要實(shí)際意義面試核心1. 析構(gòu)代碼逐行講解反面場景坑2. 虛析構(gòu)完整帶注釋代碼3. 考試 / 面試小結(jié)2.1.6 override 和final關(guān)鍵字1、override檢查是否成功重寫虛函數(shù)問題背景override 的作用2. final兩個用途用途 1修飾虛函數(shù) —— 禁止派生類重寫這個虛函數(shù)用途 2修飾類 —— 禁止這個類被繼承總結(jié)補(bǔ)充小對比2.1.7 重載/重寫/隱藏的對比1. 重載同一作用域2. 重寫 / 覆蓋實(shí)現(xiàn)多態(tài)繼承體系 virtual 虛函數(shù)3. 隱藏又叫遮蔽繼承體系不滿足重寫條件總結(jié)對照表方便記憶1. 多態(tài)的概念同一個動作交給不同對象去做表現(xiàn)出不一樣的效果這就是多態(tài)多種形態(tài)多態(tài)分成兩大類靜態(tài)多態(tài)編譯時、動態(tài)多態(tài)運(yùn)行時靜態(tài)多態(tài)【編譯階段就確定好調(diào)用哪個函數(shù)】代表函數(shù)重載、模板同一個函數(shù)名字根據(jù)你傳入不同參數(shù)直接確定執(zhí)行哪段代碼程序還沒跑起來編譯的時候編譯器一看參數(shù)就知道要用哪個方法。舉例子add(1,2)兩個數(shù)字相加add(a,b)兩個字符串拼接函數(shù)名都是 add參數(shù)不一樣功能不一樣。代碼編譯的時候就分清調(diào)用哪個所以叫靜態(tài) / 編譯時多態(tài)。動態(tài)多態(tài)【程序跑起來之后才確定執(zhí)行什么代碼】面向?qū)ο笾攸c(diǎn)規(guī)則同一個行為傳入不同對象產(chǎn)生不同結(jié)果程序運(yùn)行的時候才決定用哪段邏輯原文兩個例子通俗翻譯例子 1買票動作【買票】普通人對象執(zhí)行買票 → 付全款學(xué)生對象執(zhí)行買票 → 半價優(yōu)惠軍人對象執(zhí)行買票 → 優(yōu)先購票動作名稱不變都是 “買票”對象不一樣行為結(jié)果不一樣。例子 2動物叫喚統(tǒng)一動作【叫】貓對象調(diào)用【叫】 → 喵喵喵狗對象調(diào)用【叫】 → 汪汪汪靜態(tài)多態(tài) vs 動態(tài)多態(tài)最簡單區(qū)分? 靜態(tài)多態(tài)看參數(shù)區(qū)分邏輯編譯時定好? 動態(tài)多態(tài)看對象區(qū)分邏輯程序運(yùn)行時才確定2. 多態(tài)的定義及實(shí)現(xiàn)2.1 多態(tài)的構(gòu)成條件多態(tài)是?個繼承關(guān)系的下的類對象去調(diào)?同?函數(shù)產(chǎn)?了不同的?為。?如Student繼承了 Person。Person對象買票全價Student對象優(yōu)惠買票。2.1.1 實(shí)現(xiàn)多態(tài)還有兩個必須重要條件條件 1要用基類指針 / 基類引用去調(diào)用函數(shù)基類相當(dāng)于通用模板比如人、動物派生類是具體種類學(xué)生、貓、軍人、狗。普通指針只能固定指向一種對象基類指針 / 引用很靈活既能指向父類對象也能指向子類對象。舉例子基類 動物派生類 貓、狗動物* p; // 基類指針 p new 貓(); // 可以指向貓 p new 狗(); // 也可以指向狗如果不用基類指針直接用貓指針只能指向貓狗指針只能指向狗沒法統(tǒng)一用一套代碼接收多種子類自然談不上多態(tài)。簡單理解基類指針就是一個 “通用容器”能裝下所有子類。條件 2調(diào)用的函數(shù)必須是虛函數(shù)并且子類完成重寫覆蓋基類里的函數(shù)前面加上virtual它就變成虛函數(shù)。派生類寫一個一模一樣的函數(shù)函數(shù)名、參數(shù)、返回值完全匹配這就叫重寫 / 覆蓋。沒有虛函數(shù)會發(fā)生什么就算基類指針指向貓、指向狗調(diào)用方法時只會死板執(zhí)行基類里面的代碼不會自動調(diào)用貓、狗自己實(shí)現(xiàn)的版本出不來多態(tài)效果虛函數(shù)作用相當(dāng)于給程序開了一個開關(guān)。程序運(yùn)行的時候不去死板看指針是什么類型而是看指針真正指向的是什么對象調(diào)用這個對象自己重寫的函數(shù)。結(jié)合動物例子串起來看基類【動物】里面寫虛函數(shù)virtual 叫()子類【貓】重寫叫()喵喵喵子類【狗】重寫叫()汪汪汪動物* a new 貓(); a-叫(); // 輸出喵喵喵 a new 狗(); a-叫(); // 輸出汪汪汪滿足兩條規(guī)則多態(tài)生效以下是反面教材如果函數(shù)不加 virtual不是虛函數(shù)基類指針哪怕指向貓調(diào)用叫()只會執(zhí)行動物默認(rèn)的叫聲不會調(diào)用貓的版本。如果不用基類指針直接用貓指針、狗指針各自調(diào)用雖然也能正常叫喚但是只是普通函數(shù)調(diào)用不是多態(tài)沒法用統(tǒng)一的指針接收所有子類。2.1.2 虛函數(shù)類成員函數(shù)前?加virtual修飾那么這個成員函數(shù)被稱為虛函數(shù)。注意?成員函數(shù)不能加virtual修飾。class Person { public: virtual void BuyTicket() { cout 買票全價 endl;} };2.1.3 虛函數(shù)的重寫/覆蓋(1)定義基類寫了一個虛函數(shù)子類里面寫一個函數(shù)函數(shù)名一模一樣、參數(shù)一模一樣、返回值符合要求。這就叫子類重寫覆蓋了基類虛函數(shù)。(2) 重點(diǎn)注意點(diǎn)子類可以不寫virtual依然能構(gòu)成重寫基類函數(shù)已經(jīng)是虛函數(shù)繼承到子類后這個函數(shù)天生自帶虛函數(shù)身份。哪怕子類函數(shù)前面省略virtual依舊算是重寫多態(tài)照樣生效。?但是不推薦省略 virtual代碼可讀性很差規(guī)范開發(fā)都建議寫上??荚嚫哳l坑選擇題經(jīng)常故意把子類的virtual刪掉迷惑你很多人誤以為沒有 virtual 就不能重寫、無法多態(tài)這是易錯點(diǎn)。簡單記住虛屬性是從基類繼承下來的子類可省略 virtual但規(guī)范上一定要加上。(3) 補(bǔ)充一個硬性前提想要實(shí)現(xiàn)動態(tài)多態(tài)必須滿足重寫如果函數(shù)名字 / 參數(shù)不一樣那不叫重寫自然觸發(fā)不了多態(tài)。4結(jié)合兩段代碼講解案例 1買票 Person Studentclass Person { public: virtual void BuyTicket() { cout 買票-全價 endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout 買票-打折 endl; } };基類PersonBuyTicket()帶有virtual是虛函數(shù)子類Student函數(shù)名BuyTicket、參數(shù)列表、返回值和基類完全一致 →構(gòu)成重寫void Func(Person* ptr) // 條件1基類指針 { ptr-BuyTicket(); } int main() { Person ps; Student st; Func(ps); // 普通人類對象 → 全價買票 Func(st); // 學(xué)生對象 → 打折買票 return 0; }? 兩條動態(tài)多態(tài)條件全部滿足① 使用基類指針調(diào)用函數(shù)② 虛函數(shù) 子類完成重寫運(yùn)行結(jié)果買票-全價 買票-打折 關(guān)鍵點(diǎn)指針永遠(yuǎn)都是Person*類型但是程序運(yùn)行時看指針指向什么對象就調(diào)用哪個類的函數(shù)這就是多態(tài)。拓展測試把 Student 里面的 virtual 刪掉void BuyTicket() { cout 買票-打折 endl; }依然構(gòu)成重寫多態(tài)依舊生效只是代碼不規(guī)范考試要認(rèn)清這個情況。案例 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擁有虛函數(shù)talk()Dog、Cat 中talk()函數(shù)名、參數(shù)、const 修飾、返回值完全匹配基類構(gòu)成重寫。void letsHear(const Animal animal) // 條件1基類引用指針/引用二選一都可以 { animal.talk(); } int main() { Cat cat; Dog dog; letsHear(cat); letsHear(dog); return 0; }? 使用基類引用也是實(shí)現(xiàn)多態(tài)合法方式 虛函數(shù)重寫滿足條件。運(yùn)行結(jié)果(^ω^)喵 汪汪再次區(qū)分指針、引用都能實(shí)現(xiàn)多態(tài)直接用父類對象接收子類值傳遞切割無法實(shí)現(xiàn)多態(tài)坑點(diǎn)總結(jié)子類重寫虛函數(shù)時省略 virtual依舊是重寫能夠觸發(fā)多態(tài)很多考生誤以為子類必須寫 virtual 才算重寫這是出題人最愛設(shè)置的陷阱但是工程開發(fā)規(guī)范建議子類重寫虛函數(shù)時主動寫上 virtualC11 推薦再加 override 關(guān)鍵字如果函數(shù)名 / 參數(shù)不一樣不叫重寫叫函數(shù)隱藏?zé)o法實(shí)現(xiàn)多態(tài)2.1.4 多態(tài)場景的?個選擇題各位大佬請亮出您的答案答案B: B-1解釋先看能不能觸發(fā)多態(tài)基類 Avirtual void func(int val 1)派生類 Bvoid func(int val 0)函數(shù)名、返回值、參數(shù)列表完全相同構(gòu)成虛函數(shù)重寫子類不寫 virtual 依然有效。調(diào)用流程梳理B* p new B; p-test();test()這個函數(shù)定義在 A 類里面virtual void test(){ func(); }雖然 test 寫在 A 中但是 test 內(nèi)部調(diào)用func()。由于 func 是虛函數(shù)會觸發(fā)多態(tài)執(zhí)行 B 類重寫的 func。重點(diǎn)最終跑的是B::func()不是 A::func ()。最容易踩坑默認(rèn)參數(shù)編譯期確定不參與多態(tài)? 記住鐵律虛函數(shù)是運(yùn)行時確定調(diào)用哪個函數(shù)函數(shù)默認(rèn)參數(shù)值是編譯階段就確定好的不會跟著子類改變test()屬于 A 類的成員函數(shù)編譯的時候編譯器看到func();無傳參直接拿A 類 func 的默認(rèn)參數(shù) val1。運(yùn)行時執(zhí)行的是 B 的 func 函數(shù)但傳入的參數(shù)值是編譯期敲定的1。最終執(zhí)行運(yùn)行B::func(1)打印輸出B-1總結(jié)調(diào)用哪個函數(shù)A::func 還是 B::func運(yùn)行時看虛函數(shù)參數(shù)默認(rèn)值在編譯時就定死由調(diào)用處所在的類版本決定不會動態(tài)跟隨子類易錯辨析很多人會誤以為調(diào)用 B 的 func就應(yīng)該使用 B 的默認(rèn)參數(shù) 0。?錯誤默認(rèn)參數(shù)不參與動態(tài)多態(tài)編譯時在 A 的 test 函數(shù)內(nèi)部func 不傳參直接綁定 A 的默認(rèn)值 1。拓展驗(yàn)證如果代碼寫成A* p new B(); p-func(); // 依然輸出 B-1同樣原理對應(yīng)選項(xiàng): 選項(xiàng)B: B-1?簡易流程圖p-test()→ 執(zhí)行 A::test () → test 里面調(diào)用 func ()→ 虛函數(shù)多態(tài)運(yùn)行找到 B::func→ 參數(shù)編譯階段確定為 A 的默認(rèn)值1→ 執(zhí)行B::func(1)→ 輸出B-12.1.5 虛函數(shù)重寫的?些其他問題1.協(xié)變Covariance正常虛函數(shù)重寫要求返回值必須一模一樣。協(xié)變是唯一的特例基類虛函數(shù)返回【基類指針 / 基類引用】派生類重寫的虛函數(shù)返回【派生類指針 / 派生類引用】這種返回值不一樣但依然合法構(gòu)成重寫就叫做協(xié)變。?? 硬性限制只允許指針、引用普通對象不能協(xié)變必須是「基類 ← 派生類」的繼承關(guān)系日常開發(fā)極少使用了解概念即可。1.1 結(jié)合協(xié)變代碼講解: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; } };基類虛函數(shù)返回A*子類重寫函數(shù)返回B*B 公有繼承 A滿足協(xié)變條件 →合法重寫可以觸發(fā)多態(tài)void Func(Person* ptr) { ptr-BuyTicket(); } int main() { Person ps; Student st; Func(ps); // 輸出買票-全價 Func(st); // 輸出買票-打折 return 0; }運(yùn)行邏輯Func 接收基類指針調(diào)用 BuyTicket。因?yàn)闃?gòu)成合法重寫多態(tài)生效。ptr 指向 Student 對象自動執(zhí)行 Student::BuyTicket。1.2 協(xié)變完整帶注釋代碼#include iostream using namespace std; class A {}; // B公有繼承A滿足協(xié)變的繼承關(guān)系 class B : public A {}; class Person { public: // 基類虛函數(shù)返回基類A的指針 virtual A* BuyTicket() { cout 買票-全價 endl; return nullptr; } }; class Student : public Person { public: // 協(xié)變返回派生類B*允許構(gòu)成重寫 virtual B* BuyTicket() { cout 買票-打折 endl; return nullptr; } }; // 參數(shù)基類指針滿足動態(tài)多態(tài)條件 void Func(Person* ptr) { ptr-BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }2. 析構(gòu)函數(shù)的虛函數(shù)重寫面試重點(diǎn)正常成員函數(shù)重寫函數(shù)名、參數(shù)必須完全相同。析構(gòu)函數(shù)是編譯器特殊處理的例外源代碼里基類析構(gòu)~A()派生類析構(gòu)~B()名字看起來不同編譯階段編譯器把所有析構(gòu)函數(shù)統(tǒng)一改名destructor只要基類析構(gòu)加 virtual派生類析構(gòu)自動構(gòu)成重寫派生類析構(gòu)寫不寫 virtual 都有效。最重要實(shí)際意義面試核心當(dāng)用基類指針指向派生類對象執(zhí)行delete 指針;如果基類析構(gòu)不是虛函數(shù)只會調(diào)用基類析構(gòu)派生類析構(gòu)不執(zhí)行。派生類里面申請的內(nèi)存無法釋放 →內(nèi)存泄漏。如果基類析構(gòu)是虛函數(shù)觸發(fā)多態(tài)先調(diào)用派生類析構(gòu)再調(diào)用基類析構(gòu)資源完整釋放。1. 析構(gòu)代碼逐行講解class A { public: // 基類析構(gòu)設(shè)置為virtual virtual ~A() { cout ~A() endl; } }; class B : public A { public: // 自動重寫基類虛析構(gòu)可省略virtual ~B() { cout ~B()-delete: _p endl; delete[] _p; // 釋放子類申請的堆內(nèi)存 } protected: int* _p new int[10]; // 子類持有堆資源 };int main() { A* p1 new A; A* p2 new B; delete p1; // 釋放A對象調(diào)用 ~A() delete p2; // 基類析構(gòu)是虛函數(shù)觸發(fā)多態(tài) // 先調(diào)用 ~B() 釋放_p數(shù)組再調(diào)用 ~A() return 0; }? 運(yùn)行輸出~A() ~B()-delete:0x... ~A()反面場景坑如果刪掉virtual變成~A(){}delete p2;只會執(zhí)行~A()不會執(zhí)行B ()_p指向的堆數(shù)組沒有釋放永久內(nèi)存泄漏。面試標(biāo)準(zhǔn)答案如果一個類可能被繼承最好把基類析構(gòu)定義為虛析構(gòu)函數(shù)防止基類指針管理派生類對象時發(fā)生內(nèi)存泄漏。2. 虛析構(gòu)完整帶注釋代碼#include iostream using namespace std; class A { public: // 重點(diǎn)基類析構(gòu)加上virtual成為虛析構(gòu)函數(shù) virtual ~A() { cout ~A() endl; } }; class B : public A { public: // 自動重寫基類虛析構(gòu)virtual可寫可不寫推薦寫上增強(qiáng)可讀性 ~B() { cout ~B()-delete: _p endl; delete[] _p; // 釋放子類堆資源 } protected: // 子類在堆上開辟內(nèi)存 int* _p new int[10]; }; int main() { A* p1 new A; A* p2 new B; delete p1; delete p2; return 0; }3. 考試 / 面試小結(jié)協(xié)變僅支持指針 / 引用日常開發(fā)很少用認(rèn)識即可析構(gòu)函數(shù)名稱源代碼不同編譯器統(tǒng)一改名滿足虛函數(shù)重寫條件被繼承的基類析構(gòu)函數(shù)建議加 virtual避免基類指針 delete 派生類對象造成內(nèi)存泄漏。2.1.6 override 和final關(guān)鍵字1、override檢查是否成功重寫虛函數(shù)問題背景虛函數(shù)重寫要求很嚴(yán)函數(shù)名、參數(shù)、返回值協(xié)變除外都要匹配。一不小心寫錯函數(shù)名、參數(shù)編譯器不會報錯它只會認(rèn)為你在派生類新增了一個獨(dú)立函數(shù)沒有完成重寫。等到運(yùn)行程序多態(tài)失效結(jié)果不對你還要花大量時間查 bug。override 的作用在派生類的虛函數(shù)后面寫上override編譯器會主動檢查這個函數(shù)能不能成功重寫基類的虛函數(shù)。滿足重寫規(guī)則正常編譯寫錯名字 / 參數(shù)無法構(gòu)成重寫直接編譯報錯提前發(fā)現(xiàn)錯誤。示例class A { public: virtual void func(){} }; class B : public A { public: // 正確成功重寫 void func() override{} // void func(int x) override{} // 報錯參數(shù)不一樣無法重寫 };小建議寫重寫的虛函數(shù)時習(xí)慣性加上 override。2. final兩個用途用途 1修飾虛函數(shù) —— 禁止派生類重寫這個虛函數(shù)基類虛函數(shù)后面加final后續(xù)所有子類都不能重寫它。子類強(qiáng)行重寫 → 編譯報錯。class A { public: virtual void func() final {} }; class B : public A { public: // void func(){} // 編譯報錯func被final禁止重寫 };用途 2修飾類 —— 禁止這個類被繼承類名后面加 final別的類不能繼承它c(diǎn)lass A final {}; // class B:public A{}; // 報錯A不能被繼承總結(jié)override告訴編譯器「我打算重寫虛函數(shù)幫我檢查有沒有寫錯」防止隱性 bug。final上鎖。修飾函數(shù) 不讓子類重寫修飾類 不讓別人繼承這個類。補(bǔ)充小對比override只用在派生類的虛函數(shù)final可以寫在基類虛函數(shù)后也可以寫在類名后2.1.7 重載/重寫/隱藏的對比如果追女生是每個少年男生心中第一件事喵那么搞清楚這三個對比就是現(xiàn)在的當(dāng)務(wù)之急重中之重喵。其實(shí)我想說這是第0件事哈哈哈哈哈因?yàn)橛?jì)算機(jī)的世界是二進(jìn)制的花園喵先一句話分清場景重載同一個班級里重寫父子班級 虛函數(shù)隱藏父子班級、但是達(dá)不到重寫條件。1. 重載同一作用域條件同一個范圍函數(shù)名字一樣參數(shù)不一樣。例子班里同一個教室同一個類 / 同一個作用域有兩位老師都叫 “講課”講課 (講數(shù)學(xué))講課 (講語文)名字都叫講課但是授課內(nèi)容參數(shù)不同。調(diào)用的時候看你傳什么參數(shù)自動匹配對應(yīng)函數(shù)。關(guān)鍵點(diǎn)全都在同一個教室同一作用域和繼承沒關(guān)系。返回值隨便可以相同也可以不同只看參數(shù)。// 同一個類內(nèi)部屬于重載 class Person { public: void Talk(string subject); void Talk(int age); };2. 重寫 / 覆蓋實(shí)現(xiàn)多態(tài)繼承體系 virtual 虛函數(shù)條件父類、子類兩個不同作用域父類函數(shù)是 virtual 虛函數(shù)函數(shù)名、參數(shù)一致協(xié)變除外。例子父親開小店virtual 買票 () → 全價票兒子繼承店鋪重新改造規(guī)則買票 () → 學(xué)生打折票規(guī)則名稱一模一樣入?yún)⒁粯?。有人通過店鋪招牌父類指針買票時自動區(qū)分是誰經(jīng)營店鋪執(zhí)行對應(yīng)的買票邏輯這就是多態(tài)。重點(diǎn)區(qū)別必須加virtual不加就不叫重寫。只有重寫才能觸發(fā)動態(tài)多態(tài)。class Person { public: virtual void BuyTicket() { cout 全價票; } }; class Student : public Person { public: void BuyTicket() override { cout 打折票; } // 重寫 };3. 隱藏又叫遮蔽繼承體系不滿足重寫條件規(guī)則父子類函數(shù)名相同只要構(gòu)不成重寫全部是隱藏。兩種常見情況1父類沒有寫 virtual2有 virtual但是參數(shù)列表不一樣例子父親店鋪功能買票 ()無參數(shù)兒子店鋪買票 (int num)帶參數(shù)名字都叫買票但是參數(shù)不一樣。如果你拿著兒子店鋪的招牌調(diào)用買票 ()只會看到兒子自己的版本父親同名函數(shù)直接被藏起來了。想要調(diào)用父類版本必須手動指定Person::BuyTicket()。?? 重要坑隱藏≠重寫不會觸發(fā)多態(tài)額外補(bǔ)充父子類同名成員變量也屬于隱藏父親有個變量int id;子類也寫int id;子類 id 會隱藏父類 id。class Person { public: virtual void BuyTicket() { cout 全價票; } }; class Student : public Person { public: void BuyTicket(int x) { cout 打折票; } // 參數(shù)不一樣無法構(gòu)成重寫屬于【隱藏】 };總結(jié)對照表方便記憶重載同一個房間同名不同參重寫父子房間、帶 virtual、簽名一致實(shí)現(xiàn)多態(tài)隱藏父子房間、名字相同但達(dá)不到重寫標(biāo)準(zhǔn)父類同名成員被遮蔽又是美好的一天各位觀眾姥爺們?nèi)粲惺裁床欢牡胤綒g迎在評論區(qū)留言哦我會給出相應(yīng)解釋的。如有什么疏忽的細(xì)節(jié)請點(diǎn)出我會及時糾正。感謝觀看。喵喵