C++對象模型:g++的實現(六)

来源:https://www.cnblogs.com/lycpp/archive/2022/11/06/16863886.html
-Advertisement-
Play Games

==Servlet01== 官方api文檔:https://tomcat.apache.org/tomcat-8.0-doc/servletapi/index.html Servlet和Tomcat的關係:一句話,Tomcat支持Servlet Servlet是跟Tomcat關聯在一起的,換而言之, ...


這篇博客開始介紹《深度探索C++對象模型》第四章的剩餘部分,包括成員函數指針和內聯函數。

成員函數指針

對於靜態成員函數,其和常規的函數是一樣的,故這裡不做介紹。下麵主要介紹非靜態的成員函數指針,包括普通的非virtual成員函數指針和virtual成員函數指針。
註意,這篇是按照《深度探索C++對象模型》的內容寫的,最後講到支持多繼承的成員函數指針時才會給出真正的成員函數指針的實現!

非virtual成員函數指針

對於一個非virtual的成員函數取址,得到的就是該成員函數在記憶體中的地址,但是它不能單獨調用,需要使用其綁定的對象/指針/引用調用。

// test26.cpp

class Test {
public:
    Test(int i)
        : m_i(i) 
    {}

    int getInt() const {
        return m_i;
    }

    void setInt(int i) {
        m_i = i;
    }

private:
    int m_i;
};

int main() {
    Test t(1);
    int i = t.getInt();
    void (Test::*pMemberFunc)(int) = nullptr;   // 成員函數指針
    pMemberFunc = &Test::setInt;
    (t.*pMemberFunc)(2);
    i = t.getInt();
}

支持“指向虛成員函數”的指針

對於非虛成員函數我們可以直接拿到其地址,因為其沒有多態性。但對於虛函數,其地址要在運行時確定,因此對於虛成員函數我們取的應該是其相對虛表指針的偏移index。
所以如果有如下類:

class Point {
public:
    Point(int x, int y);
    virtual
    ~Point();

    int x() const {return m_x;}
    int y() const {return m_y;}
    virtual
    int z() const { return 0; }
private:
    int m_x;
    int m_y;
};

對於析構函數取值&Point::~Point取得的是0。
對於x()和y()取址&Point::x, &Point::y得到的是其地址,因為他們不是虛函數。
對於z()取址&Point::z得到的是1。通過pMemberFunc調用z(),其會是類似下麵的形式:

(*ptr->vptr[(int)pMemberFunc])(ptr)

支持多繼承的成員函數的指針

在多繼承的情況下還要考慮虛函數表的位置問題,因為在多重繼承下可能有多個虛函數表;還有this指針可能需要進行偏移,如果派生類沒有覆蓋第二個或後面的基類的虛函數的話。
為了要支持以上種種特性:如果是非虛函數,指針中要包括其地址;如果是虛函數,要包括其相對虛表指針的偏移;如果是多重繼承,還要找到虛函數在哪個虛表中和對this指針進行偏移。
在《深度探索C++對象模型》中提出的是這樣的結構:

struct _mptr{
    int delta;
    int index;
    union {
        PtrToFunc faddr;
        int v_offset;
    };
};

其中delta是this指針要進行的偏移,index是虛函數在虛表指針指向空間中的下標,faddr是非虛函數的地址,v_offset是虛表指針的的位置。
所以下麵的操作:

(ptr->*pmf)();

會變成:

// 我覺得這個可能是有問題
pmf.index < 0
    ? // 非虛函數調用
    (*pmf.faddr)(paddr)
    : // 虛函數調用
    (*ptr->vptr[pmf.index])(ptr)

《深度探索C++對象模型》中是這麼寫的,但按照作者的說法,實際的代碼應該是:

pmf.index < 0
    ? 
    (pmf.faddr)(pmf + delta)
    : 
    (((vptr*)(ptr+pmf.v_offset))[pmf.index])(ptr+delta)
    // (ptr+pmf.v_offset) 是虛表地址
    // ((vptr*)(ptr+pmf.v_offset))[pmf.index] 是虛表的第pmf.index項
    // ptr+delta是對this指針進行偏移

讓我們來看看g++中是怎麼實現的:

// test27.cpp

class Point {
public:
    Point(int x, int y);
    virtual
    ~Point();

    int x() const {return m_x;}
    int y() const {return m_y;}
    virtual
    int z() const { return 0; }
private:
    int m_x;
    int m_y;
};

