【C++】繼承和多態擴展學習

目錄

1. 菱形虛擬繼承原理剖析

1.1.虛基表

2. 單繼承和多繼承的虛函數表深入探索

2.1 單繼承虛函數表深入探索

2.2 多繼承虛函數表深入探索

?編輯

2.3 菱形繼承、菱形虛擬繼承

3. 繼承和多態考察的一些常見問題


1. 菱形虛擬繼承原理剖析

繼承的文章中我們講到C++的多繼承就會引發一些場景出現菱形繼承,有了菱形繼承,就會出現數據冗余和二義性的問題,C++又引入了虛繼承來解決數據冗余和二義性。

class Person
{
public:string _name; // 姓名
};
// class Student : public Person
class Student : virtual public Person
{
protected:int _num; // 學號
};
// class Teacher : public Person
class Teacher : virtual public Person
{
protected:int _id; // 職工編號
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修課程
};
void Test()
{// 這樣會有二義性無法明確知道訪問的是哪一個Assistant a;a._name = "peter";// 需要顯示指定訪問哪個父類的成員可以解決二義性問題,但是數據冗余問題無法解決a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}

? 為了研究虛擬繼承原理,我們給出了一個簡化的菱形繼承繼承體系,再借助內存窗口觀察對象成員的模型。要注意的是這里必須借助內存窗口才能看到真實的底層對象內存模型,vs編譯器的監視窗口是經過特殊處理的,以它的角度給出了一個方便看的樣子,但并不是本來的樣子。但是有時想看清真實的內存模型,往往需要借助內存窗口。

1.1.虛基表

在前面繼承的文章中,我們了解到為了避免菱形繼承所導致的數據冗余,子類會將重復繼承的部分合并為一份,放在類的最上或者最下面。但是這里引出一個問題是當我們通過父類指針訪問子類對象,這是對于合并的部分,要如何確定位置呢?大家可能覺得合并的部分不是已經放在最后或者最上面了嗎?但是這里如果我們使用不同的父類指針,偏移多少才能到底呢?因此需要虛基表記錄父類對應的偏移量。

虛基表是編譯器為了解決多重繼承場景下的菱形繼承問題所設計的,虛基表(vbtable)通過記錄虛基類實例的偏移量來指示派生類如何訪問唯一的虛基類實例。當子類通過多繼承方式繼承多個具有共同基類的父類時,如果不使用虛繼承,子類會包含多分共同基類的數據,這會導致數據冗余。而是要虛繼承,子類中只會包含一份共同基類的數據。

? 通過下面的簡化菱形虛擬繼承模型,我們可以看到,D對象中的B和C部分中分別包含一個指向虛基表的指針,B指向的虛基表中存儲了B對象部分距離公共的A的相對偏移量距離,C指向的虛基表中存儲了C對象部分距離公共的A的相對偏移量距離。這樣公共的虛基類A部分在D對象中就只有一份了,這樣就解決了數據冗余和二義性的問題。


? 通過B的對象模型,我們發現菱形虛擬繼承中B和C的對象模型跟D保持的一致的方式去存儲管理A,這樣當B的這指針訪問A時,無論B指針切片指向D對象,還是B指針直接指向B對象,訪問A成員都是通過虛基表指針的方式查找到A成員再訪問。

class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._a = 3;d._b = 4;d._c = 5;d._d = 6;return 0;
}
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._a = 3;d._b = 4;d._c = 5;d._d = 6;B b;b._a = 7;b._b = 8;// B的指針指向B對象B *p2 = &b;// B的指針指向D對象切片B *p1 = &d;// p1和p2分別對指向的_a成員訪問修改// 分析內存模型,我們發現B對象也使用了虛基表指向A成員的模型// 所以打開匯編我們看到下面的訪問_a的方式是一樣的p1->_a++;p2->_a++;return 0;
}

2. 單繼承和多繼承的虛函數表深入探索

2.1 單繼承虛函數表深入探索

? vs編譯器的監視窗口是經過特殊處理的,以它的角度給出了一個方便看的樣子,但并不是本來的樣子。多態部分我們講了,虛函數指針都要放進虛函數表,這里我們通過監視窗口觀察Derive對象,看不到func3和func4在虛表中,借助內存窗口可以看到一個地址,但是并不確認是不是func3和func4的地址。所以下面我們寫了一份特殊代碼,通過指針的方式,強制訪問了虛函數表,調用了虛函數,確認繼承中虛函數表中的真實內容。

