(Visual Studio 2019)的使用)
此文章關(guān)于多態(tài)的代碼全部是使用Visua Studio2019 (x86) 實現(xiàn)的C多態(tài)在不同編譯器中的實現(xiàn)細(xì)節(jié)可能不同所以部分情況下相同代碼運行結(jié)果可能不同在此聲明。多態(tài)的概念多態(tài)顧名思義即多種形態(tài)。具體來說就是對于某個行為不同的對象去完成該行為會產(chǎn)生不同的狀態(tài)。就像 “叫” 這個行為貓來完成時其叫聲是 “喵喵喵” 而狗則是 “汪汪汪”。還有坐公交車?yán)先撕托『⒆囀敲馄倍渌藙t是全價票。同樣的一個動作不同的對象來執(zhí)行其實現(xiàn)的方法不同。多態(tài)的定義與實現(xiàn)多態(tài)是建立在繼承基礎(chǔ)上的在不同的繼承關(guān)系下的類對象調(diào)用同一函數(shù)產(chǎn)生不同的行為。在C中繼承體系下構(gòu)成多態(tài)需要滿足以下兩個條件1.必須通過基類指針或引用調(diào)用虛函數(shù)。2.被調(diào)用函數(shù)必須是虛函數(shù)且派生類必須對基類虛函數(shù)進(jìn)行重寫。虛函數(shù)虛函數(shù)即被 virtual 修飾的類的成員函數(shù)。123456classA {public:virtualintadd(inta,intb) {returna b;}}使用virtual修飾虛函數(shù)時只需要修飾類內(nèi)部修飾即可。12345678classA {public:virtualvoidprint_1();};virtualvoidA::print_1() {cout 1 : class A endl;}虛函數(shù)的重寫虛函數(shù)的重寫指的是派生類中有一個與基類完全相同的虛函數(shù)(即派生類的虛函數(shù)與基類虛函數(shù)的返回值類型參數(shù)列表函數(shù)名完全相同)稱子類的虛函數(shù)重寫了基類的虛函數(shù)。12345678910111213141516171819202122232425classA {public:virtualvoidprint_1() {cout 1 : class A endl;}};classB :publicA {public:virtualvoidprint_1() {cout 1 : class B endl;}};voidfunc(A a) {a.print_1();}intmain() {A a;B b;func(a);func(b);return0;}PS.子類重寫父類虛函數(shù)時子類虛函數(shù)可以不加virtual關(guān)鍵字這樣也可以構(gòu)成重寫——繼承后基類的虛函數(shù)被繼承下來了在派生類之中依舊保持虛函數(shù)屬性。但是這種寫法并不規(guī)范并不建議使用。虛函數(shù)重寫的兩個例外1.協(xié)變基類與派生類虛函數(shù)返回值不同派生類重寫基類虛函數(shù)時若基類虛函數(shù)的返回類型是基類的指針或引用新的返回類型是派生類的指針或引用依舊可以構(gòu)成重寫。這樣的重寫被稱為返回類型協(xié)變。123456789101112131415classA {public:virtualA print_1() {cout 1 : class A endl;return*this;}};classB :publicA {public:virtualB print_1() {cout 1 : class B endl;return*this;}};需要注意的是只有返回對應(yīng)類型的指針或引用才會構(gòu)成協(xié)變單純返回派生類或基類類型并不構(gòu)成協(xié)變。123456789101112131415classA {public:virtualA print_1() {cout 1 : class A endl;return*this;}};classB :publicA {public:virtualB print_1() {cout 1 : class B endl;return*this;}};2.析構(gòu)函數(shù)的重寫基類與派生類析構(gòu)函數(shù)函數(shù)名不同當(dāng)基類的析構(gòu)函數(shù)為虛函數(shù)時此時派生類的虛構(gòu)函數(shù)只要定義無論是否被virtual關(guān)鍵字修飾都與基類析構(gòu)函數(shù)構(gòu)成重寫。1234567891011121314151617181920classA {public:virtual~A() {cout A endl;}};classB :publicA {public:virtual~B() {cout B endl;}};intmain() {A* a newB;deletea;return0;}析構(gòu)函數(shù)的重寫最常見的應(yīng)用便是基類指針指向派生類對象的情況下如果我們所使用的派生類對象是從堆上 new 來的那么在沒有重寫析構(gòu)函數(shù)的情況下在基類對象 delete 時只會調(diào)用基類的析構(gòu)函數(shù)釋放基類的空間而派生類對象所占用的空間則不會被釋放就會導(dǎo)致內(nèi)存泄漏。C11 override final1. override在C中override關(guān)鍵字用來表示當(dāng)前虛函數(shù)重寫了基類虛函數(shù)。在函數(shù)比較多繼承關(guān)系負(fù)責(zé)的情況下override關(guān)鍵字可以起到提示編寫者與其后繼者某個函數(shù)重寫了基類的虛函數(shù)——也代表這個函數(shù)不是派生類中定義的。override關(guān)鍵字可以強制編譯器去檢查某個函數(shù)是否重寫了基類虛函數(shù)若否則會報錯。1234567891011121314classA {public:virtualvoidprint() {cout a endl;}};classB :publicA {public:virtualvoidprint() override {cout b endl;}};當(dāng)override修飾的函數(shù)并非重寫的基類虛函數(shù)時會報錯1234567891011121314classA {public:virtualvoidprint() {cout a endl;}};classB :publicA {public:virtualvoidnot_print() override {cout b endl;}};2.finalfinal用于修飾虛函數(shù)表明該虛函數(shù)不能被重寫我們是可以直接用final修飾父類虛函數(shù)的表明該虛函數(shù)不能被重寫但并不建議這樣做虛類既然與平常的類不同其實例化時自然會比普通的類多一份開而final只是指明了這個虛函數(shù)不可以被重寫但是虛類的相關(guān)開銷依舊在。123456789101112131415161718192021222324252627282930313233343536373839404142classA {public:inta;virtualvoidprint() final{cout a endl;}};classB :publicA {public:intb;voida_print() {cout b endl;}};classC {public:inta;voidprint() {cout a endl;}};classD :publicC {public:intb;voida_print() {cout b endl;}};intmain() {B b;D d;b.b 3;b.a 2;d.a 2;d.b 3;while(0);return0;}重載重寫重定義重載將語義或功能相似的函數(shù)用同一個名字表示。條件1.兩個函數(shù)在同一個作用域2.函數(shù)名相同3.參數(shù)列表不同4.返回值可以不同重寫 (覆蓋)覆蓋是C中多態(tài)的基礎(chǔ)使派生類可以重新定義基類的虛函數(shù)。條件1.兩個函數(shù)別屬于基類成員函數(shù)與派生類成員函數(shù)2.兩個函數(shù)都必須是虛函數(shù)3.函數(shù)名相同 (析構(gòu)函數(shù)除外至少看起來不同)4.返回值類型相同 (協(xié)變例外)5.參數(shù)列表相同6.函數(shù)的訪問權(quán)限可以不同(如基類中的虛函數(shù)的訪問權(quán)限是 private 派生類中可以重寫為public、protected)重定義 (同名隱藏)在繼承體系中子類與父類中有同名成員子類可以屏蔽父類指針對子類中父類成員的直接訪問。(但是可以使用 :: 訪問父類成員)條件1.在不同作用域2.函數(shù)名相同3.返回值可以不同4.在繼承體系中只要不構(gòu)成重寫就是重定義參數(shù)相同時基類沒有關(guān)鍵字virtual修飾基類的函數(shù)被隱藏參數(shù)不同時無論有沒有virtual關(guān)鍵字修飾基類函數(shù)都被隱藏12345678910111213141516171819202122classA {voidprint() {cout a endl;}};classB :publicA {public:intb;voidprint() {cout b endl;}};intmain() {A a;B b;a.print();b.print();while(0);return0;}純虛函數(shù)及抽象類在有些時候基類的某些行為在沒有被派生類重寫之前會非常不明確或者說不合常理不好定義。比如我們先前舉過的例子——動物的 “叫” 這個行為在動物這個基類中我們很難定義它具體是一種什么樣的行為表達(dá)方式是怎樣的。我們是無法對這個行為做出具體的定義的無法提供實現(xiàn)。但是作為一種 “共性” 我們又想要在基類中聲明它。于是乎在虛函數(shù)的基礎(chǔ)上C又提出了一種新的函數(shù)——純虛函數(shù)。在虛函數(shù)后面寫上 0則這個函數(shù)就是純虛函數(shù)。包含純虛函數(shù)的類叫做接口類——也被稱為抽象類抽象類不能實例化對象。派生類繼承抽象類后如果不重寫基類的純虛函數(shù)也無法實例化對象。純虛函數(shù)規(guī)定了派生類必須重寫同時也實現(xiàn)了接口繼承。12345678910111213141516171819classA {public:virtualvoidprint() 0;};classB :publicA {public:intb;virtualvoidprint() {cout b endl;}};intmain() {B b;b.print();while(0);return0;}接口繼承與實現(xiàn)繼承普通函數(shù)的繼承是一種實現(xiàn)繼承派生類繼承了基類的函數(shù)可以使用函數(shù)這種繼承繼承的是函數(shù)的實現(xiàn)。虛函數(shù)的繼承則是一種接口繼承派生類繼承的只是虛函數(shù)的接口目的是為了重寫從而達(dá)成多態(tài)繼承的是一個接口。所以如果不實現(xiàn)多態(tài)就不要把函數(shù)設(shè)置為虛函數(shù)。多態(tài)的原理1234567891011121314151617181920classA {public:int_a1;int_a2;virtualvoidstudy() {cout study endl;}A():_a1(1),_a2(2){}};intmain() {A a;return0;}以上代碼中類A的實例化對象a在實例化后其內(nèi)存如上圖所示明顯在我們所定義的變量a1, a2之前還有著一串神奇的數(shù)字——00 75 9b 34。(據(jù)說有些編譯器這玩意可能在后邊)單從表面上來看它應(yīng)該是一個指針。而實質(zhì)上它的確是一個指針。在VS調(diào)試模式下的局部變量窗口中我們可以很清楚的看到這玩意還不是一個簡單的指針?biāo)且粋€二級指針指針名為 __vfptr v——virtual f——function我們通常稱其為虛函數(shù)表指針。進(jìn)一步將這個虛函數(shù)表指針展開來看很容易看到這個虛函數(shù)表指針指向的對象是一個函數(shù)指針而函數(shù)指針指向的對象則是我們代碼中所定義的虛函數(shù)——A::study(void)一個含有虛函數(shù)的類中都至少有一個虛函數(shù)表指針因為虛函數(shù)的地址要被存放到虛函數(shù)表中。虛函數(shù)表也簡稱虛表。下面我們將之放入繼承體系之中來分析。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647classA {public:int_a1;int_a2;virtualvoidfunc1() {cout A::func1 endl;}virtualvoidfunc2() {cout A::func2 endl;}virtualvoidfunc3() {cout A::func3 endl;}A():_a1(1),_a2(2){}};classB :publicA{public:int_b1;int_b2;virtualvoidfunc1() {cout B::func1 endl;}virtualvoidfunc2() {cout B::func2 endl;}B():_b1(3),_b2(4){}};intmain() {A a;B b;return0;}通過以上調(diào)試圖片可以看出1.基類中有一個虛函數(shù)表指針虛函數(shù)表中存儲著三個基類虛函數(shù)的地址。2.派生類對象中也有一個虛函數(shù)表指針且該虛函數(shù)表由兩部分構(gòu)成——派生類重寫的虛函數(shù)以及從基類中繼承的虛函數(shù)。3.基類對象與派生類對象的虛函數(shù)表中的內(nèi)容是不同的由于我們在派生類代碼中重寫了虛函數(shù)func1以及func2所以派生類的虛函數(shù)表中對應(yīng)位置存儲的是被派生類重寫后的B::func1(void)以及B::func2(void)。所以虛函數(shù)的重寫也被稱為覆蓋覆蓋指的是虛表中虛函數(shù)的覆蓋派生類重寫的虛函數(shù)地址覆蓋了基類的虛函數(shù)地址。重寫是語法層面的稱呼覆蓋是原理層面的稱呼。4.虛函數(shù)表本質(zhì)是一個存放虛函數(shù)指針的指針數(shù)組一般情況下這個數(shù)組最后會放一個nullptr。5.派生類虛表生成的過程如下首先將基類中虛表內(nèi)容拷貝一份到派生類之中而后如果派生類重寫了某個虛函數(shù)用派生類自身的虛函數(shù)的地址覆蓋虛函數(shù)表中基類虛函數(shù)的地址最后派生類將派生類之中新增的虛函數(shù)增加到虛函數(shù)表的最后。6.虛函數(shù)表中只存放繼承或派生類自身定義的虛函數(shù)非虛函數(shù)不會被存放。虛函數(shù)存在哪里虛表存在哪里與普通的函數(shù)一樣虛函數(shù)存放于代碼段中只是它的指針被存儲到了虛表之中。虛表并不存放于對象之中對象中存儲的是虛表指針。虛表存放于代碼段之中。(個人實力原因暫時無法證明)多態(tài)的原理分析了這么多多態(tài)的原理究竟是什么這里就需要用到我們之前屢試不爽的方法——通過基類指針/引用調(diào)用函數(shù)1234567891011voidFunc(A* tmp) {tmp-func1();}intmain() {A a;B b;Func(a);Func(b);return0;}通過對以上代碼進(jìn)行反匯編我們可以看到在使用基類引用調(diào)用虛函數(shù)時的操作如下1mov eax,dword ptr [tmp]tmp中存儲的是基類對象的指針將tmp中的值移動到eax寄存器中。1mov edx,dword ptr [eax]將eax的值指向的內(nèi)容存儲到edx中即將基類對象前4個字節(jié)(虛表指針)移動到edx中。1mov ecx,dword ptr [tmp]取tmp指針存儲的值到ecx寄存器中1mov eax,dword ptr [edx]取edx寄存器中的值指向的內(nèi)容相當(dāng)于取了虛表的前四個字節(jié)即虛表中存儲的第一個虛函數(shù)的地址。如果我們此處調(diào)用的是fun2或者func3則會在edx基礎(chǔ)上偏移一定字節(jié)。1call eax調(diào)用eax寄存器中存儲的指針指向的函數(shù)。動態(tài)綁定與靜態(tài)綁定靜態(tài)綁定靜態(tài)綁定又被稱為前期綁定或早綁定在程序編譯期間便已經(jīng)確定了程序的行為也被稱為靜態(tài)多態(tài)。動態(tài)綁定動態(tài)綁定又被稱為后期綁定或晚綁定在程序運行期間根據(jù)具體拿到的類型來確定程序的行為調(diào)用具體的函數(shù)也稱為動態(tài)多態(tài)。多繼承下的虛函數(shù)表我們上文所主要講解使用的是單繼承情況下的虛函數(shù)表。這里我們就簡單研究下如果在多繼承體系下虛函數(shù)表又會有怎樣的不同。