Point::Point(int x, int y)
    : m_x(x), m_y(y)
{}

Point::~Point() {
    m_x = m_y = 0;
}

int main() {
    Point p(1, 2);

    using MemberFunction_t = int (Point::*)() const ;

    MemberFunction_t pVirtualMemberFunc = nullptr;
    MemberFunction_t pMemberFunc = nullptr;

    pMemberFunc = &Point::x;
    pVirtualMemberFunc = &Point::z;
    
    int x = (p.*pMemberFunc)();
    int z = (p.*pVirtualMemberFunc)();
    
    ++z;
}

我們使用gdb看一下這個成員函數指針的size:

(gdb) p sizeof(MemberFunction_t)
$1 = 16

在賦值之後,查看pMemberFunc和pVirtualMemberFunc的二進位是什麼:

(gdb) x/2ag &pMemberFunc
0x7ffffffee0d0: 0x8000a86 <Point::x() const>    0x0
(gdb) x/2ag &pVirtualMemberFunc
0x7ffffffee0c0: 0x11    0x0

可以看到g++實現的成員函數指針有兩個QWORD。如果函數指針指向的是非虛函數,第一個QWORD裡面是該函數的地址;如果是的話,看上去是該虛函數相對於虛表的偏移+1,因為Point::z在vptr[2]的地方(vptr[0]是Point::~Point,但不調用::operator deletevptr[0]也是Point::~Point,會隨後調用::operator delete),那偏移就是0x10,但內容是0x11,可能就是加了1。
讓我們看一下彙編代碼是怎麼操作的:
彙編代碼
上面的彙編是即將執行int x = (p.*pMemberFunc)();這一語句。
總結如下:

  • 如果不是虛函數,低8個位元組是函數的地址,高8個位元組是this指針的偏移;
  • 如果是虛函數,低8個位元組是虛表指針相對於this指針的偏移&1(位與操作),而高8個位元組同樣是this指針的偏移;

這兩種情況就按低8個位元組的QWORD的最低位是不是1決定:如果是1則是虛成員函數指針,不是1則是非虛成員函數指針。
虛函數地址相對於vptr偏移的位元組數肯定是指針大小的整數倍,一般為4或8位元組,最後一位肯定是0,所以與一個1可以理解,用的時候只需要減去這一位即可。
但函數地址最後一位肯定是0嗎?我就這個問題查閱了資料,在博客《C++語言學習(十四)——C++類成員函數調用分析》中提到:

一般來說因為對齊的關係,函數地址都至少是4位元組對齊的。即函數地址的最低位兩個bit總是0。

雖然和我的觀察略微有不同(在我編譯的程式里,Point::x的地址是0x8000a86,只有最後一位是0,倒數第二位是1),但也說明瞭函數地址確實是有對齊這一現象的。
這裡再繼續引用一下這篇博客里的論述,用以輔助讀者理解(感我寫得不如這篇博客遠矣):

GCC對於成員函數指針統一使用下麵的結構進行表示:

struct        
{    
    void* __pfn;  //函數地址,或者是虛擬函數的index    
    long __delta; // offset, 用來進行this指針調整   
};

不管是普通成員函數,還是虛成員函數,信息都記錄在__pfn。一般來說因為對齊的關係,函數地址都至少是4位元組對齊的。即函數地址的最低位兩個bit總是0。 GCC充分利用了這兩個bit。如果是普通的函數,__pfn記錄函數的真實地址,最低位兩個bit就是全0,如果是虛成員函數,最後兩個bit不是0,剩下的30bit就是虛成員函數在函數表中的索引值。
// 註意,在我的版本里(g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0),檢查的是隨後一位,函數地址也只是2對齊,而不是4對齊
GCC先取出函數地址最低位兩個bit看看是不是0,若是0就使用地址直接進行函數調用。若不是0,就取出前面30位包含的虛函數索引,通過計算得到真正的函數地址,再進行函數調用。

這篇博客里還介紹了MSVC對於成員函數指針的實現,使用了thunk技術,大家可以去看一下。(其實這個在《深度探索C++對象模型》,里也有提到,大家感興趣也可以看看原書)。

內聯函數

關於這一部分只是做一個總結,我也不知道如何比較好得驗證其中的內容。
關鍵詞inline只是一個請求,一般而言,處理一個inline函數會有兩個階段:

  • 分析函數定義,以解決函數的"intrinsic inline ability"(本質的inline能力)。"intrinsic"(本質的、固有的)一詞在這裡意指“與編譯器相關”【書中原話】

