[CPP] 類的記憶體佈局

sinkinben發表於2021-10-28

本文討論的是下面 3 個問題:

  • 以不同方式繼承之後,類的成員變數是如何分佈的?
  • 虛擬函式表及虛擬函式表指標,在可執行檔案中的位置?
  • 單一繼承、多繼承、虛擬繼承之後,類的虛擬函式表的內容是如何變化的?

在這裡涉及的變數有:有無繼承、有無虛擬函式、是否多繼承、是否虛繼承。

準備工作

在開始探索類的記憶體佈局之前,我們先了解虛擬函式表的概念,位元組對齊的規則,以及如何列印一個類的記憶體佈局。

檢視類的記憶體佈局

我們可以使用 clang++ 來檢視類的記憶體佈局:

# 檢視物件佈局, 要求 main 中有 sizeof(class_t)
clang++ -Xclang -fdump-record-layouts xxx.cpp
# 檢視虛擬函式表佈局, 要求 main 中例項化一個物件
clang++ -Xclang -fdump-record-layouts xxx.cpp
# 或者
clang -cc1 -fdump-vtable-layouts -emit-llvm xxx.cpp 

虛擬函式表

  • 每個類都有一個屬於自己虛擬函式表,虛擬函式表屬於類,而不是某一個例項化物件。
  • 如果一個類宣告瞭虛擬函式,那麼在該類的所有例項化物件中,在 [0, 7] 這 8 個位元組(假設是 64 位機器),會存放一個虛擬函式表的指標 vtable
  • 虛擬函式表中的每一個元素都是一個函式地址,指向程式碼段的某一虛擬函式。
  • 虛擬函式表指標 vtable 是在物件例項化的時候填入的(因此建構函式不能用 virtual 宣告為一個虛擬函式)。
    • 假設 B 繼承了 A ,假如我們在執行時有 A *a = new B() ,那麼 a->vtable 實際上填入的是類 B 的虛擬函式表地址。
    • 如何獲得 vtable 的值?通過讀取物件的起始 8 個位元組的內容,即 *(uint64_t *)&object
+---------+                                                   +----------------+
| entity1 |                                                   | .text segment  |
+---------+                                                   +----------------+
| vtable  |-------+                                  +------->| Entity::vfunc1 |
| member1 |       |         +-----------------+      |  +---->| Entity::vfunc2 | 
| member2 |       |         | Entity's vtable |      |  |     |       ...      |
+---------+       |         +-----------------+      |  |     +----------------+
                  +-------->| 0 : vfunc_ptr0  |------+  |     | Entity::func1  |
+---------+       |         | 1 : vfunc_ptr1  |---------+     | Entity::func2  |
| entity2 |       |         |      ...        |               |       ...      |
+---------+       |         +-----------------+               +----------------+
| vtable  |-------+
| member1 |
| member2 |
+---------+

