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