面試系列之C++的對象佈局【建議收藏】

我們都知道C++多態是通過虛函數表來實現的,那具體是什麼樣的大家清楚嗎?開篇依舊提出來幾個問題:

  • 普通類對象是什麼佈局?
  • 帶虛函數的類對象是什麼佈局?
  • 單繼承下不含有覆蓋函數的類對象是什麼佈局?
  • 單繼承下含有覆蓋函數的類對象是什麼佈局?
  • 多繼承下不含有覆蓋函數的類對象是什麼佈局?
  • 多繼承下含有覆蓋函數的類對象的是什麼佈局?
  • 多繼承中不同的繼承順序產生的類對象佈局相同嗎?
  • 虛繼承的類對象是什麼佈局?
  • 菱形繼承下類對象是什麼佈局?
  • 爲什麼要引入虛繼承?
  • 爲什麼虛函數表中有兩個析構函數?
  • 爲什麼構造函數不能是虛函數?
  • 爲什麼基類析構函數需要是虛函數?

要回答上述問題我們首先需要了解什麼是多態。

什麼是多態

多態可以分爲編譯時多態和運行時多態。

  • 編譯時多態:基於模板和函數重載方式,在編譯時就已經確定對象的行爲,也稱爲靜態綁定。

  • 運行時多態:面向對象的一大特色,通過繼承方式使得程序在運行時纔會確定相應調用的方法,也稱爲動態綁定,它的實現主要是依賴於傳說中的虛函數表。

如何查看對象的佈局

在gcc中可以使用如下命令查看對象佈局:

g++ -fdump-class-hierarchy model.cc後查看生成的文件

在clang中可以使用如下命令:

clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc
// 查看對象佈局
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc
// 查看虛函數表佈局

上面兩種方式其實足夠了,也可以使用gdb來查看內存佈局,這裏可以看文末相關參考資料。本文都是使用clang來查看的對象佈局。

接下來讓我們一起來探祕下各種繼承條件下類對象的佈局情況吧~

1. 普通類對象的佈局

如下代碼:

struct Base {
    Base() = default;
    ~Base() = default;
    
    void Func() {}

    int a;
    int b;
};

int main() {
    Base a;
    return 0; 
}

// 使用clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc查看

輸出如下:

*** Dumping AST Record Layout
         0 | struct Base
         0 |   int a
         4 |   int b
           | [sizeof=8, dsize=8, align=4,
           |  nvsize=8, nvalign=4]

*** Dumping IRgen Record Layout

畫出圖如下:

1.png

從結果中可以看見,這個普通結構體Base的大小爲8字節,a佔4個字節,b佔4個字節。

2. 帶虛函數的類對象的佈局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("FuncB\n");
    }

    int a;
    int b;
};

int main() {
    Base a;
    return 0; 
}

// 這裏可以查看對象的佈局和相應虛函數表的佈局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc

對象佈局如下:

*** Dumping AST Record Layout
         0 | struct Base
         0 |   (Base vtable pointer)
         8 |   int a
        12 |   int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

這個含有虛函數的結構體大小爲16,在對象的頭部,前8個字節是虛函數表的指針,指向虛函數的相應函數指針地址,a佔4個字節,b佔4個字節,總大小爲16。

虛函數表佈局:

Vtable for 'Base' (5 entries).
   0 | offset_to_top (0)
   1 | Base RTTI
       -- (Base, 0) vtable address --
   2 | Base::~Base() [complete]
   3 | Base::~Base() [deleting]
   4 | void Base::FuncB()

畫出對象佈局圖如下:

2.png

我們來探祕下傳說中的虛函數表:

offset_to_top(0):表示當前這個虛函數表地址距離對象頂部地址的偏移量,因爲對象的頭部就是虛函數表的指針,所以偏移量爲0。

RTTI指針:指向存儲運行時類型信息(type_info)的地址,用於運行時類型識別,用於typeid和dynamic_cast。

RTTI下面就是虛函數表指針真正指向的地址啦,存儲了類裏面所有的虛函數,至於這裏爲什麼會有兩個析構函數,大家可以先關注對象的佈局,最下面會介紹。