那麼虛擬函式表(即上圖的 Entity's vtable )會存放在哪裡呢?

一個直覺是與 static 成員變數一樣,存放在 .data segment ,因為二者都屬於是類共享的資料。

位元組對齊

位元組對齊的規則:按照編譯器「已經掃描」的最長的資料型別的位元組數 (總是為 1, 2, 4, 8 ) 進行對齊,並且儘量填滿「空隙」。

編譯器是按照宣告順序(從前往後掃描)來解析一個 struct / class 的。

需要注意的是,不同的編譯器,其位元組對齊的規則會略有差異,但總的來說是大同小異的。本文所使用的編譯器均為 clang/clang++ 。


例子一

struct Entity
{
    char c1;
    int val;
};
// sizeof(Entity) = 8
  • 如果把 char c1 換成 short val0 ,那麼還是 8 。
  • 如果把 int val 換成 double d ,那麼是 16 。

例子二

struct Entity
{
    char cval;
    short ival;
    double dval;
};
/*
*** Dumping AST Record Layout
         0 | struct Entity
         0 |   char cval
         2 |   short ival
         8 |   double dval
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]
*/
  • 如果 short ival 換成 int ival ,那麼 ival 的起始位置是 4 (因為編譯器掃描到 ival 的時候,看到的最長位元組數是 sizeof(int) = 4 )。

例子三

struct Entity
{
    char cval;
    double dval;
    char cval2;
    int ival;
};
/*
*** Dumping AST Record Layout
         0 | struct Entity
         0 |   char cval
         8 |   double dval
        16 |   char cval2
        20 |   int ival
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=24, nvalign=8]
*/

此處的例子,就是為了說明上述的「儘可能填滿空隙」,注意到 cval2ival 之間留出了 17, 18, 19 這 3 個位元組的空白。

  • cval2, ival 插入任意的一個位元組的資料型別(最多插入 3 個),不會影響 sizeof(Entity) 的大小。
  • 如果我們在 cval2, ival 之間插入一個 short sval ,那麼 sval 會位於 18 這一位置。

例子四

如果有虛擬函式,又會怎麼樣呢?

class Entity
{
    char cval;
    virtual void vfunc() {}
};
/*
*** Dumping AST Record Layout
         0 | class Entity
         0 |   (Entity vtable pointer)
         8 |   char cval
           | [sizeof=16, dsize=9, align=8,
           |  nvsize=9, nvalign=8]
*/

在 64 位機器上,一個指標的大小是 8 位元組,所以編譯器會按照 8 位元組對齊。

單一的類

成員變數

考慮無虛擬函式的條件下,成員變數的記憶體佈局。

class A
{
    private:
        short val1;
    public:
        int val2;
        double d;
        static char ch;
        void funcA1() {}
};
int main()
{
    __attribute__((unused)) int k = sizeof(A);
}
// clang++ -Xclang -fdump-record-layouts test.cpp

使用上述命令編譯之後,輸出為:

*** Dumping AST Record Layout
         0 | class A
         0 |   short val1
         4 |   int val2
         8 |   double d
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

從上面的輸出可以看出:

  • static 型別的成員並不佔用例項化物件的記憶體(因為 static 型別的成員存放在靜態資料區 .data )。
  • 成員函式不佔用記憶體(因為存放在程式碼段 .text )。
  • 成員變數的許可權級別 private, public 不影響記憶體佈局,記憶體佈局只跟宣告順序有關(可能需要位元組對齊)。

虛擬函式表

class A
{
private:
    short val1;

public:
    int val2;
    double d;
    static char ch;
    void funcA1() {}
    virtual void vfuncA1() {}
    virtual void vfuncA2() {}
};
int main()
{
    __attribute__((unused)) int k = sizeof(A);
    // __attribute__((unused)) A a;
}

記憶體佈局如下:

clang++ -Xclang -fdump-record-layouts test.cpp
*** Dumping AST Record Layout
         0 | class A
         0 |   (A vtable pointer)
         8 |   short val1
        12 |   int val2
        16 |   double d
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=24, nvalign=8]

clang++ -Xclang -fdump-vtable-layouts test.cpp
Original map
Vtable for 'A' (4 entries).
   0 | offset_to_top (0)
   1 | A RTTI
       -- (A, 0) vtable address --
   2 | void A::vfuncA1()
   3 | void A::vfuncA2()

VTable indices for 'A' (2 entries).
   0 | void A::vfuncA1()
   1 | void A::vfuncA2()

從這裡可以看出,虛擬函式表的指標預設是存放在一個類的起始位置(一般佔用 4 或者 8 位元組,視乎機器的字長)。

  • offset_to_top(0) : 表示當前這個虛擬函式表地址距離物件頂部地址的偏移量,因為物件的頭部就是虛擬函式表的指標,所以偏移量為0。如果是多繼承的情況,一個類可能存在多個 vtable 的指標。
  • RTTI : 即 Run Time Type Info, 指向儲存執行時型別資訊 (type_info) 的地址,用於執行時型別識別,用於 typeiddynamic_cast

單一繼承

成員變數

