主頁 > 後端開發 > C++物件模型:g++的實作(一)

C++物件模型:g++的實作(一)

2022-10-28 06:23:42 後端開發

剛看完了《深度探索C++物件模型》第三章,這里做一下總結,也寫一下我自己在g++ 7.5.0上的驗證,
本文中所有的源檔案都可以在這里拿到(百度網盤鏈接),
注意,這里所說的“物件”是指在C++中使用classstruct關鍵字創建的類的實體,

無繼承情況下的C++物件記憶體布局

首先當然是從最基礎的情況來講,在沒有繼承的情況下的C++物件記憶體布局是什么樣的?這又分為兩種:無虛函式和有虛函式,

無虛函式

C++類內成員變數分為兩類:static成員變數和非static成員變數,static成員變數不在類的實體的內部,在整個記憶體中只有一份,只需要使用類名即可訪問;而非static成員變數在類的實體內部,需要為其分配空間,
在這種情況下C++的物件和C的結構體是一樣的,畢竟要實作和C的兼容,主要就是結構體/類內成員變數的對齊,
其一般規則總結如下:

  1. 所有成員按照在類內的宣告順序在記憶體中排列;
// test00.cpp
#include <iostream>

int main();

class Test00 {
    friend int main();
public:
    int i1;
private:
    int i2;
public:
    int i3;
};

#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))

int main() {
    std::cout << showOffset(Test00, i1) << std::endl;
    std::cout << showOffset(Test00, i2) << std::endl;
    std::cout << showOffset(Test00, i3) << std::endl;
}

// Output:
//  0
//  4
//  8
  1. 任一非static成員變數的偏移(offset)要是其大小的倍數;
// test01.cpp
#include <iostream>
struct Test01 {
    char c;
    int i; // 如果緊湊排列,則i的偏移為1,但i的size為4,偏移要是4的倍數,因此i的偏移為4
};

#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))

int main() {
    std::cout << showOffset(struct Test01, i) << std::endl;
}

// Output:
//  4
  1. 結構體整體的size需要為最大非static成員變數size的倍數;
// test02.cpp
#include <iostream>

// 如果緊湊排,Test02_1的size應為9,
// 但要與int(size為4)對其,所以其size為12
struct Test02_1 {
    char c1;  // Offset: 0
    int i;    // Offset: 4
    char c2;  // Offset: 8
};

// Test02_2成員和Test02_1相同,但順序不同,
// 受規則2和3影響,其size為8
struct Test02_2 {
    char c1;  // Offset: 0
    char c2;  // Offset: 1
    int i;    // Offset: 4
};

int main() {
    std::cout << "sizeof Test02_1: " << sizeof(Test02_1) << std::endl;
    std::cout << "sizeof Test02_2: " << sizeof(Test02_2) << std::endl;
}

// Output:
//  sizeof Test02_1: 12
//  sizeof Test02_2: 8
  1. 空物件的size為1,為了保證每個物件都有唯一的記憶體位置(memory location)
// test03.cpp
#include <iostream>

struct Test03 {}; // Empty class

int main() {
    Test03 a, b;
    std::cout << "sizeof Test03: " << sizeof(Test03) << std::endl;
    if (&a == &b)
        std::cerr << " Error! &a == &b, at " << static_cast<void*>(&a) <<  std::endl;
    else
        std::cout << "a and b has different address, &a = " << static_cast<void*>(&a) << " and &b = " << static_cast<void*>(&b) << std::endl;
}

// Output:
//  sizeof Test03: 1
//  a and b has different address, &a = 0x7fffe62e8486 and &b = 0x7fffe62e8487
  1. 當類(class)/結構體(struct) A 作為一個類B的內部成員變數時,其對齊要求為類A內部最大的對齊要求;
// test04.cpp
#include <iostream>

// 規則2中的類,size為8,對齊要求為4
struct Test01{
    char c;
    int i;
};

struct Test04 {
    char c;   // Offset: 1, size 1
    Test01 t; // Offset: 4, size 8
};

#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))

int main() {
    std::cout << "Offset of t in struct Test04: " << showOffset(Test04, t) << std::endl;
    std::cout << "sizeof Test04: " << sizeof(Test04) << std::endl; 
}

// Output:
//  Offset of t in struct Test04: 4
//  sizeof Test04: 12
  1. 空的類(empty class)A作為作為一個類B的成員變數時,類A占用一個位元組的空間,對其要求也為1;
// test05.cpp
#include <iostream>

// 規則4中的類,空類,size = 1
struct Test03 {};

struct Test05 {
    char c;     // Offset: 0, size: 1
    Test03 t;   // Offset: 1, size: 1
};

#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))

int main() {
    std::cout << "Offset of t in struct Test04: " << showOffset(Test05, t) << std::endl;
    std::cout << "sizeof Test05: " << sizeof(Test05) << std::endl; 
}

// Output:
//  Offset of t in struct Test04: 1
//  sizeof Test05: 2

有虛函式

