C++對象在繼承情況下的記憶體佈局

来源:https://www.cnblogs.com/dishengAndziyu/archive/2019/05/24/10916130.html
-Advertisement-
Play Games

1,C++ 中繼承是非常重要的一個特性,本節課研究在繼承的情形下,C++ 的對象模 型又有什麼不同; 2,繼承對象模型(最簡單的情況下): 1,在 C++ 編譯器的內部類可以理解為結構體; 2,子類是由父類成員疊加子類新成員得到的; 1,代碼示例: 1 class Derived : public ...


1,C++ 中繼承是非常重要的一個特性,本節課研究在繼承的情形下,C++ 的對象模 型又有什麼不同;  

 

2,繼承對象模型(最簡單的情況下):

    1,在 C++ 編譯器的內部類可以理解為結構體;

    2,子類是由父類成員疊加子類新成員得到的;

       1,代碼示例:

1 class Derived : public Demo

2 {

3 int mk;

4 }; 

       2,對象排布:

 

           1,在對象模型中,先排布父類對象模型,再排布子類對象模型,見 本文3中內容;

   

3,繼承對象模型初探編程實驗:

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class Demo
 7 {
 8 protected:
 9     int mi;
10     int mj;
11 public:
12     virtual void print()
13     {
14         cout << "mi = " << mi << ", "
15              << "mj = " << mj << endl;
16     }
17 };
18 
19 class Derived : public Demo
20 {
21     int mk;
22 public:
23     Derived(int i, int j, int k)
24     {
25         mi = i;
26         mj = j;
27         mk = k;
28     }
29     
30     void print()
31     {
32         cout << "mi = " << mi << ", "
33              << "mj = " << mj << ", "
34              << "mk = " << mk << endl;
35     }
36 };
37 
38 struct Test
39 {
40     void* p;  // 為了證明 C++ 編譯器真的會在對象中塞入一個指針成員變數,且指針放在最開始的位元組處;    
41     int mi;
42     int mj;
43     int mk;
44 };
45 
46 int main()
47 {
48     cout << "sizeof(Demo) = " << sizeof(Demo) << endl; // 8 bytes         
49     cout << "sizeof(Derived) = " << sizeof(Derived) << endl;  // 12 bytes
50     
51     Derived d(1, 2, 3);
52     Test* p = reinterpret_cast<Test*>(&d);
53     
54     cout << "Before changing ..." << endl;
55     
56     d.print();  // mi = 1, mj = 2, mk = 3;
57 
58     /* 通過 p 對象改變成員變數的值,這裡加了 p 指針後任然能夠成功的訪問; */    
59     p->mi = 10;
60     p->mj = 20;
61     p->mk = 30;
62     
63     cout << "After changing ..." << endl;
64     
65     d.print();  // mi = 10, mj = 20, mk = 30;在外界訪問不到的保護成員變數的值被改變了,改變是因為 d 對象的記憶體分佈 Test 結構體的(此時類中未有虛函數,Test 中未有 空指針),因此可以用 p 指針改變 d 對象當中成員變數的值;
66     
67     return 0;
68 }

 

   