class A
{
public:
    char aval;
    static int sival;
    void funcA1();
};
class B : public A
{
public:
    double bval;
    void funcB1();
};
class C : public B
{
public:
    int cval;
    void funcC1() {}
};

記憶體佈局:

clang++ -Xclang -fdump-record-layouts test.cpp

*** Dumping AST Record Layout
         0 | class A
         0 |   char aval
           | [sizeof=1, dsize=1, align=1,
           |  nvsize=1, nvalign=1]

*** Dumping AST Record Layout
         0 | class B
         0 |   class A (base)
         0 |     char aval
         8 |   double bval
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping AST Record Layout
         0 | class C
         0 |   class B (base)
         0 |     class A (base)
         0 |       char aval
         8 |     double bval
        16 |   int cval
           | [sizeof=24, dsize=20, align=8,
           |  nvsize=20, nvalign=8]

可以看出,普通的單一繼承,成員變數是從上到下依次排列的,並且遵循前面提到的位元組對齊規則。

虛擬函式表

  • A 中有 2 個虛擬函式 vfuncA1, vfuncA2 .
  • B 重寫 (Override) 了 vfuncA1 ,自定義虛擬函式 vfuncB .
  • C 重寫了 vfunc1 ,自定義虛擬函式 vfuncC .
class A
{
public:
    char aval;
    static int sival;
    virtual void vfuncA1() {}
    virtual void vfuncA2() {}
};
class B : public A
{
public:
    double bval;
    virtual void vfuncA1() {}
    virtual void vfuncB() {}
};
class C : public B
{
public:
    int cval;
    virtual void vfuncA1() {}
    virtual void vfuncC() {}
};

成員變數佈局:

clang++ -Xclang -fdump-record-layouts test.cpp

*** Dumping AST Record Layout
         0 | class A
         0 |   (A vtable pointer)
         8 |   char aval
           | [sizeof=16, dsize=9, align=8,
           |  nvsize=9, nvalign=8]

*** Dumping AST Record Layout
         0 | class B
         0 |   class A (primary base)
         0 |     (A vtable pointer)
         8 |     char aval
        16 |   double bval
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=24, nvalign=8]

*** Dumping AST Record Layout
         0 | class C
         0 |   class B (primary base)
         0 |     class A (primary base)
         0 |       (A vtable pointer)
         8 |       char aval
        16 |     double bval
        24 |   int cval
           | [sizeof=32, dsize=28, align=8,
           |  nvsize=28, nvalign=8]

3 個類的虛擬函式表如下:

clang++ -Xclang -fdump-vtable-layouts test.cpp
Original map
 void C::vfuncA1() -> void B::vfuncA1()
 void B::vfuncA1() -> void A::vfuncA1()
Vtable for 'C' (6 entries).
   0 | offset_to_top (0)
   1 | C RTTI
       -- (A, 0) vtable address --
       -- (B, 0) vtable address --
       -- (C, 0) vtable address --
   2 | void C::vfuncA1()
   3 | void A::vfuncA2()
   4 | void B::vfuncB()
   5 | void C::vfuncC()

VTable indices for 'C' (2 entries).
   0 | void C::vfuncA1()
   3 | void C::vfuncC()

Original map
 void C::vfuncA1() -> void B::vfuncA1()
 void B::vfuncA1() -> void A::vfuncA1()
Vtable for 'B' (5 entries).
   0 | offset_to_top (0)
   1 | B RTTI
       -- (A, 0) vtable address --
       -- (B, 0) vtable address --
   2 | void B::vfuncA1()
   3 | void A::vfuncA2()
   4 | void B::vfuncB()

VTable indices for 'B' (2 entries).
   0 | void B::vfuncA1()
   2 | void B::vfuncB()

Original map
 void C::vfuncA1() -> void B::vfuncA1()
 void B::vfuncA1() -> void A::vfuncA1()
Vtable for 'A' (4 entries).
   0 | offset_to_top (0)
   1 | A RTTI
       -- (A, 0) vtable address --
   2 | void A::vfuncA1()
   3 | void A::vfuncA2()

