C++之多型(內容不錯)
編譯環境:WIN10 VS2017
這篇部落格有點長,但都是滿滿的乾貨,一定要看到最後,那才是重點。
什麼是多型?
顧名思義就是同一個事物在不同場景下的多種形態。
下面會具體的詳細的介紹。
靜態多型
我們以前說過的函式過載就是一個簡單的靜態多型
int Add(int left,int right) { return left + right; } double Add(double left,int right) { return left + right; } int main() { Add(10,20); //Add(10.0,20.0); //這是一個問題程式碼 Add(10.0,20); //正常程式碼 return 0; }
可以看出來,靜態多型是編譯器在編譯期間完成的,編譯器會根據實參型別來選擇呼叫合適的函式,如果有合適的函式可以呼叫就調,沒有的話就會發出警告或者報錯。。。比較簡單,不做多介紹。
動態多型
什麼是動態多型呢?
動態多型: 顯然這和靜態多型是一組反義詞,它是在程式執行時根據基類的引用(指標)指向的物件來確定自己具體該呼叫哪一個類的虛擬函式。
我在西安臨潼上學,我就以這邊的公交車舉個栗子啊:
class TakeBus { public: void TakeBusToSubway() { cout << "go to Subway--->please take bus of 318" << endl; } void TakeBusToStation() { cout << "go to Station--->pelase Take Bus of 306 or 915" << endl; } }; //知道了去哪要做什麼車可不行,我們還得知道有沒有這個車 class Bus { public: virtual void TakeBusToSomewhere(TakeBus& tb) = 0; //???為什麼要等於0 }; class Subway:public Bus { public: virtual void TakeBusToSomewhere(TakeBus& tb) { tb.TakeBusToSubway(); } }; class Station :public Bus { public: virtual void TakeBusToSomewhere(TakeBus& tb) { tb.TakeBusToStation(); } }; int main() { TakeBus tb; Bus* b = NULL; //假設有十輛公交車,如果是奇數就是去地鐵口的,反之就是去火車站的 for (int i = 1; i <= 10; ++i) { if ((rand() % i) & 1) b = new Subway; else b = new Station; } b->TakeBusToSomewhere(tb); delete b; return 0; }
這就是一個簡單的動態多型的例子,它是在程式執行時根據條件去選擇呼叫哪一個函式。
而且,從上面的例子我們還發現了我在每一個函式前都加了virtual這個虛擬關鍵字,想想為什麼?如果不加會不會構成多型呢?
幹想不如上機實踐:
class Base { public: virtual void Funtest1(int i) { cout << "Base::Funtest1()" << endl; } void Funtest2(int i) { cout << "Base::Funtest2()" << endl; } }; class Drived :public Base { virtual void Funtest1(int i) { cout << "Drived::Fubtest1()" << endl; } virtual void Funtest2(int i) { cout << "Drived::Fubtest2()" << endl; } void Funtest2(int i) { cout << "Drived::Fubtest2()" << endl; } }; void TestVirtual(Base& b) { b.Funtest1(1); b.Funtest2(2); } int main() { Base b; Drived d; TestVirtual(b); TestVirtual(d); return 0; }
在呼叫FuncTest2的時候我們看出來他並沒有給我們呼叫派生類的函式,因此我們可以對動態多型的實現做個總結。
動態多型的條件:
●基類中必須包含虛擬函式,並且派生類中一定要對基類中的虛擬函式進行重寫。
●通過基類物件的指標或者引用呼叫虛擬函式。
重寫 :
(a)基類中將被重寫的函式必須為虛擬函式(上面的檢測用例已經證實過了)
(b)基類和派生類中虛擬函式的原型必須保持一致(返回值型別,函式名稱以及引數列表),協變和解構函式(基類和派生類的解構函式是不一樣的)除外
(c)訪問限定符可以不同
那麼問題又來了,什麼是協變?
協變:基類(或者派生類)的虛擬函式返回基類(派生類)的指標(引用)
總結一道面試題:那些函式不能定義為虛擬函式?
經檢驗下面的幾個函式都不能定義為虛擬函式:
1)友元函式,它不是類的成員函式
2)全域性函式
3)靜態成員函式,它沒有this指標
3)建構函式,拷貝建構函式,以及賦值運算子過載(可以但是一般不建議作為虛擬函式)
抽象類:
在前面公交車的例子上我提了一個問題:
class Bus { public: virtual void TakeBusToSomewhere(TakeBus& tb) = 0; //???為什麼要等於0 };
在成員函式(必須為虛擬函式)的形參列表後面寫上=0,則成員函式為純虛擬函式。包含純虛擬函式的類叫做抽象類(也叫介面類),抽象類不能例項化出物件。純虛擬函式在派生類中重新定義以後,派生類才能例項化出物件。純虛擬函式是一定要被繼承的,否則它存在沒有任何意義。
多型呼叫原理
class Base { public: virtual void Funtest1(int i) { cout << "Base::Funtest1()" << endl; } virtual void Funtest2(int i) { cout << "Base::Funtest2()" << endl; } int _data; }; int main() { cout << sizeof(Base) << endl; Base b; b._data = 10; return 0; }
8?不知道大家有沒有問題,反正我是有疑惑了。以前在物件模型(https://blog.csdn.net/qq_39412582/article/details/80808754)時我提到過怎麼來求一個類的大小。按照那個方法,這裡應該是4才對啊,為什麼會是8呢?
通過觀察。我們發現這個例子裡面和以前不一樣,類成員函式變成了虛擬函式,這是不是引起類大小變化的原因呢?
我們假設就是這樣,然後看看記憶體裡是怎麼儲存的呢?
可以看到它在記憶體裡多了四個位元組,那這四個位元組的內容到底是什麼呢?
是不是有點看不懂,我們假設它是一個地址去看地址裡存的東西的時候發現它存的是兩個地址。
我假設它是虛擬函式的地址,我們來驗證一下:
typedef void (__stdcall *PVFT)(); //函式指標 int main() { cout << sizeof(Base) << endl; Base b; b._data = 10; PVFT* pVFT = (PVFT*)(*((int*)&b)); while (*pVFT) { (*pVFT)(); pVFT+=1; } return 0; }
結果好像和我們的猜想一樣,是一件開心的事。然後我給一張圖總結一下:
在反彙編中我們還可以看到,如果含有虛擬函式的類中沒有定義建構函式,編譯器會自動合成一個建構函式
對於派生類的東西我給個連結仔細看,人家總結的超級贊,我偷個懶就不寫了,老鐵們包容下啊。
派生類虛表:
1.先將基類的虛表中的內容拷貝一份
2.如果派生類對基類中的虛擬函式進行重寫,使用派生類的虛擬函式替換相同偏移量位置的基類虛擬函式
3.如果派生類中新增加自己的虛擬函式,按照其在派生類中的宣告次序,放在上述虛擬函式之後
https://coolshell.cn/articles/12176.html
多型缺陷
●降低了程式執行效率(多型需要去找虛表的地址)
●空間浪費