◆ 概念介紹
繼承:為了代碼的重用,保留基類的原本結構,並新增派生類的部分,同時可能覆蓋(overide)基類的某些成員。
多態:一種將不同的特殊行為和單個泛化記號相關聯的能力,分為靜態多態和動態多態。
◆ 繼承:
一個派生類可以通過繼承獲得基類的所有成員,而無需再次定義它們。分為public、protected和private三種繼承方式,前兩種方式保持基類的所有成員的屬性不變,且派生類可以訪問基類的public和protected成員,但仍然不能訪問基類的private成員;private繼承將使得基類的所有成員在派生類中表現為private屬性。
聲明一個派生類對象,即在構造派生類對象時,遵循基類的接口,先構造基類子對象,再構造派生類增加的部分。其中的組成由下圖所示:
當出現菱形繼承時,例如下圖所示:
要構造一個SleepSofa對象,就要構造一個Sofa和一個Bed子對象,這其中又同時構造了兩次Furniture對象,這是不合理的。因此Bed和Sofa類要對Furniture類進行虛繼承(virtual public Furniture)來避免這種狀況。
◆ 多態:
靜態多態:在編譯時期就已經確定了的行為,例如帶變量的宏,模板,函數重載,運算符重載,拷貝構造等。
動態多態:在運行時期才能確定調用的行為。例如虛函數調用機制。本部分主要討論的是動態多態。虛函數是實現動態多態的機制,其核心理念就是通過基類指針來訪問派生類定義的成員。成員函數在基類為虛函數時,在派生類同樣也是虛函數。純虛函數是指不希望基類對象調用的成員函數,需要派生類覆蓋實現這樣的純虛函數。(注:如果某個成員函數在基類中沒有用virtual關鍵字修飾,即普通函數,而在派生類中卻又有完全相同的成員函數聲明,兩個函數即使有相同的名字和相同的參數類型與數量,這兩個函數也是完全不同的函數,因為類的作用域不同)
虛函數表(vtable):每個類都擁有一個虛函數表,虛函數表中羅列了該類中所有虛函數的地址,排列順序按聲明順序排列,例如這樣兩個類
class Base { virtual void f() {} virtual void g() {} //其他成員 };
Base b;
class Derive : public Base { void f() {} virtual void d() {} //其他成員 }; Derive d;
虛表指針(vptr):每個類有一個虛表指針,當利用一個基類的指針綁定基類或者派生類對象時,程序運行時調用某個虛函數成員,會根據對象的類型去初始化虛指針,從而虛表指針會從正確的虛函數表中尋找對應的函數進行動態綁定,因此可以達到從基類指針調用派生類成員的效果。
那么為什么需要虛指針和虛函數表來實現動態多態呢?因為無論是什么函數,包括類內的虛函數和非虛函數,都會儲存在內存中的代碼段。但是當編譯器在編譯時,就可以確定普通函數和非虛函數的入口地址,以及其調用的信息,所以這指的是常量指針。當遇到動態多態時,虛函數真正的入口地址的指針要在運行時根據對象的類型才能確定,所以要通過虛指針從虛函數表中找虛函數對應的入口地址。
當然,用基類指針綁定的子類對象,只能通過這個基類指針調用基類中的成員,因為作用域僅限於基類的子對象,子類新增的部分是看不見的。
總結為下面這個例程:
#include <iostream> using std::cout; using std::endl; class Base { public: void fun() { cout << "Base::fun()" << endl; } virtual void vfun() { cout << "Base::virtual fun()" << endl; } }; class Derive : public Base { public: void fun() { cout << "Derive::fun()" << endl; } virtual void vfun() { cout << "Derive::virtual fun()" << endl; } void dfun() { cout << "Derive::dfun()" << endl; } }; int main() { Base* bp = new Base(); Base* dp = new Derive(); bp->fun(); bp->vfun(); dp->fun(); dp->vfun(); //dp->dfun(); //編譯錯誤:基類指針指向子類中基類的子對象 //不能看到子類的成員 delete bp; delete dp; return 0; }
輸出為:
可以看出,bp綁定一個基類對象,調用自己的成員無異議;dp綁定的是一個子類對象,因此調用fun()時,由於dp是一個基類指針,作用域在於基類中,所以調用的是基類的fun(),而調用vfun()是通過動態綁定調用虛函數表中被子類覆蓋的Derive::vfun(),而如果要調用dfun()時則會出現編譯錯誤,因為子類獨有成員基類指針不可見。
注:在解有關動態多態的題時,只要把握住一點:這個指針指向的到底是基類對象還是子類對象,如果是基類對象,則調用基類的成員函數,如果是子類對象,則要考慮到這個虛成員函數是否被子類中的成員覆蓋掉,即是否產生了動態綁定。另外還有一點,從子類對象強制類型轉換為基類對象是允許的,而相反地要從基類對象強制轉換成子類對象是錯誤的(編譯不通過)。
Base* dp1 = new Derive(); Derive* dp2 = (Derive*) dp1; //基類指針指向的是子類對象,可以強制轉化為子類指針 Base* bp1 = new Base(); Derive* bp2 = (Base*) bp1; //錯誤,[Error] invalid conversion from 'Base*' to 'Derive*' [-fpermissive]
//基類指針指向的是基類對象,不能強制轉化為子類指針