一、繼承概念
在cpp中,封裝、繼承、多態是面向對象的三大特性。這里的繼承就是允許已經存在的類(也就是基類)的基礎上創建新類(派生類或者子類),從而實現代碼的復用。
如上圖所示,Person是基類,Stu與Tea是派生類,Stu與Tea分別繼承了基類中的對象,同時也有自己的類對象。
1.1派生類對基類的修改
派生類對象可以賦值給基類對象、基類指針、基類引用,這里的賦值只是把派生類中原本繼承于父類的類對象賦值回去,對于派生類對象自己的類對象不會賦值。但是基類對象不能賦值給派生類對象。
如上圖,派生類只能將基類中原有的(或者說繼承過來的)_name和_gender賦值給父類,其余的無法賦值,如果是引用或指針,也是將派生類中基類對應的對象引用給或地址傳給基類,基類修改時,子類也會受影響。
如上圖,代碼驗證。注意,以上代碼是在public繼承時才會生效,如果換成protected時代碼就會報錯,protected繼承下來的父類對象就是protected而非public,不支持修改的,private繼承同理。
1.2父子類類成員變量、函數重名
當父類類成員變量名與子類成員變量名沖突時,默認時優先使用子類的。其實子類中也繼承了父類中的重名變量,只不過將其隱藏,可以通過指定類成員名::變量名的方式訪問。
再提一點,如果子類中沒有實現Print函數而是依靠父類中的Print函數,那么打印結果會是這樣的,如下圖。
這是因為返回給父類的是一個Person類型的this指針,解引用訪問的就是Person類中的_val.
當存在同名的函數名時,子類會調用自身的函數,也可以通過類名指定的方式進行訪問。
如上圖,這里A::func與B::func關系是隱藏,注意與函數重載區分(函數重載條件是同一作用域內函數名相同,參數列表不同構成重載)。
1.3派生類的默認成員函數
#include <iostream> using namespace std; class Person { public://構造函數Person() :_name("張三") {cout << "Person()" << endl;}//析構函數~Person() {cout << "~Person()" << endl;}//拷貝構造Person(const Person& p1):_name(p1._name) {cout << "Person(copy construct)" << endl;}Person& operator=(const Person& p1) {cout << "operator=" << endl;if (this != &p1) {this->_name = p1._name;}return *this;}public:string _name; };class Son :public Person { public:Son(const char* name = "", const string id = "111"):_id(id){}void display() {cout << _name << " " << _id << endl;} private:string _id; };int main() {Son s1;s1.display();return 0; }
子類繼承父類時會調用父類的構造函數來初始化繼承過來的成員,然后子類在初始化自己的成員,同理對于析構、拷貝構造、賦值重載等都是同理。
如上圖,s1會對繼承的成員調用其對應的Person類的構造函數,當然,這也是我沒有自定義時會調用父類的構造函數對其進行構造。
那么如何進行自定義構造_name呢?
如上圖所示,通過son的構造函數對s1進行實例化構造,但是對于從父類繼承下來的_name進行自定義時需要注意的是,在初始化_name時我們不能通過直接初始化的方式進行構造(如38行代碼,這是錯誤的),而是通過父類的構造函數對父類成員進行初始化。在上圖中也可以看見代碼在初始化列表時(代碼36行)就會調用父類的構造函數對_name進行初始化。
當然,也可以不自定義,此時_name就調用父類默認的構造函數對其進行初始化(前提是父類要有全缺省的構造函數,不然代碼就會報錯)。也可以使用初始化匿名對象的方式完成。
如上圖所示,同時也要在父類中定義相對應的構造函數類型。
實現子類對象的拷貝構造函數
如上圖,在實現子類的拷貝構造函數時,可以用子類類型的s來實例化Person,(這就是切片:父類可以提取子類中從父類繼承來的_name進行初始化通過參數來初始化基類成員)
實現子類對象的賦值重載函數
如上圖,實現子類對象的賦值重載函數時需要指明具體是哪一個重載函數,否則就會出現死循環,因為子類和父類出現同名函數時會優先調用子類的函數。代碼第61行將Son類對象s進行切片,然后調用父類Person的重載函數將s中父類的部分切給Person完成賦值重載。
1.4繼承與友元的關系
如上圖所示,父類A的友元函數為display,子類B繼承了父類A,此時友元函數只能訪問子類的公開成員,對于受保護和私有的則無法訪問。
1.5繼承與靜態成員
如上圖,父類A中定義的靜態成員變量在整個繼承體系中都是存在的。
1.6菱形繼承
如下圖,A是B和C的父類,D又同時繼承了B和C,此時D中含有基類成員_d和父類B(_b)和父類C(_c),同時B和C又同時含有A(_a),因此我們在訪問_a時需要指定類域。
在上述圖中可見,在開辟空間時,內存中64~68是父類B的空間,其中存放了B::_a和B::_b,對應的值就是1和3;而6C~70是父類C的空間,其中存放的就是C::_a和C::_C,對應的值就是2和4,最后一個位置就是D::_d。
如上圖,整個44~54是類對象D的空間。
造成代碼冗余與二義性問題
在上述代碼中,子類D會同時存儲了兩份A的繼承,分別是繼承B和C的,這個就造成了代碼冗余與內存消耗;其次當D訪問A中成員時必須要指定具體哪個類中的(無法通過d._a方式訪問)。解決方法就是虛擬繼承。
如上圖,通過虛擬繼承的方法可以直接訪問d._a,其實這里的B和C共享同一分A的繼承,也就是說代碼第91和92行對_a的修改是對同一個對象的修改(這一點在代碼運行過程中可以看出)。
如上圖所示,不難發現雖然_a是類中共享的一份區域,但是C和B區域與非虛擬繼承相比又多出一塊區域(如上圖中綠色區域所示)。在分析內存時,0x0078FEAC指向的位置是0x00929bf4,0x0078FEC0指向的位置是0x00929c00,其內存圖如下圖所示
如上圖所示,雖然0x00929bf4與0x00929c00指向的位置內容為空,但是其后一個位置的0000000c從十六進制轉換為十進制剛好是12,其實這也就是C到_a的偏移量,這個表叫做虛基表,而只想虛機表的指針叫做虛機表指針。