測試環境:target: x86_64-linux-gnu
gcc version 5.3.1 20160413 (ubuntu 5.3.1-14ubuntu2.1)
多型一詞最初**於希臘語,意思是具有多種形式或形態的情形,當然這只是字面意思,它在c++語言中多型有著更廣泛的含義。
這要先從物件的型別說起!物件的型別有兩種:
舉個栗子:derived1類和derived2類繼承base類
物件有靜態型別,也有動態型別,這就是一種型別的多型。class base
{};class derived1 : public base
{};class derived2 : public base
{};int main()
多型有靜態多型,也有動態多型,靜態多型,比如函式過載,能夠在編譯器確定應該呼叫哪個函式;動態多型,比如繼承加虛函式的方式(與物件的動態型別緊密聯絡,後面詳解),通過物件呼叫的虛函式是哪個是在執行時才能確定的。
【靜態多型】
栗子:函式過載
執行結果:long long add(int left, int right)
double add(float left, float right)
int main()
第一組毫無疑問,通過本類物件和本類物件的指標就是呼叫本類的函式;第二組中先讓基類指標指向子類物件,然後呼叫該函式,呼叫的是子類的,後讓基類指標指向另乙個子類物件,呼叫的是子類的函式。這是因為p的型別是乙個基類的指標型別,那麼在p看來,它指向的就是乙個基類物件,所以呼叫了基類函式。就像乙個int型的指標,不論它指向哪,讀出來的都是乙個整型(在沒有崩潰的前提下),即使將它指向乙個float。再來對比著看下乙個栗子。
栗子:繼承+虛函式
執行結果:class person
;class man : public person
return 0;
}
就像上邊這個栗子所演示的那樣,通過重寫虛函式(不再是普通的成員函式,是虛函式!),實現了動態繫結,即在程式執行期間(非編譯期)判斷所引用物件的實際型別,根據其實際型別呼叫相應的方法。使用virtual關鍵字修飾函式時,指明該函式為虛函式(在栗子中為純虛函式),派生類需要重新實現,編譯器將實現動態繫結。在上邊栗子中,當指標p指向man類的物件時,呼叫了man類自己的函式,p指向woman類物件時,呼叫了woman類字幾的函式。
今天的重點來了,就是要分析這個動態繫結實現的原理(以下測試在vs2013環境下進行):
為了方便除錯,我將程式修改如下:
先求一下兩個普通的繼承類的大小,在這裡為空類,沒有包含成員變數,所以為1,表示佔位:class person;};
class man : public person
};int main()
假如基類中包含乙個int型變數,那麼這裡的大小都會是4。這不是今天的重點,不再敘說。主要是想看看普通空類的大小。
再改一下程式,在基類的成員函式前加virtual關鍵字,將這個函式變為虛函式。
其它部分**不變,執行結果:class person
;};
大小變成了4.所以這個類裡面肯定是有什麼東西的。
在main中加入以下**:
man man;
person *p = &man;
p->gotowashroom();
檢視監視視窗:
man物件裡存在了乙個指標,而且是void**型別的,那麼這個指標指向哪呢?可以在記憶體中檢視一下這個位址所在記憶體中的內容。
嗯,看不懂,不要緊,把這個數字記下來:0x01 27 13 de
繼續執行程式,轉到反彙編:
同時,_vfptr的值為0x01 27 13 de。接下來就要準備呼叫函式了。
一句句分析:
分析暫告一段落。我們知道了man物件中維護了乙個虛表指標,虛表中存放著虛函式的位址。基於這個虛表指標,實現動態繫結,才可以用基類指標呼叫了man類中的虛函式。因為指標p看到的是虛表的指標,它呼叫的虛函式是從虛表中查詢的。如果基類中有多個虛函式的話,那麼虛表中也會依次按基類中虛函式定義順序存放虛函式的位址,並以0x 00 00 00 00 結尾。再如果子類中有自己定義的新的虛函式,那麼會排在虛函式表的後邊。在呼叫虛函式時由編譯器自動計算偏移取得相應的虛函式位址。
看看是如何構造子類物件的:
執行結果:class person
~person()
virtual void gotowashroom()
{};};class man : public person
~man()
void gotowashroom() };
int main()
先呼叫基類建構函式,虛表指標先指向基類虛表,然後呼叫子類建構函式,這時候子類物件的虛表指標就指向了子類自己的虛表。這才是動態繫結實現原理。詳細內容可以檢視反彙編,這裡不再寫了。
再舉個栗子看看虛表指標在物件的首部還是尾部,又或者是在中間某個地方存放:
檢視記憶體中:&manclass person
;public:
int i1;
int i2;
int i3;
};class man : public person
};int main()
可以看出,虛表指標位於物件的首部。
【動態繫結條件】
必須是虛函式
通過基類型別的引用或者指標呼叫
派生類重寫基類的虛函式實現多型,要求函式名、引數列表、返回值完全相同。(協變除外)
基類中定義了虛函式,在派生類中該函式始終保持虛函式的特性
只有類的成員函式才能定義為虛函式,靜態成員函式不能定義為虛函式
如果在類外定義虛函式,只能在宣告函式時加virtual關鍵字,定義時不用加
建構函式不能定義為虛函式,雖然可以將operator=定義為虛函式,但最好不要這麼做,使用時容 易混淆
不要在建構函式和析構函式中呼叫虛函式,在建構函式和析構函式中,物件是不完整的,可能會 出現未定義的行為
最好將基類的析構函式宣告為虛函式。(析構函式比較特殊,因為派生類的析構函式跟基類的析構 函式名稱不一樣,但是構成覆蓋,這裡編譯器做了特殊處理)
虛表是所有類物件例項共用的
容易混淆的點://協變,也可以構成重寫(覆蓋),但返回值是該類型別的指標或引用
class base
};
class derived : public base
};
c 多型剖析
一句話概括 在基類的函式前加上virtual關鍵字,在派生類中重寫該函式,執行時將會根據物件的實際型別來呼叫相應的函式。如果物件型別是派生類,就呼叫派生類的函式 如果物件型別是基類,就呼叫基類的函式。1 用virtual關鍵字申明的函式叫做虛函式,虛函式肯定是類的成員函式。2 存在虛函式的類都有乙個...
C 筆記 深度剖析多型(虛函式表)上
摘要 多型性是物件導向程式設計語言中資料抽象和繼承之外的第三個基本特性。要想認識多型,我們需要從最基礎的知識開始著手,這篇部落格是我整理了很久才發出來的,裡面對於多型的底層分析很詳盡,希望可以對你們有所幫助 多型的概念 多型,顧名思義就是一種事物具有多種形態,用比較正式的話來說,大概就是下面這段話啦...
剖析C 的多型
一 什麼是多型 物件導向程式設計中的另外乙個重要概念是多型性。在執行時,可以通過指向基類的指標,來呼叫實現派生類中的方法。可以把一組物件放到乙個陣列中,然後呼叫它們的方法,在這種場合下,多型性作用就體現出來了,這些物件不必是相同型別的物件。當然,如果它們都繼承自某個類,你可以把這些派生類,都放到乙個...