說白了就是編譯器要看看能不能內聯,要是太複雜就直接編譯成函數,(在理想情況下)鏈接器會把生成的重覆的內聯函數清理掉。strip命令也可以達成這個目的。

  • 真正的inline函數擴展操作是在調用的那一點上,這會帶來參數的求值操作和臨時對象的管理。

所謂求值操作是和巨集函數做對比的,巨集函數只是簡單的複製粘貼,但inline函數在調用前會對傳參進行求值(無論其內聯展開與否)。
比如:

inline
int min(int i, int j) {
    return i < j ? i : j;
}

對於minval = min(foo(), bar() + 1)會擴展成:

int t1, t2;
minval = (t1 = foo()), (t2 = bar() + 1),
        t1 < t2 ? t1 : t2;
// 逗號操作符,
// 從左到右計算,表達式結果為最後一個值。
// 比如 t = foo(), bar();
// 會先調用foo(), 再調用bar(),t的值為bar()的返回值

這種特性使得內聯函數比巨集函數安全得多。
而臨時對象管理則是在函數內聯時會產生很多臨時變數,比如形參列表、內聯函數中的局部變數等等。


其他比如成員函數指針的執行效率我就不多做測試了,這一章也就結束了。
關於後面的內容,我會在有時間的時候做簡要的總結,不會像這兩章這麼詳細得分析彙編了,因為我覺得對象佈局和虛函數的實現就是書最主要的內容了。
好的,就這樣了。


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

-Advertisement-
Play Games
更多相關文章
  • 簡介: 橋接模式又叫橋梁模式,屬於結構型模式。目的是將抽象與實現分離,使它們都可以獨立的變化,解耦。繼承有很多好處,但是會增加耦合,而橋接模式偏向組合和聚合的方式來共用。 適用場景: 不希望或不適用使用多繼承的場景。 一個類存在2個或更多的 獨立變化維度 , 並且這些維度都需要 獨立擴展 優點: 解 ...
  • 日誌概念 1. 日誌文件 日誌文件是用於記錄系統操作事件的文件集合 1.1 調試日誌 1.2 系統日誌 系統日誌是記錄系統中硬體、軟體和系統問題的信息,同時還可以監視系統中發生的事件。用戶可以通過它來檢查錯誤發生的原因,或者尋找受到攻擊時攻擊者留下的痕跡 日誌門面 當我們的系統變的更加複雜的時候,我 ...
  • Fsm1 這裡需要實現一個簡單的摩爾狀態機,即輸出只與狀態有關的狀態機。 我這裡代碼看上去比長一點,答案用的case和三目運算符,結果是一樣的。 module top_module( input clk, input areset, // Asynchronous reset to state B ...
  • 看《C++ Primer Plus》時整理的學習筆記,部分內容完全摘抄自《C++ Primer Plus》(第6版)中文版,Stephen Prata 著,張海龍 袁國忠譯,人民郵電出版社。只做學習記錄用途。 ...
  • 前言 ​ 對於我們平時寫代碼運行,我們很少去關註編譯和鏈接的過程,因為現在的開發環境都是集成(IDE)的,這些IDE一般都會將編譯和鏈接的過程一步搞定,這一過程又被稱為構建。但若經常寫代碼,經常會有很多莫名其妙的錯誤讓我們不知所措,對於這些錯誤若我們能知其原因,那是再好不過了。因此本系列就是帶你瞭解 ...
  • C++ 類:實體的抽象類型 實體(屬性,行為) ->ADT(abstract data type) 類(屬性->成員變數,行為->成員方法) OOP語言4大特征 抽象 封裝/隱藏(通過public private protected) 繼承 多態 點擊查看代碼 class Student{ //屬性 ...
  • 免費課頁面前端搭建 點擊查看代碼 <template> <div class="course"> <Header></Header> <div class="main"> <!-- 篩選條件 --> <div class="condition"> <ul class="cate-list"> <li ...
  • 原創:扣釘日記(微信公眾號ID:codelogs),歡迎分享,轉載請保留出處。 簡介 要說Java中什麼異常最容易出現,我想NullPointerException一定當仁不讓,為瞭解決這種null值判斷問題,Java8中提供了一個新的工具類Optional,用於提示程式員註意null值,併在特定場 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...