VTable indices for 'A' (2 entries).
   0 | void A::vfuncA1()
   1 | void A::vfuncA2()

可以看出,在單一繼承中,子類的虛擬函式表通過以下步驟構造出來:

  • 先拷貝上一層次父類的虛擬函式表。
  • 如果子類有自定義虛擬函式(例如 B::vfuncB, C::vfuncC),那麼直接在虛擬函式表後追加這些虛擬函式的地址。
  • 如果子類覆蓋了父類的虛擬函式,使用新地址(例如 B::vfuncA1, C::vfuncA1)覆蓋原有地址(即 A::vfunc1)。

多繼承

現直接組合成員變數和虛擬函式一起來看。

class A
{
    char aval;
    virtual void vfuncA1() {}
    virtual void vfuncA2() {}
};
class B
{
    double bval;
    virtual void vfuncB1() {}
    virtual void vfuncB2() {}
};
class C : public A, public B
{
    char cval;
    virtual void vfuncC() {}
    virtual void vfuncA1() {}
    virtual void vfuncB1() {}
};

記憶體佈局如下(注意類 C 的佈局):

clang++ -Xclang -fdump-record-layouts test.cpp

*** Dumping AST Record Layout
         0 | class A
         0 |   (A vtable pointer)
         8 |   char aval
           | [sizeof=16, dsize=9, align=8,
           |  nvsize=9, nvalign=8]

*** Dumping AST Record Layout
         0 | class B
         0 |   (B vtable pointer)
         8 |   double bval
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping AST Record Layout
         0 | class C
         0 |   class A (primary base)
         0 |     (A vtable pointer)
         8 |     char aval
        16 |   class B (base)
        16 |     (B vtable pointer)
        24 |     double bval
        32 |   char cval
           | [sizeof=40, dsize=33, align=8,
           |  nvsize=33, nvalign=8]

注意到類 C 的記憶體佈局:

  • 一共 40 位元組,有 2 個 vtable 指標。
  • 繼承有 primary base 父類和普通 base 父類之分。

實際上就是:

+--------+--------+---------------+
| offset |  size  |   content     |
+--------+--------+---------------+
|   0    |   8    | vtable1       |
|   8    |   1    | aval          |
|   9    |   7    | aligned bytes |
|   16   |   8    | vtable2       |
|   24   |   8    | bval          |
|   32   |   1    | cval          |
|   33   |   7    | aligned bytes |
+--------+--------+---------------+

總的來說,在最底層子類的記憶體佈局中,多繼承的成員變數,以及 vtable 指標的排列規則是:

  • 第一個宣告的繼承是 primary base 父類。
  • 按照繼承的宣告順序依次排列,並需要遵循編譯器的位元組對齊規則。
  • 最後排列最底層子類的成員變數。

虛擬函式表如下(省略了 A 和 B 的內容):

clang++ -Xclang -fdump-vtable-layouts test.cpp
Original map
 void C::vfuncA1() -> void A::vfuncA1()
Vtable for 'C' (10 entries).
   0 | offset_to_top (0)
   1 | C RTTI
       -- (A, 0) vtable address --
       -- (C, 0) vtable address --
   2 | void C::vfuncA1()
   3 | void A::vfuncA2()
   4 | void C::vfuncC()
   5 | void C::vfuncB1()
   6 | offset_to_top (-16)
   7 | C RTTI
       -- (B, 16) vtable address --
   8 | void C::vfuncB1()
       [this adjustment: -16 non-virtual] method: void B::vfuncB1()
   9 | void B::vfuncB2()

Thunks for 'void C::vfuncB1()' (1 entry).
   0 | this adjustment: -16 non-virtual

VTable indices for 'C' (3 entries).
   0 | void C::vfuncA1()
   2 | void C::vfuncC()
   3 | void C::vfuncB1()