class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }private:int a;
};
class Derive : public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }void func5() { cout << "Derive::func5" << endl; }private:int b;
};
typedef void (*VFPTR)();
void PrintVTable(VFPTR vTable[])
{// 依次取虛表中的虛函數指針打印并調用。調用就可以看出存的是哪個函數cout << " 虛表地址>" << vTable << endl;// 注意如果是在g++下面,這里就不能用nullptr去判斷訪問虛表結束了for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%p,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;// 32位程序的訪問思路如下:// 需要注意的是如果是在64位下,指針是8byte,對應程序位置就需要進行更改// 思路:取出b、d對象的頭4bytes,就是虛表的指針,前面我們說了虛函數表本質是一個存虛函數指針的指針數組,vs下這個數組最后面放了一個nullptr,g++ 下面最后沒有nullptr// 1.先取b的地址,強轉成一個int*的指針// 2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針// 3.再強轉成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。// 4.虛表指針傳遞給PrintVTable進行打印虛表// 5.需要說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的 -生成 - 清理解決方 案,再編譯就好了。 VFPTR *vTable1 = (VFPTR *)(*(int *)&b);PrintVTable(vTable1);VFPTR *vTable2 = (VFPTR *)(*(int *)&d);PrintVTable(vTable2);return 0;
}

2.2 多繼承虛函數表深入探索

class Base1
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }private:int b1;
};
class Base2
{
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }private:int b2;
};
class Derive : public Base1, public Base2
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }private:int d1;
};
typedef void (*VFPTR)();
void PrintVTable(VFPTR vTable[])
{cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%p,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;VFPTR *vTableb1 = (VFPTR *)(*(int *)&d);PrintVTable(vTableb1);VFPTR *vTableb2 = (VFPTR *)(*(int *)((char *)&d + sizeof(Base1)));PrintVTable(vTableb2);Base1 *p1 = &d;p1->func1();Base2 *p2 = &d;p2->func1();d.func1();return 0;
}

? 跟前面單繼承類似,多繼承時Derive對象的虛表在監視窗口也觀察不到部分虛函數的指針。所以我們一樣可以借助上面的思路強制打印虛函數表。


? 需要注意的是多繼承時,Derive中同時繼承了Base1和Base2,內存中先繼承的對象在前面,并且Derive中包含的Base1和Base2各有一張虛函數表,通過觀察我們發現Derive沒有重寫的虛函數func3,選擇放在先繼承的Base1的虛函數表中。


? 另外需要注意的是,有些細心的讀者發現Derive對象中重寫的Base1虛表的func1地址和重寫Base2虛表的func1地址不一樣,這是為什么呢?這個問題還比較復雜。需要我們分別對這兩個函數進行多態調用,并翻閱對應的匯編代碼進行分析,才能捋清楚問題所在。這里簡單說一個結論就是本質Base2虛表中func1的地址并不是真實的func1的地址,而是封裝過的func1地址,因為Base2指針p2指向Derive時,Base2部分在中間位置,切片時,指針會發生偏移,那么多態調用p2->func1()時,p2傳遞給this前需要把p2給修正回去指向Derive對象,因為func1是Derive重寫的,里面this應該是指向Derive對象的

2.3 菱形繼承、菱形虛擬繼承

實際中我們不建議設計出菱形繼承及菱形虛擬繼承,一方面太復雜容易出問題,另一方面這樣的模
型,訪問基類成員有一定得性能損耗。所以菱形繼承、菱形虛擬繼承我們的虛表本文就不看了,一般我們也不需要研究清楚,因為實際中很少用。好奇心強的讀者,可以去看下面的兩篇鏈接文章。

1. C++ 虛函數表解析
2. C++ 對象的內存布局

class A
{
public:virtual void func1() {}public:int _a;
};
class B : virtual public A
{
public:virtual void func1() {}virtual void func2() {}public:int _b;
};
class C : virtual public A
{
public:virtual void func1() {}virtual void func3() {}public:int _c;
};
class D : public B, public C
{
public:D(): _d(1){}inline virtual void func1() {}virtual void func4() {}public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._a = 3;d._b = 4;d._c = 5;d._d = 6;return 0;
}

3. 繼承和多態考察的一些常見問題

1. 什么是多態?答:參考前面多態文章


2. 什么是重載、重寫(覆蓋)、重定義(隱藏)?答:參考前面多態文章


3. 多態的實現原理?答:參考前面多態文章


4. inline函數可以是虛函數嗎?答:可以,不過編譯器就忽略inline屬性,這個函數就不再是inline屬性,因為虛函數要放到虛表中去,也就是說inline屬性和虛函數屬性是不同同時存在的。


5. 靜態成員可以是虛函數嗎?答:不能,因為靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。


6. 構造函數可以是虛函數嗎?答:不能,因為對象中的虛函數表指針是在構造函數初始化列表階段才初始化的。


7. 析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?答:可以,并且最好把基類的析構函數定義成虛函數。參考本文內容