C++使用虛函式來實作多型,非虛函式不展現多型性,當呼叫非虛函式時,只要呼叫一個寫死的地址即可,無論是使用物件呼叫還是使用指標/參考呼叫;而當使用指標/參考呼叫虛函式需要視其系結到的實際物件來呼叫對應的虛函式,以展現多型性(用物件呼叫虛函式不展現多型性),
而C++實作虛函式用到的便是虛表,所謂虛表,就是保存該類所有虛函式地址的一張表,一個類的某個確定的虛函式在虛表的確定位置,而類實體中有一個虛表指標指向該虛表,當出現類繼承并覆寫(override)了該虛函式時,只需要將虛表指標指向另一張虛表,該虛表中對應位置的函式指標換為新的函式即可,另外,一個類的所有物件共享同一張虛表,因此不會帶來大的記憶體消耗,該虛表由編譯器生成,
這里只是對于虛函式和虛表進行了簡單的描述,詳細可查詢網路資源,這里不再贅述,
就像上面所說,相比于沒有虛函式的類,由虛函式的類的實體只是多了一個指向虛表的指標,其放在類的開頭或者結尾(g++將其放在類的開頭),大小和對其要求視平臺而定,在x86-64平臺上,虛表指標大小和對其要求為8位元組,

// test06.cpp
class Point {
public:
    Point(int x)
        :m_x(x)
    {}

    virtual
    int getX()
    { return m_x; }

private:
    int m_x;
};


int main() {
    Point p(1);
    int x = p.getX();
}

gdb除錯圖片1
使用gdb觀察,可以看到Point類實體p的size為16,包括size為8的虛表指標和size為4的int型別的成員變數m_x,同時,由于虛表指標的對其要求為8,所以Point的size必須是8的倍數,所以其size為16,
同時查看p的記憶體布局,可以看到虛表指標被放置于類實體的頭部,占用8個位元組,后面緊跟4個位元組的int型別的成員變數m_i,最后填充了4個位元組以使類Point的size為8的倍數,
gdb除錯圖片2
我們在查看一下虛表指標指向的記憶體,我這里使用的是64位系統和程式,所以函式指標是8位大小,虛表指標指向的虛表的第一個表項是地址0x080007b2,同時查看反匯編,因為我們使用物件來呼叫虛函式,不展現多型性,這里直接call了Point::getX()的地址,可以看到其地址為0x080007b2,正好是前面虛表的第一個表項,
還有就是Point型別對應的typeinfo物件的地址,在《深度探索C++物件模型》中提到其位于虛表的第一個表項,但前面我們看到虛表第一個表項存放的是虛函式,那typeinfo的地址放在哪里呢?我們來找一下,

// file test07.cpp
#include <typeinfo>
  2 class Point {
  3 public:
  4     Point(int x)
  5         :m_x(x)
  6     {}
  7
  8     virtual
  9     int getX()
 10     { return m_x; }
 11
 12 private:
 13     int m_x;
 14 };
 15
 16 int main() {
 17     Point p(1);
 18     auto& ti = typeid(p);
 19     int x = p.getX();
 20 }

gbd除錯圖片3
可以看到反匯編中保存了0x8200da8這一地址到堆疊上,再結合我們的原始碼,很可能gdb所提示的<_ZTI5Point>這一物件就是Point類的typeinfo物件,我們使用工具c++filt來看_ZTI5Point這個被修飾過的符號是什么含義,不出所料,正是Point類對應的typeinfo物件,

liuyun@DESKTOP-Q5AT31V:/tmp/test/cppObjectModel/chap03/blog$ c++filt _ZTI5Point
typeinfo for Point

既然Point對應的typeinfo物件的地址為0x8200da8,我們查看虛表附近的地址,發現虛表指標指向的地址的前面的一個QWORD的內容正好是typeinfo的地址,那是不是虛表指標指向的并不是虛表的開頭,而是第一個虛函式所在的地址,而在虛表中,第一個虛函式這一表項前面便是該類對應的typeinfo的地址?
g++使用-fdump-class-hierarchy選項生成類資訊
在查閱資料的時候,《C++虛函式之二:虛函式表與虛函式呼叫》這篇博客提到g++支持-fdump-class-hierarchy這一編譯選項,可以生成一個名為{source_file_name}.002t.class的檔案,檔案中詳細記錄了各個類的資訊,包括其虛表資訊,
正如我們所想,如果我們使用vptr指代虛表指標,那么vptr[0]就是第一個虛函式的地址,vptr[-1]則是該類對應的typeinfo的地址,而在最前面,g++還填充了一個空的表項,

最后還有一個問題,再沒有虛函式的時候,編譯器為了讓每一個物件都有自己獨一無二的地址,會在物件中插入一個位元組占位,而在有虛函式的時候類中會有一個原生的虛表指標vptr,從而至少占8位元組大小(x86-64上),那么是否就不需要再插入一個位元組了呢?事實正如我們所想,Test08類的size為8而不是16,

// test08.cpp
#include <iostream>

class Test08 {
public:
virtual
int getNumber() { return s_i++; }

private:
    static int s_i;
};

int Test08::s_i = 0;

int main() {
    std::cout << "sizeof Test08: " << sizeof(Test08) << std::endl;
    Test08 t;
    int i = t.getNumber();
}
// Output:
//  sizeof Test08: 8

這一篇博客就先寫到這里,下一篇再談談在繼承體系下g++是如何實作C++物件的記憶體布局的,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/521765.html

標籤:C++

上一篇:c++執行期語意

下一篇:C++物件模型:g++實作(二)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more