從上面可以看出,C 的虛擬函式表是由 2 部分組成的:

  • 首先是 「C 繼承 A」,按照上述單一繼承的虛擬函式表生成原則,生成了第一個虛擬函式表。此時 C::vfuncB1() 對於 A 來說是一個自定義的虛擬函式,因此虛擬函式表的第一部分有 4 個函式地址。
  • 其次是「C 繼承 B」,同樣按照單一繼承的規則生成,但不用追加 C::vfuncC() ,因為 C::vfuncC() 已經在第一部分填入。

可以發現的是:

  • C 的虛擬函式表存在一個重複的函式地址 C::vfuncB1
  • 雖然 C 有 2 個 vtable 指標,但仍然只有一個虛擬函式表( ? 其實也可以理解為 2 個表,不過這 2 個表是緊挨著的),而 2 個 vtable 指標指向了虛擬函式表的不同位置(也許跟編譯器的處理有關,至少 clang 下的情況是這樣的)。

加入虛擬函式表後,C 的記憶體佈局如下:

                                                         +-----------------------+
                                                         |-2: offset_to_top(0)  |
                                                         |-1: C RTTI            |
+--------+--------+---------------+                      +-----------------------+
| offset |  size  |   content     |                      | class C's vtable      |
+--------+--------+---------------+                      +-----------------------+
|   0    |   8    | vtable1       |--------------------->| 0: C::vfuncA1_ptr     |
|   8    |   1    | aval          |                      | 1: A::vfuncA2_ptr     |
|   9    |   7    | aligned bytes |                      | 2: C::vfuncC_ptr      |
|   16   |   8    | vtable2       |------------+         | 3: C::vfuncB1_ptr     |
|   24   |   8    | bval          |            |         | 4: offset_to_top(-16) |
|   32   |   1    | cval          |            |         | 5: C RTTI             |
|   33   |   7    | aligned bytes |            +-------->| 6: C::vfuncB1_ptr     |
+--------+--------+---------------+                      | 7: B::vfuncB2_ptr     |
                                                         +-----------------------+

如何驗證這個想法呢?

class A
{
public:
    char aval;
    virtual void vfuncA1() { cout << "A::vfuncA1()" << endl; }
    virtual void vfuncA2() { cout << "A::vfuncA2()" << endl; }
};
class B
{
public:
    double bval;
    virtual void vfuncB1() { cout << "B::vfuncB1()" << endl; }
    virtual void vfuncB2() { cout << "B::vfuncB2()" << endl; }
};
class C : public A, public B
{
public:
    char cval;
    virtual void vfuncC() { cout << "C::vfuncC()" << endl; }
    virtual void vfuncA1() { cout << "C::vfuncA1()" << endl; }
    virtual void vfuncB1() { cout << "C::vfuncB1()" << endl; }
};

int main()
{
    __attribute__((unused)) int k = sizeof(C);
    C c;
    uint64_t *cvtable = (uint64_t *)*(uint64_t *)(&c);
    uint64_t *cvtable2 = (uint64_t *)*(uint64_t *)((uint8_t *)(&c) + 16);
    typedef void (*func_t)(void);

    cout << "---- vtable1 ----" << endl;
    ((func_t)(*(cvtable + 0)))();    // C::vfuncA1()
    ((func_t)(*(cvtable + 1)))();    // A::vfuncA2()
    ((func_t)(*(cvtable + 2)))();    // C::vfuncC()
    ((func_t)(*(cvtable + 3)))();    // C::vfuncB1()
    
    printf("offset_to_top = %d\n", *(cvtable2 - 2));  // -16

    cout << "---- vtable2 ----" << endl;
    ((func_t)(*(cvtable2 + 0)))();   // C::vfuncB1(), same as cvtable + 6
    ((func_t)(*(cvtable2 + 1)))();   // B::vfuncB2(), same as cvtable + 7
}

稜形繼承和虛擬繼承

如果我們需要用到類似「稜形」的繼承鏈,那麼就要通過「虛擬繼承」的方式實現。