4,多態對象模型:

    1,C++ 多態的實現原理:

       1,當類中聲明虛函數時,編譯器會在類中生成一個虛函數表;

       2,虛函數表是一個存儲成員函數地址的數據結構;

           1,存儲虛函數成員地址的數據結構;

       3,虛函數表是由編譯器自動生成與維護的;

       4,virtual 成員函數會被編譯器放入虛函數表中;

           1,這個表是給對象使用的;

           2,對象在創建時,在內部有一個虛函數表指針,這個指針指向虛函數表;

       5,存在虛函數時,每個對象中都有一個指向虛函數表的指針;

    2,框圖展示:

  1,框架一

 

         1,編譯父類時,編譯器發現了 virtual 成員函數,因此編譯器創建了一個虛函數表,並且將虛函數的地址放到了虛函數表裡面;

         2,編譯子類時,繼承自 Demo,編譯器發現重寫了 add 函數,因此必須是虛函數,於是編譯器就為子類也生成一張虛函數表,並且也會在虛函數表中放入重寫過後的 add 虛函數的地址;

  2,框架二

 

          1,當創建父類對象的時候,會為 Demo 對象自動的塞入一個指針 VPTR,也 就是如果類中有虛函數的話,在最終生成類對象的時候,會被編譯器強         制賽一個指針成員變數,這個指針成員變數對於程式員是不可見的,但是它確確實實的會存在對象當中,這個指針成員變數指向了虛函數表;

         2,當創建子類對象的時候,會為 Derived 對象自動的塞入一個指針 VPTR,其是一個虛函數表指針,最終會指向創建的虛函數表;

         3,通過 p 指針來調用虛函數 add(),編譯器就會判斷,當前調用的 add() 函數是不是虛函數,如果是虛函數,編譯器肯定可以知道這個虛函數地址位於虛函數表裡面,編譯器根據 p 指向的實際對象通過強行塞入的指針來查找虛函數表,然後在虛函數表裡面取得具體的 add() 函數地址,然後通過這個地址來調用,這樣子就實現了多態;

         4,當通過指針調用的函數不是虛函數,這時就不會查找虛函數表了,此時就能夠直接確定函數地址;

  3,框架三

 

         1,紅色箭頭代表定址操作,即代表確定最後 add() 地址的操作;

         2,通過 p 指針找到具體的對象,然後通過具體的對象找到這個虛函數表指針,之後通過虛函數表指針找到虛函數表,在虛函數表裡面通過查找找到最後的函數地址;

            3,多態發生的情形下,調用一個函數要經歷三次定址,這個調用效率不會高,即虛函數的調用效率低於普通的成員函數,C++ 中的多態是通過犧牲效率得到的;

            4,所以在寫 C++ 面向對象程式的時候,要考慮一個成員函數有沒有必要成為虛函數,因為每當我們定義一個虛函數,就會犧牲一定的效率,而 C++ 因為繼承了 C 語言的特性,所以天生就要高效,既要高效,又要實現多態,這就交給了程式員了;

            5,虛函數中的指針指向具體對象,具體對象指針指向虛函數表,虛函數表中的指針指向具體的虛函數實現函數;

      

5,多態本質分析編程實驗(用 C 實現多態):

    1,51-2.h 文件:

 1 #ifndef _51_2_H_
 2 #define _51_2_H_
 3 
 4 typedef void Demo;
 5 typedef void Derived;  // C 語言實現繼承用 C++ 中的方法,即疊加;
 6 
 7 /* 父類中繼承的成員函數 */
 8 Demo* Demo_Create(int i, int j);
 9 int Demo_GetI(Demo* pThis);