3. 單繼承下不含有覆蓋函數的類對象的佈局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    int a;
    int b;
};

struct Derive : public Base{
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

子類對象佈局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int a
        12 |     int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

和上面相同,這個含有虛函數的結構體大小爲16,在對象的頭部,前8個字節是虛函數表的指針,指向虛函數的相應函數指針地址,a佔4個字節,b佔4個字節,總大小爲16。

子類虛函數表佈局:

Vtable for 'Derive' (5 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (Base, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Base::FuncB()

畫圖如下:

3.png

這個和上面也是相同的,注意下虛函數表這裏的FuncB函數,還是Base類中的FuncB,因爲在子類中沒有重寫這個函數,那麼如果子類重寫這個函數後對象佈局是什麼樣的,請繼續往下看哈。

4. 單繼承下含有覆蓋函數的類對象的佈局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    int a;
    int b;
};

struct Derive : public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

子類對象佈局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int a
        12 |     int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

依舊和上面相同,這個含有虛函數的結構體大小爲16,在對象的頭部,前8個字節是虛函數表的指針,指向虛函數的相應函數指針地址,a佔4個字節,b佔4個字節,總大小爲16。

子類虛函數表佈局:

Vtable for 'Derive' (5 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (Base, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncB()

4.png

注意這裏虛函數表中的FuncB函數已經是Derive中的FuncB啦,因爲在子類中重寫了父類的這個函數。

再注意這裏的RTTI中有了兩項,表示Base和Derive的虛表地址是相同的,Base類裏的虛函數和Derive類裏的虛函數都在這個鏈條下,這裏可以繼續關注下面多繼承的情況,看看有何不同。

5. 多繼承下不含有覆蓋函數的類對象的佈局

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseA, public BaseB{
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

類對象佈局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseA (primary base)
         0 |     (BaseA vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseB (base)
        16 |     (BaseB vtable pointer)
        24 |     int a
       28 |     int b
          | [sizeof=32, dsize=32, align=8,
          |  nvsize=32, nvalign=8]

Derive大小爲32,注意這裏有了兩個虛表指針,因爲Derive是多繼承,一般情況下繼承了幾個帶有虛函數的類,對象佈局中就有幾個虛表指針,並且子類也會繼承基類的數據,一般來說,不考慮內存對齊的話,子類(繼承父類)的大小=子類(不繼承父類)的大小+所有父類的大小

虛函數表佈局:

Vtable for 'Derive' (10 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseA, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void BaseA::FuncB()
   5 | offset_to_top (-16)
   6 | Derive RTTI
       -- (BaseB, 16) vtable address --
   7 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   8 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
   9 | void BaseB::FuncC()

可畫出對象佈局圖如下:

5.png

offset_to_top(0):表示當前這個虛函數表(BaseA,Derive)地址距離對象頂部地址的偏移量,因爲對象的頭部就是虛函數表的指針,所以偏移量爲0。

再注意這裏的RTTI中有了兩項,表示BaseA和Derive的虛表地址是相同的,BaseA類裏的虛函數和Derive類裏的虛函數都在這個鏈條下,截至到offset_to_top(-16)之前都是BaseA和Derive的虛函數表。

offset_to_top(-16):表示當前這個虛函數表(BaseB)地址距離對象頂部地址的偏移量,因爲對象的頭部就是虛函數表的指針,所以偏移量爲-16,這裏用於this指針偏移,下一小節會介紹。

注意下後面的這個RTTI:只有一項,表示BaseB的虛函數表,後面也有兩個虛析構函數,爲什麼有四個Derive類的析構函數呢,又是怎麼調用呢,請繼續往下看~

6. 多繼承下含有覆蓋函數的類對象的佈局

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseA, public BaseB{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

對象佈局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseA (primary base)
         0 |     (BaseA vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseB (base)
        16 |     (BaseB vtable pointer)
        24 |     int a
        28 |     int b
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

類大小仍然是32,和上面一樣。

虛函數表佈局:

Vtable for 'Derive' (11 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseA, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncB()
   5 | void Derive::FuncC()
   6 | offset_to_top (-16)
   7 | Derive RTTI
       -- (BaseB, 16) vtable address --
   8 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   9 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  10 | void Derive::FuncC()
       [this adjustment: -16 non-virtual]

6.png

offset_to_top(0):表示當前這個虛函數表(BaseA,Derive)地址距離對象頂部地址的偏移量,因爲對象的頭部就是虛函數表的指針,所以偏移量爲0。

再注意這裏的RTTI中有了兩項,表示BaseA和Derive的虛表地址是相同的,BaseA類裏的虛函數和Derive類裏的虛函數都在這個鏈條下,截至到**offset_to_top(-16)**之前都是BaseA和Derive的虛函數表。

offset_to_top(-16):表示當前這個虛函數表(BaseB)地址距離對象頂部地址的偏移量,因爲對象的頭部就是虛函數表的指針,所以偏移量爲-16。當基類BaseB的引用或指針base實際接受的是Derive類型的對象,執行base->FuncC()時候,由於FuncC()已經被重寫,而此時的this指針指向的是BaseB類型的對象,需要對this指針進行調整,就是offset_to_top(-16),所以this指針向上調整了16字節,之後調用FuncC(),就調用到了被重寫後Derive虛函數表中的FuncC()函數。這些帶adjustment標記的函數都是需要進行指針調整的。至於上面所說的這裏虛函數是怎麼調用的,估計您也明白了吧~

7. 多重繼承不同的繼承順序導致的類對象的佈局相同嗎?

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseB, public BaseA{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

對象佈局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseB (primary base)
         0 |     (BaseB vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseA (base)
        16 |     (BaseA vtable pointer)
        24 |     int a
        28 |     int b
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

這裏可見,對象佈局和上面的不相同啦,BaseB的虛函數表指針和數據在上面,BaseA的虛函數表指針和數據在下面,以A,B的順序繼承,對象的佈局就是A在上B在下,以B,A的順序繼承,對象的佈局就是B在上A在下。

虛函數表佈局:

Vtable for 'Derive' (11 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseB, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncC()
   5 | void Derive::FuncB()
   6 | offset_to_top (-16)
   7 | Derive RTTI
       -- (BaseA, 16) vtable address --
   8 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   9 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  10 | void Derive::FuncB()
       [this adjustment: -16 non-virtual]

對象佈局圖如下:

7.png

虛函數表的佈局也有所不同,BaseB和Derive共用一個虛表地址,在整個虛表佈局的上方,而佈局的下半部分是BaseA的虛表,可見繼承順序不同,子類的虛表佈局也有所不同。

8. 虛繼承的佈局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct Derive : virtual public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

對象佈局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   (Derive vtable pointer)
         8 |   struct Base (virtual base)
         8 |     (Base vtable pointer)
        16 |     int a
        20 |     int b
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout

虛繼承下,這裏的對象佈局和普通單繼承有所不同,普通單繼承下子類和基類共用一個虛表地址,而在虛繼承下,子類和虛基類分別有一個虛表地址的指針,兩個指針大小總和爲16,再加上a和b的大小8,爲24。

虛函數表:

Vtable for 'Derive' (13 entries).
   0 | vbase_offset (8)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (Derive, 0) vtable address --
   3 | void Derive::FuncB()
   4 | Derive::~Derive() [complete]
   5 | Derive::~Derive() [deleting]
   6 | vcall_offset (-8)
   7 | vcall_offset (-8)
   8 | offset_to_top (-8)
   9 | Derive RTTI
       -- (Base, 8) vtable address --
  10 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  11 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  12 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

對象佈局圖如下:

8.png

vbase_offset(8):對象在對象佈局中與指向虛基類虛函數表的指針地址的偏移量

vcall_offset(-8):當虛基類Base的引用或指針base實際接受的是Derive類型的對象,執行base->FuncB()時候,由於FuncB()已經被重寫,而此時的this指針指向的是Base類型的對象,需要對this指針進行調整,就是vcall_offset(-8),所以this指針向上調整了8字節,之後調用FuncB(),就調用到了被重寫後的FuncB()函數。

9. 虛繼承帶未覆蓋函數的對象佈局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    virtual void FuncC() {
        printf("Base FuncC\n");
    }

    int a;
    int b;
};

struct Derive : virtual public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

對象佈局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   (Derive vtable pointer)
         8 |   struct Base (virtual base)
         8 |     (Base vtable pointer)
        16 |     int a
        20 |     int b
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout

和上面虛繼承情況下相同,普通單繼承下子類和基類共用一個虛表地址,而在虛繼承下,子類和虛基類分別有一個虛表地址的指針,兩個指針大小總和爲16,再加上a和b的大小8,爲24。

虛函數表佈局:

Vtable for 'Derive' (15 entries).
   0 | vbase_offset (8)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (Derive, 0) vtable address --
   3 | void Derive::FuncB()
   4 | Derive::~Derive() [complete]
   5 | Derive::~Derive() [deleting]
   6 | vcall_offset (0)
   7 | vcall_offset (-8)
   8 | vcall_offset (-8)
   9 | offset_to_top (-8)
  10 | Derive RTTI
       -- (Base, 8) vtable address --
  11 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  12 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  13 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]
  14 | void Base::FuncC()

對象佈局圖如下:

9.png

vbase_offset(8):對象在對象佈局中與指向虛基類虛函數表的指針地址的偏移量

vcall_offset(-8):當虛基類Base的引用或指針base實際接受的是Derive類型的對象,執行base->FuncB()時候,由於FuncB()已經被重寫,而此時的this指針指向的是Base類型的對象,需要對this指針進行調整,就是vcall_offset(-8),所以this指針向上調整了8字節,之後調用FuncB(),就調用到了被重寫後的FuncB()函數。

vcall_offset(0):當Base的引用或指針base實際接受的是Derive類型的對象,執行base->FuncC()時候,由於FuncC()沒有被重寫,所以不需要對this指針進行調整,就是vcall_offset(0),之後調用FuncC()。

10. 菱形繼承下類對象的佈局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseA : virtual public Base {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB : virtual public Base {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseB, public BaseA{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

類對象佈局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseB (primary base)
         0 |     (BaseB vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseA (base)
        16 |     (BaseA vtable pointer)
        24 |     int a
        28 |     int b
        32 |   struct Base (virtual base)
        32 |     (Base vtable pointer)
        40 |     int a
        44 |     int b
           | [sizeof=48, dsize=48, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

大小爲48,這裏不用做過多介紹啦,相信您已經知道了吧。

虛函數表:

Vtable for 'Derive' (20 entries).
   0 | vbase_offset (32)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (BaseB, 0) vtable address --
       -- (Derive, 0) vtable address --
   3 | Derive::~Derive() [complete]
   4 | Derive::~Derive() [deleting]
   5 | void Derive::FuncC()
   6 | void Derive::FuncB()
   7 | vbase_offset (16)
   8 | offset_to_top (-16)
   9 | Derive RTTI
       -- (BaseA, 16) vtable address --
  10 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
  11 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  12 | void Derive::FuncB()
       [this adjustment: -16 non-virtual]
  13 | vcall_offset (-32)
  14 | vcall_offset (-32)
  15 | offset_to_top (-32)
  16 | Derive RTTI
       -- (Base, 32) vtable address --
  17 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  18 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  19 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

對象佈局圖如下:

10.png

vbase_offset (32)

vbase_offset (16):對象在對象佈局中與指向虛基類虛函數表的指針地址的偏移量

offset_to_top (0)

offset_to_top (-16)

offset_to_top (-32):指向虛函數表的地址與對象頂部地址的偏移量。

vcall_offset(-32):當虛基類Base的引用或指針base實際接受的是Derive類型的對象,執行base->FuncB()時候,由於FuncB()已經被重寫,而此時的this指針指向的是Base類型的對象,需要對this指針進行調整,就是vcall_offset(-32),所以this指針向上調整了32字節,之後調用FuncB(),就調用到了被重寫後的FuncB()函數。

爲什麼要虛繼承

如圖:

11.png

非虛繼承時,顯然D會繼承兩次A,內部就會存儲兩份A的數據浪費空間,而且還有二義性,D調用A的方法時,由於有兩個A,究竟時調用哪個A的方法呢,編譯器也不知道,就會報錯,所以有了虛繼承,解決了空間浪費以及二義性問題。在虛擬繼承下,只有一個共享的基類子對象被繼承,而無論該基類在派生層次中出現多少次。共享的基類子對象被稱爲虛基類。在虛繼承下,基類子對象的複製及由此而引起的二義性都被消除了。

爲什麼虛函數表中有兩個析構函數

前面的代碼輸出中我們可以看到虛函數表中有兩個析構函數,一個標誌爲deleting,一個標誌爲complete,因爲對象有兩種構造方式,棧構造和堆構造,所以對應的實現上,對象也有兩種析構方式,其中堆上對象的析構和棧上對象的析構不同之處在於,棧內存的析構不需要執行 delete 函數,會自動被回收。

爲什麼構造函數不能是虛函數。

構造函數就是爲了在編譯階段確定對象的類型以及爲對象分配空間,如果類中有虛函數,那就會在構造函數中初始化虛函數表,虛函數的執行卻需要依賴虛函數表。如果構造函數是虛函數,那它就需要依賴虛函數表纔可執行,而只有在構造函數中才會初始化虛函數表,雞生蛋蛋生雞的問題,很矛盾,所以構造函數不能是虛函數。

爲什麼基類析構函數要是虛函數。

一般基類的析構函數都要設置成虛函數,因爲如果不設置成虛函數,在析構的過程中只會調用到基類的析構函數而不會調用到子類的析構函數,可能會產生內存泄漏。

小總結

offset_to_top:對象在對象佈局中與對象頂部地址的偏移量。

RTTI指針:指向存儲運行時類型信息(type_info)的地址,用於運行時類型識別,用於typeid和dynamic_cast。

vbase_offset:對象在對象佈局中與指向虛基類虛函數表的指針地址的偏移量。

vcall_offset:父類引用或指針指向子類對象,調用被子類重寫的方法時,用於對虛函數執行指針地址調整,方便成功調用被重寫的方法。

thunk: 表示上面虛函數表中帶有adjustment字段的函數調用需要先進行this指針調整,纔可以調用到被子類重寫的函數。

最後通過兩張圖總結一下對象在Linux中的佈局:

A *a = new Derive(); // A爲Derive的基類

如圖:

12.png

a作爲對象指針存儲在棧中,指向在堆中的類A的實例內存,其中實例內存佈局中有虛函數表指針,指針指向的虛函數表存放在數據段中,虛函數表中的各個函數指針指向的函數在代碼段中。

13.png

虛表結構大體如上圖,正常的虛表結構中都含有後三項,當有虛繼承情況下會有前兩個表項。

參考資料:

https://www.cnblogs.com/qg-whz/p/4909359.html

https://blog.csdn.net/fuzhongmin05/article/details/59112081

https://zhuanlan.zhihu.com/p/67177829

https://mp.weixin.qq.com/s/sqpwQpPYBFkPWCmccruvNw

https://jacktang816.github.io/post/virtualfunction/

https://blog.mengy.org/cpp-virtual-table-2/

https://blog.mengy.org/cpp-virtual-table-1/

https://blog.mengy.org/extend-gdb-with-python/

https://www.zhihu.com/question/389546003/answer/1194780618

https://www.zhihu.com/question/29251261/answer/1297439131

https://zhuanlan.zhihu.com/p/41309205

https://wizardforcel.gitbooks.io/100-gdb-tips/examine-memory.html

https://www.cnblogs.com/xhb19960928/p/11720314.html

https://www.lagou.com/lgeduarticle/113008.html

backgroud.jpg

更多文章,請關注我的V X 公 主 號:程序喵大人,歡迎交流~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章