8. 對象訪問普通函數快還是虛函數更快?答:首先如果是普通對象調用,是一樣快的。如果是指針或者是引用對去調用,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找。


9. 虛函數表是在什么階段生成的,存在哪的?答:虛函數表是在編譯階段就生成的,一般情況下存在代碼段(常量區)的。


10. C++菱形繼承的問題?虛繼承的原理?答:參考前面繼承文章。注意這里不要把虛函數表和虛基表搞混了。


11. 什么是抽象類?抽象類的作用?答:參考前面繼承文章;抽象類強制重寫了虛函數,另外抽象類體現出了接口繼承關系。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/90142.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/90142.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/90142.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Visual Studio Code 遠端云服務器開發使用指南

目錄 一、下載安裝 1、官方下載 2、下載加速方案 二、基于Ubuntu系統的開發環境搭建方案 1、開發環境配置 2、云服務器架構 3、工作流程關系 4、總結 三、推薦插件 1、免配置插件 1. Remote-SSH - 遠程登錄Linux服務器 2. C/C - 必備的C/C開發插件 3. C/C Extensi…

技術演進中的開發沉思-41 MFC系列:定制 AppWizard

MFC開發&#xff0c;最為重要的無非就是用“MFC AppWizard” 對話框做開發了&#xff0c;第一次使用感覺像拆收音機的孩子 —— 左邊是項目類型選擇&#xff0c;右邊是一堆打勾的選項&#xff0c;點完 “完成”&#xff0c;屏幕上就冒出了能直接編譯運行的窗口程序。那時還不知…

Libevent(3)之使用教程(2)創建事件

Libevent(3)之使用教程(2)創建事件 Author: Once Day Date: 2025年6月29日 一位熱衷于Linux學習和開發的菜鳥&#xff0c;試圖譜寫一場冒險之旅&#xff0c;也許終點只是一場白日夢… 漫漫長路&#xff0c;有人對你微笑過嘛… 本文檔翻譯于&#xff1a;Fast portable non-bl…

Kotlin 作用域函數 let 的實現原理

Kotlin 中的 let 是一個 標準庫擴展函數&#xff0c;它廣泛用于作用域函數&#xff08;Scope Functions&#xff09;中&#xff0c;尤其適用于對可空對象&#xff08;nullable&#xff09;做非空判斷并執行代碼塊的場景。 示例代碼 val name: String? "123" name?…

從FDTD仿真到光學神經網絡:機器學習在光子器件設計中的前沿應用工坊

FDTD仿真與光學神經網絡的基礎概念 FDTD&#xff08;時域有限差分&#xff09;是一種數值方法&#xff0c;用于求解麥克斯韋方程組&#xff0c;廣泛應用于光子器件設計。光學神經網絡通過光波導、衍射元件等物理結構實現矩陣運算&#xff0c;具有低能耗、高并行的優勢。 機器學…

在Ubutu22系統上面離線安裝Go語言環境【教程】

0.引言 Go語言&#xff08;又稱Golang&#xff09;是Google開發的一種靜態強類型、編譯型、并發型編程語言&#xff0c;由Robert Griesemer、Rob Pike和Ken Thompson于2007年開始設計&#xff0c;2009年正式發布。 1.到官網下載壓縮包 2.從win10系統離線上傳壓縮包給ubuntu22…

CMake實踐:CMake3.30版本之前和之后鏈接boost的方式差異

目錄 1.背景 2.boost引入CMake時機 3.CMake 3.30 之前&#xff08;含 3.29&#xff09;鏈接 Boost 的方式 4.CMake 3.30 及之后鏈接 Boost 的方式 5.CMake3.30后引入Boost的步驟 6.遷移建議&#xff08;3.30 之前 → 3.30 之后&#xff09; 7.CMake 3.30 移除FindBoost的…

告別掛馬風險!PBootCMS完美替代方案BadouCMS

開發企業網站時一直比較喜歡用pbootcms,標簽套用很簡單&#xff0c;使用也方便。 但是pbootcms一直有被掛馬的問題&#xff0c;官方好像也不怎么更新了&#xff01;換過好幾個cms&#xff0c;比如eyoucms、dedecms、帝國等等&#xff0c;感覺都不怎么能用得習慣&#xff0c;還…

開發者如何集成AI繪畫?智創聚合API簡化Midjourney接入

在 AI 繪畫領域&#xff0c;Midjourney 的大名如雷貫耳&#xff0c;其強大的圖像生成能力&#xff0c;能將我們腦海中的奇思妙想&#xff0c;迅速轉化為精美的視覺畫面&#xff0c;深受設計師、藝術家以及廣大創意愛好者的青睞。然而&#xff0c;使用 Midjourney 的過程中&…