10 int Demo_GetJ(Demo* pThis);
11 int Demo_Add(Demo* pThis, int value);  // 虛函數
12 void Demo_Free(Demo* pThis);
13 
14 /* 子類中新定義的成員函數 */
15 Derived* Derived_Create(int i, int j, int k);  // 構造函數;
16 int Derived_GetK(Derived* pThis);
17 int Derived_Add(Derived* pThis, int value);  // 虛函數
18 
19 #endif 

     2,51-2.c 文件:

  1 #include "51-2.h"
  2 #include "malloc.h"
  3 
  4 static int Demo_Virtual_Add(Demo* pThis, int value);  // 父類,先在這裡聲明,實現見第六步;
  5 static int Derived_Virtual_Add(Demo* pThis, int value);  // 子類 3,聲明子類虛函數,實現見下麵
  6 
  7 struct VTable     // 2. 定義虛函數表數據結構(用結構體表示虛函數表的數據結構,其用來創建虛函數表,見 static struct VTable g_Demo_vtbl)
  8 {
  9     int (*pAdd)(void*, int);   // 3. 虛函數表裡面存儲什麼?
 10 };
 11 
 12 /* 父類成員函數 */
 13 struct ClassDemo
 14 {
 15     struct VTable* vptr;     // 1. 定義虛函數表指針  ==》 虛函數表指針類型是什麼,見第二步定義;
 16     int mi;
 17     int mj;
 18 };
 19 
 20 /* 子類成員函數 */
 21 struct ClassDerived
 22 {
 23     struct ClassDemo d;  // 父類的成員變數疊加上子類的成員變數,最開始的部分為父類;
 24     int mk;
 25 };
 26 
 27 /* 父類,創建一個全局的虛函數表變數,通過 static 關鍵字將虛函數表隱藏在當前的文件中,外界不可訪問 */
 28 static struct VTable g_Demo_vtbl = 
 29 {
 30     Demo_Virtual_Add  // 7,用真正意義上的虛函數來初始化虛函數表指針;
 31 };
 32 
 33 /* 子類 2 放子類真正意義上的虛函數 */
 34 static struct VTable g_Derived_vtbl =   // static 關鍵字是對虛函數表這個變數隱藏在當前文件當中,完結不可訪問。
 35 {
 36     Derived_Virtual_Add
 37 };
 38 
 39 /* 父類構造函數 */
 40 Demo* Demo_Create(int i, int j)
 41 {
 42     struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo)); 
 43 
 44     if( ret != NULL )
 45     {
 46         ret->vptr = &g_Demo_vtbl;   // 4. 關聯對象和虛函數表
 47         ret->mi = i;
 48         ret->mj = j;
 49     }
 50     
 51     return ret;
 52 }
 53 
 54 /* 父類成員函數 */
 55 int Demo_GetI(Demo* pThis)
 56 {
 57      struct ClassDemo* obj = (struct ClassDemo*)pThis;    
 58 
 59      return obj->mi;
 60 }
 61 
 62 /* 父類成員函數 */
 63 int Demo_GetJ(Demo* pThis)
 64 {
 65     struct ClassDemo* obj = (struct ClassDemo*)pThis;
 66 
 67     return obj->mj;
 68 }
 69 
 70 // 6. 定義虛函數表中指針所指向的具體函數
 71 static int Demo_Virtual_Add(Demo* pThis, int value)
 72 {
 73     struct ClassDemo* obj = (struct ClassDemo*)pThis;
 74     
 75     return obj->mi + obj->mj + value;
 76 }
 77 
 78 /* 這個函數功能和上個函數功能並沒有重覆,這個函數變成對外的用戶所使用的函數介面 */
 79 // 5. 分析具體的虛函數是什麼?要定義一個全局意義上的真正的虛函數,並且這個虛函數只在當前文件中可以訪問;
 80 int Demo_Add(Demo* pThis, int value)
 81 {
 82     struct ClassDemo* obj = (struct ClassDemo*)pThis;
 83 
 84     /* 通過對象找到具體的虛函數表指針,然後再找到具體的 add() 函數,具體的 add() 函數地址保存在 pAdd 裡面,在這裡應該是 Demo_Virtual_Add()函數 */
 85     return obj->vptr->pAdd(pThis, value);
 86 }
 87 
 88 /* 父類析構函數 */
 89 void Demo_Free(Demo* pThis)
 90 {
 91     free(pThis);
 92 }
 93 
 94 /* 子類構造函數 */
 95 Derived* Derived_Create(int i, int j, int k)
 96 {
 97     struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived));
 98     
 99     if( ret != NULL )
100     {
101         ret->d.vptr = &g_Derived_vtbl;  // 子類 1 ,首先關聯虛函數表指針,指向子類虛函數表;
102         ret->d.mi = i;  // 初始化父類成員變數,d 是子類中父類的結構體變數;
103         ret->d.mj = j;
104         ret->mk = k;
105     }
106     
107     return ret;
108 }
109 
110 /* 子類成員函數 */
111 int Derived_GetK(Derived* pThis)
112 {
113     struct ClassDerived* obj = (struct ClassDerived*)pThis;
114     
115     return obj->mk;
116 }
117 
118 /* 子類成員函數 */
119 static int Derived_Virtual_Add(Demo* pThis, int value)
120 {
121     struct ClassDerived* obj = (struct ClassDerived*)pThis; 
122 
123     return obj->mk + value;
124 }
125 
126 /* 子類成員函數 */
127 int Derived_Add(Derived* pThis, int value)
128 {   
129     struct ClassDerived* obj = (struct ClassDerived*)pThis;
130    
131     return obj->d.vptr->pAdd(pThis, value);
132 }

     3,應用文件:

 1 #include "stdio.h"
 2 #include "51-2.h"
 3 
 4 void run(Demo* p, int v)
 5 {
 6     int r = Demo_Add(p, v);  // DEmo_Add(p, 3);  沒有實現多態的時候,C++ 編譯器這樣做更安全;
 7     
 8     printf("r = %d\n", r);  
 9 }