假設此處的繼承鏈為:

  Base
 /     \
A       B
  \   /
  Child

如果不使用 virtual 修飾繼承方式:

class Base { public: int value; };
class A : public Base { };
class B : public Base { };
class Child : public A, public B { };
int main()
{
    Child child;
    child.value;
}

那麼成員變數 child.value 會出現編譯時錯誤 (clang++) ,類似於「命名衝突」。

單一虛擬繼承

class Base
{
    char baseval;
    virtual void vfuncBase1() {}
    virtual void vfuncBase2() {}
};

class A : virtual public Base
{
    double aval;
    virtual void vfuncBase1() {}
    virtual void vfuncA() {}
};

class B : virtual public Base
{
    double bval;
    virtual void vfuncBase2() {}
    virtual void vfuncB() {}
};

以 A 為例子進行說明。成員變數佈局:

clang++ -Xclang -fdump-record-layouts diamond2.cpp
*** Dumping AST Record Layout
         0 | class A
         0 |   (A vtable pointer)
         8 |   double aval
        16 |   class Base (virtual base)
        16 |     (Base vtable pointer)
        24 |     char baseval
           | [sizeof=32, dsize=25, align=8,
           |  nvsize=16, nvalign=8]

與上述的「單一繼承」不同,此處虛擬繼承是會有 2 個 vtable 指標的,並且被虛擬繼承的目標(即 Base 會排列在最後面)。


虛擬函式表的內容如下:

clang++ -Xclang -fdump-vtable-layouts diamond2.cpp
Original map
Vtable for 'A' (11 entries).
   0 | vbase_offset (16)
   1 | offset_to_top (0)
   2 | A RTTI
       -- (A, 0) vtable address --
   3 | void A::vfuncBase1()
   4 | void A::vfuncA()
   5 | vcall_offset (0)
   6 | vcall_offset (-16)
   7 | offset_to_top (-16)
   8 | A RTTI
       -- (Base, 16) vtable address --
   9 | void A::vfuncBase1()
       [this adjustment: 0 non-virtual, -24 vcall offset offset] method: void Base::vfuncBase1()
  10 | void Base::vfuncBase2()

Virtual base offset offsets for 'A' (1 entry).
   Base | -24

Thunks for 'void A::vfuncBase1()' (1 entry).
   0 | this adjustment: 0 non-virtual, -24 vcall offset offset

VTable indices for 'A' (2 entries).
   0 | void A::vfuncBase1()
   1 | void A::vfuncA()

化簡一下:

A vtable:                      B vtable:
  - A::vfuncBase1()              - B::vfuncBase2()
  - A::vfuncA()                  - B::vfuncB()
  - A::vfuncBase1()              - Base::vfuncBase1()
  - Base::vfuncBase2()           - B::vfuncBase2()

從上面可以看出:

  • 虛擬函式表的第一部分 3-4 ,按照 A 是一個「單一的類」時的規則構造。
  • 虛擬函式表的第二部分 9-10 ,按照 A 單一繼承 Base 的規則構造。

稜形繼承的成員變數

class Child : public A, public B
{
    char childval;
    virtual void vfuncC() {}
    virtual void vfuncB() {}
    virtual void vfuncA() {}
};

Child 成員變數記憶體佈局如下:

clang++ -Xclang -fdump-record-layouts diamond.cpp

