C++的繼承與多型
◆ 概念介紹
繼承:為了程式碼的重用,保留基類的原本結構,並新增派生類的部分,同時可能覆蓋(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]
//基類指標指向的是基類物件,不能強制轉化為子類指標