10 
11 int main()
12 {
13     Demo* pb = Demo_Create(1, 2);
14     Derived* pd = Derived_Create(1, 22, 333);
15     
16     printf("pb->add(3) = %d\n", Demo_Add(pb, 3));  // 6
17     printf("pd->add(3) = %d\n", Derived_Add(pd, 3));  // 336
18     
19     run(pb, 3);  // 沒有實現多態的時候,列印 6;實現多態後,列印 6;
20     run(pd, 3);  // 沒有實現多態的時候,列印 26;實現多態後,列印 336;
21     
22     Demo_Free(pb);
23     Demo_Free(pd);  // 子類可以繼承父類的析構函數,所以可以通過父類的析構函數來析構子類對象;
24     
25     return 0;
26 }

    4,步驟:

       1,先實現基本的子類繼承和其成員函數基本功能;

       2,後實現多態;

    5,C 實現 C++ 中的多態(第三個視頻這裡不是很明白):

       1,子類繼承:

           1,另外生成結構體,內容由子類疊加父類的結構體內容;

       2,子類構造函數:

           1,另外寫,先在堆上面生成指向結構體的指針,子類調用父類的構造函數是不影響父類原來的構造函數的;

       3,多態實現:

           1,在對象的結構體中定義虛函數表指針(要考慮虛函數表指針類型);

           2,在虛函數結構體中定義虛函數表數據結構(就是定義一個空的結構體);

           3,在虛函數結構表中存放指向虛函數成員函數的指針;

           4,在構造函數中關聯具體的對象和虛函數表;

           5,分析讓那個函數稱為真正的虛函數( static 修飾 );

           6,定義虛函數表指針所指向的具體函數。

   

6,小結:

    1,繼承的本質就是父子間成員變數的疊加;

    2,C++ 中的多態是通過虛函數表實現的;

    3,虛函數表是由編譯器自動生成與維護的;

    4,虛函數的調用效率低於普通成員函數;


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • v-show與v-if的區別 1.v-show只是css級別的display屬性none和block之間的切換。而v-if 是“真正”的條件渲染,因為它會確保在切換過程中條件塊內的事件監聽器和子組件適當地被銷毀和重建 2.v-show一般用在操作比較頻繁的地方,v-if用在運行時條件很少改變的地方。 ...
  • AJAX全稱為 Asynchronous Javasript And XML,是在瀏覽器端進行網路編程(發送請求,接收響應)的技術方案。AJAX 也就是瀏覽器提供的一套API,可以供 Javascript 調用,從而通過代碼來控制請求和響應,實現網路編程。 AJAX 使用基本模板 使用 AJAX 的 ...
  • 前面 網頁中漂亮的導航、整齊規範的文章標題列表和圖片列表等等。這些都是離不開HTML里一個重要的元素 列表,在HTML中有無序列表、有序列表和定義列表三種類型。其中,無序列表應用最為廣泛,下麵,我們一塊認識一下HTML列表。 列表在文檔編輯中也是十分常見的,我們可以結合word里的列表來對比學習HT ...
  • 組合模式(Composite),將對象組合成樹形結構以表示“部分-整體”的層次結構,組合模式使得用戶對單個對象和組合對象的使用具有一致性。組合模式的目的是讓客戶端不再區分操作的是組合對象(Composite)還是葉子對象(Leaf),而是以一個統一的方式來操作。實現這個目標的關鍵之處,是設計一個抽象 ...
  • 安裝zookeeper(單機) 下載bin.tar.gz的版本,解壓 conf下的zoo_sample.cfg改zoo.cfg zoo.cfg里添加配置 啟動zookeeper服務 bin\zkServer.cmd 安裝dubbo-admin http://dubbo.apache.org/en-u ...
  • 一、什麼是觀察者模式 觀察者一般可以看做是第三者,比如在學校上自習的時候,大家肯定都有過交頭接耳、各種玩耍的經歷,這時總會有一個“放風”的小伙伴,當老師即將出現時及時“通知”大家老師來了。再比如,拍賣會的時候,大家相互叫價,拍賣師會觀察最高標價,然後通知給其它競價者競價,這就是一個觀察者模式。 對於 ...
  • SaltStack使用salt-ssh模式 salt-ssh 介紹 參考官檔 salt-ssh是 0.17.0 新引入的一個功能,不需要minion對客戶端進行管理,也可以不需要master;salt-ssh也支持salt大部分的功能:比如grains,modules,state等;salt-ssh ...
  • 委托定義類型,類型指定特定方法簽名。 可將滿足此簽名的方法(靜態或實例)分配給該類型的變數,然後(使用適當參數)直接調用該方法,或將其作為參數本身傳遞給另一方法再進行調用。 以下示例演示了委托的用法。 public delegate string Reverse(string s); 行創建特定簽名 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...