前言:
????????各位代碼航海家,歡迎回到C++繼承宇宙!上回我們解鎖了繼承的「基礎裝備包」,成功馴服了public
、protected
和花式成員隱藏術。但——
??????????前方高能預警: 繼承世界的暗流涌動遠不止于此!今天我們將勇闖三大神秘海域:
多繼承の百慕大三角 👻——當你的類同時認了兩個"爹"
虛繼承の量子糾纏 ??——專治"菱形繼承"引發的時空悖論
繼承構造/析構の連鎖反應 💥——比《信條》更燒腦的逆向工程
????????準備好你的IDE光劍和調試護盾,我們即將潛入繼承深淵!
🎮 本關Boss預告
class 爺爺 {}; ? class 爸爸1 : virtual public 爺爺 {}; // 虛繼承! ? class 爸爸2 : virtual public 爺爺 {}; ? class 你 : public 爸爸1, public 爸爸2 {}; // 多繼承の最終形態! ?
靈魂拷問:
當
爺爺
的遺產被爸爸1
和爸爸2
重復繼承時,你
會繼承幾份祖傳代碼?為什么祖師爺要發明"虛繼承"這種黑科技?
構造函數們究竟在繼承鏈上玩什么接力賽?
(摸魚提示:文末附贈「菱形繼承生存指南」,保你跳出編譯錯誤的黑洞!)
? 建議裝備
咖啡因補給包 ×1
防止指針錯亂的思維導圖 ×1
暫時忘記Java/Python的勇氣 ×1
? 3秒后進入繼承下篇—— 程序員,你是否選擇「接受挑戰」?
(按下F5繼續執行代碼...🚀)
1.派生類的默認成員函數
1.1.四個常見的默認成員函數
????????通過之前的學習,我們知曉,C++有六種默認構造函數,忘記的或者是不知曉的讀者可以看我之前寫過的初始C嘎嘎的文章,那里詳細記載了,當然,主要的默認成員函數其實有四種:構造函數、拷貝構造函數、賦值運算符重載,析構函數默認的意思指的是我們不寫,編譯器會幫我們自動生成一個,那么在派生類中,這四種函數又是如何生成的呢?
????????1.派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。其實我們可以把基類當做是一個自定義類型的成員變量,眾所周知,自定義類型的成員變量會調用自己的默認構造函數,如果沒有默認構造函數的時候,編譯器就會報錯,必須顯示的去調用構造函數。而基類的默認構造也和這個類似,大多數情況我們都是不需要自己調用構造函數的,當然,不排除我們沒寫默認構造函數,那么它的用法如下所示:
class Person
{
public:Person(const string& name) :t_name(name) ? //這里就用一個正常的構造函數,但不是默認構造函數{}
protected:string t_name;
};
?
class Student :public Person
{
public:Student(const string& _t_name,const string& name) : Person(_t_name),_name(name) ?//直接調用其父類的構造函數即可,當做整體進行構造{}
protected:string _name;
};
????????2.派生類對象初始化先調用基類構造再調派生類構造。這個很好去理解,因為基類是比派生類的成員變量出現的早的,所以出現最早的優先調用構造函數,所以先調用基類的構造在調用子類的構造。
????????3.派生類的拷貝構造函數必須調用基類的拷貝構造來完成基類的拷貝初始化。這個也是比較好理解的,因為拷貝構造需要我們傳入對應的對象,不像默認構造函數那樣有缺省值直接調用缺省值就好,所以它要求我們去顯示的調用拷貝構造,這里的知識和我們第一篇繼承的知識聯系在了一起,不知道讀者是否還記得“切片”,也就是public繼承的派生類對象可以賦值給基類的指針 / 基類的引用。也就是如下圖所示:
????????此時我們僅需在派生類的拷貝構造函數中傳入派生類對象的引用,并且基類的拷貝構造函數的參數必須是基類的引用,此時我們就可以通過切片把派生類對象中基類的一部分切給要進行拷貝構造的派生類的基類部分了,可謂是非常的優美。下面我將通過一個例子讓各位了解用法。
?
class Person
{
public:Person(Person& s1):t_name(s1.t_name){}
protected:string t_name;
};
?
class Student :public Person
{
public:Student(Student& s1): Person(s1),_name(s1._name) ?//直接把對象傳到基類即可{}
protected:string _name;
};?
????????4.派生類的operator=必須要調用基類的operator=完成基類的復制。需要注意的是派生類的operator=隱藏了基類的operator=,所以顯示調用基類的operator=,需要指定基類作用域。這個為什么顯示調用和拷貝構造是一樣的,這里我就不詳說了,不過這里也是牽扯到了上篇博客的一個小知識點:隱藏的知識,當函數名相同的時候,并且函數分別屬于基類的作用域和派生類的作用域時,那么此時就是構成了隱藏關系。而基類和子類的賦值運算符重載名字是一模一樣的,所以構成了隱藏關系,此時我們需要指定類域才可以調用基類的賦值運算符重載,它的用法如下所示:
class Person
{
public:Person& operator=(const Person& s1){t_name = s1.t_name;return *this;}
protected:string t_name;
};
?
class Student :public Person
{
public:Student& operator=(const Student& s1){Person::operator=(s1); //一定要指明基類的作用域_name = s1._name;return *this;}
protected:string _name;
};
????????5.派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。這個也是牽扯到了我們第一次初學C++類和對象的時候學到的知識:后定義的先析構,因為派生類的成員是后來定義的,所以它是最開始析構的,所以我們先清理派生類的成員函數,在進行基類成員的清理。當然,析構函數我們最好還是不要顯示的調用,至于為何,請看第七點。
????????6.派生類對象析構清理先調用派生類析構再調基類的析構。這個上面解釋了,我就不細說了。
????????7.因為多態中一些場景析構函數需要構成重寫,重寫的條件之一是函數名相同(這個我們多態章節會講解)。那么編譯器會對析構函數名進行特殊處理,處理成destructor(),所以基類析構函數不加virtual的情況下,派生類析構函數和基類析構函數構成隱藏關系。所以我的建議就是析構函數我們還是老老實實的讓派生類自己去調用基類的析構函數吧,這樣比較方便。
????????各位讀者,看到這里是不是手癢想當"代碼界的滅霸"了?別急著敲繼承的響指!今天咱們要聊的C++貴族禮儀——如何讓類在族譜上寫下"此脈單傳,永不加粗"!
????????想打造這種孤高の王族血統?可不是給構造函數上鎖那么簡單(那招就像在城堡門口掛"內有惡犬",防得住新手防不住杠精)。C++11早給我們準備了基因改造藥劑——final關鍵字!這玩意兒比皇后的毒蘋果還好使:
1.2.實現一個不能被繼承的類
方法一
????????不知道各位讀者是否還記得我上篇文章所講述的繼承方式,基類使用private成員限定符限定的成員是不可以被派生類使用的,那么這就相當于不可以被派生類繼承(當然,還是繼承下來的,只不過不允許使用)了。所以當我們讓基類的構造函數被private限定以后,那么此時派生類就無法調用基類的默認構造函數,從而導致派生類無法實例化出對象,這就意味這個類是無法被繼承的。如下所示:
class Person
{
private:Person() {}
};
?
class Studnet : public Person
{
public:Studnet() {}
public:
};
????????不過這個做法是有點不優美的,因為它有個致命的缺陷:“家賊難防”,如果給派生類悄悄的開一個后門:friend通行證(友元),那么此時派生類依然可以調用基類的拷貝構造函數:
class 薛定諤的保險箱 {
private:薛定諤的保險箱() {}friend class 量子穿墻術; // 偷偷塞鑰匙
};
?
class 量子穿墻術 : public 薛定諤的保險箱 { // 竟然成功繼承!科學倫理委員會震怒
};
????????這個方法的局限性很大,所以準確來說:通過私有化構造函數可以制造偽·不可繼承類,但需要配合杜絕friend后門,而C++11的final關鍵字才是正宗的絕育手術刀~(≧?≦)/~(這就是方法二)。
方法二
????????C++11新增加了一個關鍵字:final關鍵字,final修改基類,那么派生類就無法繼承了,它的用法如下所示:
class Person final
{
public:Person() {}
};
?
class Studnet : public Person
{
public:Studnet() {}
public:
};
2.繼承和友元
????????友元關系不能繼承,也就是說基類友元不能訪問派生類私有和保護成員。這個很好理解,下面我就一個有趣的例子來給各位說明一下:
class 祖傳咸魚配方 { // 傳男不傳女的獨門秘方
private:int 祖傳鹽量 = 999; // 傳家寶級別的鹽值friend class 大廚; // 授予傳功長老權限
};
?
class 改良版咸魚配方 : public 祖傳咸魚配方 {
private: int 科技鹽量 = 666; // 偷偷加了海克斯科技
};
?
class 大廚 {
public:void 烹飪秘術(祖傳咸魚配方& 老壇) {老壇.祖傳鹽量 = 9527; // 暢通無阻(畢竟有friend通行證)}
?void 黑暗料理(改良版咸魚配方& 新品) {// 新品.科技鹽量 = 1314; // 報錯!編譯器怒斥:你只是他爹的基友!}
};
?
// 劇情彩蛋:就算逆天改命也不行!
class 逆子配方 : public 祖傳咸魚配方 {friend class 大廚; // 試圖繼承爹的社交圈
private:int 叛逆鹽量 = 233;
};
?
// 大廚試圖搞事情:
void 偷天換日() {逆子配方 黑化版;黑化版.叛逆鹽量 = 666; // 依然報錯!編譯器冷笑:父輩的friend不是你的ATM機!
}
這波操作生動詮釋了:
友元關系比鋼鐵直男還直——絕不拐彎繼承(派生類不會自動獲得基類友元)
友元權限比小區門禁還嚴——只認身份證原件(即便派生類主動示好,基類友元也摸不到派生類的私有成員)
想開后門?除非上演《無間道》——在派生類里重新聲明friend(但這樣可就背叛革命了)
3.繼承和靜態成員
????????基類定義了static靜態成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個派生類,都只有一個static成員實例。通過一個簡單的例子就可以知曉這個定義:
class 魔法始祖 {
public:static int 魔力源泉; // 全魔法界共享的充電寶
};
?
int 魔法始祖::魔力源泉 = 100; // 在霍格沃茨地窖初始化
?
class 火系法師 : public 魔法始祖 {};
class 水系法師 : public 魔法始祖 {};
class 風系法師 : public 魔法始祖 {};
?
int main() {火系法師 甘道夫;水系法師 梅林;風系法師 薩爾;
?// 所有法師共享同一個魔法池甘道夫.魔力源泉 += 50; ?// 火法師給充電寶續費cout << 梅林.魔力源泉 << endl; ? // 輸出150(水系躺著蹭網)薩爾.魔力源泉 -= 70; ? ? // 風系法師偷偷下載小電影cout << 魔法始祖::魔力源泉 << endl; // 輸出80(祖宗家底被敗光)// 見證奇跡的時刻!cout << &甘道夫.魔力源泉 << " ←火法地址\n"<< &梅林.魔力源泉 ? << " ←水法地址\n"<< &薩爾.魔力源泉 ? << " ←風法地址" << endl;// 三個地址完全相同,實錘共享內存
}
?這段代碼生動詮釋了:
靜態成員就像家族銀行賬戶——所有子孫刷的都是同一張卡
無論通過基類還是派生類訪問,操作的都是同一個內存地址
任一派生類搞事情,全家族成員都要背鍋(值同步變化)
4.多繼承以及其菱形繼承問題
4.1.繼承模型
????????單繼承:一個派生類只有一個直接基類時稱這個繼承關系為單繼承。這個是比較好理解的,因為它的定義就和它的名字一樣,下面我給出示例圖:
????????單繼承算是一個比較正常的繼承了,但是如果單繼承變為了多繼承,那么會很逆天!
????????(敲黑板)各位程序員請注意,咱們今天要聊的是C++里的"多重身份危機"——多繼承和它的狗血連續劇"菱形繼承"!
第一幕:多繼承の修羅場
想象一下,你是個時間管理大師:
class 社畜 : public 打工人, public 乙方舔狗, public 深夜碼農 {};
這就是多繼承——一個類同時認多個爹。內存布局就像疊羅漢,誰先繼承誰在下面:
[打工人] → [乙方舔狗] → [深夜碼農] → [社畜的私人數據]
????????但問題來了——如果多個爹有同名成員,編譯器直接懵逼:
社畜 張三;
張三._存款 = -10086; // 編譯器怒吼:你三個爹都有存款,我該改哪個?!
第二幕:菱形繼承の家族倫理劇
????????來看這張祖傳關系圖:
????????結果Assistant繼承時,老祖宗的基因復制了兩份!這就是菱形繼承的血淚史:
Assistant 小王;
小王.Student::_name = "學渣";
小王.Teacher::_name = "教授";
// 實際小王體內住著兩個老祖宗,精分現場!
????????內存布局宛如祖傳玉佩摔成兩半:
[Student版Person] → [Teacher版Person] → [Assistant數據]
????????此時訪問_name
就像問媽媽和老婆掉水里救誰——必須指定爹名:
cout << 小王.Student::_name; // "學渣"
cout << 小王.Teacher::_name; // "教授"
第三幕:虛繼承の公證處協議
????????C++祭出大招:虛繼承!相當于給老祖宗做公證:
class Student : virtual public Person {}; // 公證聲明
class Teacher : virtual public Person {}; // 同上
????????此時孫輩的內存布局變成:
[虛表指針] → [Student數據] → [虛表指針] → [Teacher數據] → [老祖宗Person] → [Assistant數據]
????????雖然解決了數據冗余,但代價是:
構造順序堪比宮廷劇——孫輩得直接給老祖宗上供:
Assistant::Assistant() : Person("工具人"), Student(), Teacher() {} // 必須親自初始化老祖宗
訪問速度像去公證處蓋章——多繞一層指針
終幕:多繼承の終極考題
來看這道送命題:
Basel b1; Base2 b2;
class 縫合怪 : public Basel, public Base2 {};
縫合怪 obj;
Basel* p1 = &obj;
Base2* p2 = &obj;
void* p3 = &obj;
指針地址關系是?
A: p1 == p2 == p3 ?(想得美)
B: p1 < p2 < p3 ?(內存不是等差數列)
C: p1 == p3 != p2 ?(Basel先繼承,地址最低)
D: p1 != p2 != p3 ?(p3和p1指向同一起點)
????????總結陳詞: 多繼承就像同時認多個干爹——給錢時很爽,爭家產時頭大。而菱形繼承則是家族內斗的終極形態,虛繼承雖然能維穩,但操作難度堪比處理婆媳關系。珍愛生命,遠離菱形!(Java笑而不語)
4.2.彩蛋:IO庫中的菱形虛擬繼承
????????其實我們日常使用iostream庫就是一個菱形虛擬繼承,其結構圖如下所示:
5.繼承和組合
????????各位讀者請注意,最后咱們要聊的是面向對象界的"婆媳關系"——繼承和組合!這可是代碼界的"到底該聽媽的還是聽媳婦的"終極難題!
第一回合:繼承——代碼界的家族企業
繼承就像你爹開公司,你直接當太子爺繼承皇位:
class 富二代 : public 土豪爹 {// 自動獲得爹的別墅、跑車、黑卡
};
優點:
是親生的(is-a關系),直接拿爹的全部家當(包括私房錢)
白嫖式開發(白箱復用),連爹的日記本都能翻
缺點:
爹改遺囑(修改基類),兒子當場破產(代碼爆炸)
耦合度堪比連體嬰,爹感冒兒子必發燒
經典翻車現場:
class 爹 {
public:void 傳家寶() { cout << "洛陽鏟"; }
};
?
class 兒子 : public 爹 {};
?
// 某天爹考古入魔:
class 爹 {
public:void 傳家寶() { cout << "盜墓筆記"; } // 從工具升級成知識
};
?
// 兒子:???我鏟子呢???
第二回合:組合——代碼界的樂高大師
組合就像自己開公司,雇個專業經理人:
class 打工人 {class 肝帝程序員 員工; // 組合一個996戰士class AWS云服務器 設備; // 再組個燒錢神器
};
優點:
是老板(has-a關系),只關心KPI(接口),不管員工私生活(黑箱復用)
耦合度堪比塑料友情,隨時換掉摸魚員工(維護性好)
經典操作:
class 特斯拉 {Battery 電池; ? // 想換寧德時代?換!Motor 電機; ? ? // 想用國產?換!Autopilot 智駕; // 想用華為?換!(馬斯克震怒)
};
// 組合就是:沒有什么是換零件解決不了的
第三回合:繼承 vs 組合の世紀對決
對比項 | 繼承 | 組合 |
---|---|---|
關系 | 你是我兒子(is-a) | 你是我工具人(has-a) |
耦合度 | 臍帶級綁定 | 點贊之交 |
封裝性 | 爹的內褲都被看光 | 打碼級保護 |
改需求傷害 | 家族式團滅 | 換個零件就能活 |
適用場景 | "狗是動物"這種鐵律 | "車有輪胎"這種可拆卸關系 |
終極大招:面向對象の生存法則
能組合就別繼承——少認爹少背鍋,多個爹多個墳頭
非要繼承時:
確認關系鐵如"企鵝是鳥"(雖然它不會飛)
準備迎接"爹動一下,兒改十行"的刺激生活
多態是繼承の免死金牌——當你要召喚"虛函數"神龍時,該認爹還得認
總結陳詞:
繼承像結婚——高風險高回報,且行且珍惜
組合像戀愛——合則來不合則換,自由無負擔
記住:代碼不是血緣關系,少繼承,多組合,你的頭發會感謝你!
6.總結
????????本文到這里也就結束嘍,今天我們完成了繼承的全部內容的講解,下面我簡單的總結一下本文講解的內容。
本章節の知識點爆米花
默認成員函數:四個"祖傳家產"(構造/拷貝/析構/賦值),教你如何打造C++界的丁克家族(final大法)
繼承與友元:塑料兄弟情——基類友元不會遺傳,就像你爹的兄弟不會給你壓歲錢!
靜態成員:全家族共享的祖傳充電寶(static),一人改參數,全家炸電路
多繼承:修羅場の生存指南——內存布局疊羅漢,菱形繼承精分現場,虛繼承公證處の騷操作
組合vs繼承:認爹不如雇工具人(has-a),代碼界的樂高哲學——寧可多拼積木,少背族譜
前方高能預警
????????你以為繼承的狗血劇這就結束了?Too young!下一章我們將迎來面向對象三幻神の最終形態——多態!
屆時您將看到:
虛函數:C++版的"我變禿了也變強了"
動態綁定:運行時の分身術,讓對象學會影流之主の秘技
純虛函數:抽象類の"畫餅大法",堪比老板的年終獎承諾
多態の黑暗面:虛表指針の內存迷蹤,性能刺客の背刺警告
(劇透小劇場)
class 打工人 {
public:virtual void 摸魚() = 0; // 老板:這是純虛函數,你必須實現!
};
?
class 程序員 : public 打工人 {
public:void 摸魚() override { cout << "在GitHub刷綠格子" << endl; // 老板:這TM也算工作?!}
};
};
????????準備好迎接"一個接口,千種姿勢"的神奇世界了嗎?下期我們將用多態實現:同一行代碼,白天當社畜,晚上變蝙蝠俠的魔法操作!(≧?≦)ノ
? ? ? ? 各位大佬下篇文章見啦!