pycharm回車、刪除、方向鍵和快捷鍵等不能使用原因

解決方法 &#xff1a;菜單欄中的Tools取消勾選Vim Emulator 原因 &#xff1a;新版的pycharm安裝中&#xff0c;默認安裝了vim擴展&#xff0c;一旦安裝了pycharm在編寫代碼時會默認使用Vim編輯器

修復ffmpeg.dll丟失錯誤|6種解決ffmpeg.dll方法詳細教程

看到電腦提示“ffmpeg.dll丟失”&#xff0c;很多人會懵。ffmpeg.dll 是個處理視頻、音頻的關鍵文件。它要是沒了或壞了&#xff0c;軟件就打不開或直接閃退。常見原因是軟件安裝不全、文件被刪、或者中病毒。下面說說它是干嘛的&#xff0c;再給解決辦法。一.ffmpeg.dll 到底是…

OkHttp 與 Stetho 結合使用:打造強大的 Android 網絡調試工具鏈

前言在 Android 應用開發過程中&#xff0c;網絡請求的調試一直是一個重要但具有挑戰性的環節。Facebook 開發的 Stetho 是一個強大的調試工具&#xff0c;當它與 OkHttp 結合使用時&#xff0c;可以為我們提供前所未有的網絡請求洞察能力。本文將詳細介紹如何將這兩者結合使用…

LangGraph教程10:LangGraph ReAct應用

文章目錄 ReAct 預構建的代理 向 ReAct 代理添加記憶 向 ReAct 代理添加系統提示 向 ReAct 代理添加人機交互 ReAct 官方文檔地址:https://langchain-ai.github.io/langgraph/how-tos/#prebuilt-react-agent 中文文檔地址:https://www.aidoczh.com/langgraph/how-tos/#react…

安卓第一個項目

測試所有攝像頭 安卓CameraX&#xff1a;https://developer.android.com/media/grow/spatial-audio?hlzh-cn 1、MainActivity.java // 定義包名 package com.mms.densenapplication;// 引入 AppCompatActivity&#xff0c;支持兼容性更強的 Activity import androidx.appcompa…

Google Gemini 體驗

文章中代碼倉庫 gemini 谷歌推出的 AI 只能模型 Gemini官網Gemini ChatGemini開發者文檔Gemini SDK 所有模型 模型變體輸入輸出優化目標Gemini 2.5 Pro gemini-2.5-pro音頻、圖片、視頻、文本和 PDF文本增強的思考和推理能力、多模態理解能力、高級編碼能力等Gemini 2.5 Fla…

Trae安裝指定版本的插件

前情 Trae是屬于國產的跟 Cursor類似的AI編程IDE&#xff0c;我也是第一時間體驗Trae的&#xff0c;雖然相比Cursor弱了一些&#xff0c;但是也絕對勝任了&#xff0c;前端因為排隊問題我轉戰了Cursor&#xff0c;等到Trae出收費模式前&#xff0c;我已經辦了Cursor會員了&…

【技術追蹤】用于醫學圖像合成和分割的噪聲一致孿生擴散模型(CVPR-2025)

孿生擴散模型&#xff0c;生成息肉圖像用于提升分割性能&#xff01; 論文&#xff1a;Noise-Consistent Siamese-Diffusion for Medical Image Synthesis and Segmentation 代碼&#xff1a;https://github.com/Qiukunpeng/Siamese-Diffusion 0、摘要 深度學習已徹底革新醫學影…

Crontab詳解

crontab是Unix/Linux系統中用于設置周期性任務的工具&#xff0c;通過編輯配置文件實現定時執行命令或腳本。以下是其語法規則和核心要點&#xff1a; 一、基本格式 * * * * * command - - - - - | | | | | | | | | ----…

中國1km逐月潛在蒸散發數據集 - matlab按shp批量裁剪

中國1km逐月潛在蒸散發數據集 - matlab按shp批量裁剪 1. 數據概述 2 利用掩膜文件對數據進行裁剪 3 完整代碼 4 結語 本篇繼續處理氣象數據,中國1km逐月潛在蒸散發數據集同前節介紹的中國1km降水數據集一樣,都可以從國家青藏高原科學數據中心獲得,數據具有同樣的空間分辨率(…

Node.js鏈接MySql

前言&#xff1a; 在現代 Web 開發和后端服務中&#xff0c;Node.js 因其高性能和異步特性被廣泛使用。MySQL 作為流行的關系型數據庫之一&#xff0c;提供了穩定高效的數據存儲和管理能力。將 Node.js 與 MySQL 結合&#xff0c;可以構建強大的數據驅動型應用。 一、環境準備…