*** Dumping AST Record Layout
         0 | class A
         0 |   (A vtable pointer)
         8 |   double aval
        16 |   class Base (virtual base)
        16 |     char baseval
           | [sizeof=24, dsize=17, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping AST Record Layout
         0 | class B
         0 |   (B vtable pointer)
         8 |   double bval
        16 |   class Base (virtual base)
        16 |     char baseval
           | [sizeof=24, dsize=17, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping AST Record Layout
         0 | class Child
         0 |   class A (primary base)
         0 |     (A vtable pointer)
         8 |     double aval
        16 |   class B (base)
        16 |     (B vtable pointer)
        24 |     double bval
        32 |   char childval
        33 |   class Base (virtual base)
        33 |     char baseval
           | [sizeof=40, dsize=34, align=8,
           |  nvsize=33, nvalign=8]

Child 中:

  • 成員變數和虛擬函式指標與「多繼承」的情況相同。
  • ChildBase (被虛擬繼承的父類)的內容排在最後(比 Child 的自定義成員還要後),並且只保留了一份 Base 的資料,這就是虛擬繼承的作用。

稜形繼承的虛擬函式表

A, B 的虛擬函式表,如「單一虛擬繼承」一節所述。 Child 的虛擬函式表如下:

clang++ -Xclang -fdump-vtable-layouts diamond.cpp
Original map
 void Child::vfuncA() -> void A::vfuncA()
Vtable for 'Child' (18 entries).
   0 | vbase_offset (40)
   1 | offset_to_top (0)
   2 | Child RTTI
       -- (A, 0) vtable address --
       -- (Child, 0) vtable address --
   3 | void A::vfuncBase1()
   4 | void Child::vfuncA()
   5 | void Child::vfuncC()
   6 | void Child::vfuncB()
   7 | vbase_offset (24)
   8 | offset_to_top (-16)
   9 | Child RTTI
       -- (B, 16) vtable address --
  10 | void B::vfuncBase2()
  11 | void Child::vfuncB()
       [this adjustment: -16 non-virtual] method: void B::vfuncB()
  12 | vcall_offset (-24)
  13 | vcall_offset (-40)
  14 | offset_to_top (-40)
  15 | Child RTTI
       -- (Base, 40) vtable address --
  16 | void A::vfuncBase1()
       [this adjustment: 0 non-virtual, -24 vcall offset offset] method: void Base::vfuncBase1()
  17 | void B::vfuncBase2()
       [this adjustment: 0 non-virtual, -32 vcall offset offset] method: void Base::vfuncBase2()

Virtual base offset offsets for 'Child' (1 entry).
   Base | -24

Thunks for 'void Child::vfuncB()' (1 entry).
   0 | this adjustment: -16 non-virtual

VTable indices for 'Child' (3 entries).
   1 | void Child::vfuncA()
   2 | void Child::vfuncC()
   3 | void Child::vfuncB()

回顧一下 A 和 B 的虛擬函式表:

A vtable:                      B vtable:
  - A::vfuncBase1()              - B::vfuncBase2()
  - A::vfuncA()                  - B::vfuncB()
  - A::vfuncBase1()              - Base::vfuncBase1()
  - Base::vfuncBase2()           - B::vfuncBase2()

可以看出,Child 的虛擬函式表有 2 部分:

  • 第一部分 3-6, 10-11 ,與 Child 多繼承 A, B 的構造規則類似,即合併 Avtable[0 - 1]Bvtable[0 - 1]
  • 第二部分 16-17 ,合併 Avtable[2 - 3]Bvtable[2 - 3]

總結

場景 成員變數 虛擬函式表
單一的類 按照宣告順序依次排列,並需要遵循位元組對齊的規則 在物件的起始 8 個位元組的記憶體中,存放 vtable 指標
單一繼承 1. 按照繼承的層次順序,依次排列,並需要遵循位元組對齊的規則
2. 只有一個 vtable 指標
1. 拷貝上一層次父類的虛擬函式表
2. 如果有自定義的虛擬函式,在虛擬函式表後追加對應的地址
3. 如果 Override 了父類虛擬函式,那麼使用新地址覆蓋原有地址。
多繼承 1. 多個 vtable 指標
2. 按照繼承的順序,依次排列父類的 <vtable, members>
參考「多繼承」一節。
單一虛擬繼承 與普通的單一繼承不同,會有多個 vtable 指標 2 部分:第一部分按照「單一的類」規則和第二部分按照「單一繼承」規則。
稜形繼承 1. 與多繼承類似
2. 在最後新增被虛擬繼承目標的資料
參考「稜形繼承的虛擬函式表」一節。

相關文章