1. 程式人生 > >轉載:虛擬函式和虛繼承的記憶體分佈

轉載:虛擬函式和虛繼承的記憶體分佈

最近做題的時候經常遇到虛擬函式和虛繼承的記憶體分佈情況問題,自己也有點生疏了,所以,趕緊在這裡回憶補充一下!

先從一個類的記憶體分佈情況開始看起:

環境:VS2012

class A
{
    int a;
public:
    A()
        :a(1)
    {}

    void fun()
    {
        cout<<"A"<<endl;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
save_snippets.png
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

按照我的理解,記憶體中只有一個int a; 這個類的大小為 4;下面寫個測試

    A a;
    cout<<sizof(a
)<<endl;
  • 1
  • 2
save_snippets.png
  • 1
  • 2

先通過除錯看記憶體情況:

這裡寫圖片描述

sizeof(a) 的結果:

這裡寫圖片描述

結果和預想的一樣,當然這只是複習的一個熱身;

將fun 變為虛擬函式:

    virtual void fun()
    {
        cout<<"A"<<endl;
    }
  • 1
  • 2
  • 3
  • 4
save_snippets.png
  • 1
  • 2
  • 3
  • 4

先通過除錯看記憶體情況:

這裡寫圖片描述

檢視 sizeof(a)的大小:

這裡寫圖片描述

我們可以看到,在類中加入虛擬函式之後,例項之後的物件會有一個虛擬函式指標,指向一個虛擬函式表,而虛擬函式表中存放的是虛擬函式地址!

下面我們來一個B類繼承A類,看看記憶體的分佈:

class B: public
A { int b; public: B() :b(2) {} void fun() { cout<<"B"<<endl; } };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
save_snippets.png
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

B繼承A,並且有一個成員 b,重寫了A的fun;

測試:

    B b;
    cout<<"sizeof(b) = "<<sizeof(b)<<endl;
  • 1
  • 2
save_snippets.png
  • 1
  • 2

除錯檢視b的記憶體佈局:

這裡寫圖片描述

記憶體佈局分析: 我們可以看到先存放的是A 的成員 a, 然後存放的才是 B的成員 b;

sizeof(b) 的結果:

這裡寫圖片描述

此時我們再讓A的fun為虛擬函式,看看 B繼承 A 之後b的記憶體分佈:

這裡寫圖片描述

sizeof(b) 的 大小:

這裡寫圖片描述

分析: 此時b的記憶體佈局是:先是一個虛表指標,然後是 a , b;

我們再看看一個類分別繼承 A 和 B的情況:此時是這樣的情況

這裡寫圖片描述

class C: public A, public B
{
    int c;
public:
    C()
        :c(3)
    {}
    void fun()
    {
        cout<<"C"<<endl;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
save_snippets.png
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

C類先繼承 A 再繼承 B 類, 注意:此時我們B類沒有繼承 A 類,並且沒有虛擬函式

測試

 C c;
 cout<<"sizeof(c) = "<<sizeof(c)<<endl;
  • 1
  • 2
save_snippets.png
  • 1
  • 2

除錯檢視記憶體:

這裡寫圖片描述

檢視 sizeof(c) 的結果:

這裡寫圖片描述

此時將 A 和 B 的fun設為虛擬函式,看看 c 的記憶體佈局:

這裡寫圖片描述

sizeof(c)的結果

這裡寫圖片描述

分析: 記憶體佈局,首先是一個虛表指標,和A的一致,然後是a的成員,接著是B的虛表指標,B的資料成員, 最後是C的資料成員;

感覺情況好多,後面的sizeof的結果就不截圖了;

到這裡,我們可以對含有虛擬函式的類的物件的記憶體佈局做一個總結:

首先,記憶體的開始一定是一個虛表指標,指向一個虛表,如果含有繼承,則和第一個繼承的基類的虛表指標一致,然後先存放基類的資料成員,再存放自己的資料成員;還有一點,那就是如果子類中對基類的虛擬函式進行了重寫,那麼會在虛表中覆蓋掉基類虛擬函式的地址!

下面我們要看的是多重繼承的記憶體分佈情況,這裡就只看一種:菱形繼承;

什麼是菱形繼承?

上面我們舉過一個例子, c 分別繼承 A 和 B; 如果我讓 A 和 B 分別再繼承一個基類 Base 的話,這就叫菱形繼承:

大概邏輯關係如下: 沒有虛擬函式

這裡寫圖片描述

class Base
{
    int base;
public:
    Base()
        :base(0)
    {}
};

class A:public Base
{
    int a;
public:
    A()
        :a(1)
    {}

    void fun()
    {
        cout<<"A"<<endl;
    }
};

class B : public Base
{
    int b;
public:
    B()
        :b(2)
    {}
     void fun()
    {
        cout<<"B"<<endl;
    }
};

class C: public A, public B
{
    int c;
public:
    C()
        :c(3)
    {}
    void fun()
    {
        cout<<"C"<<endl;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
save_snippets.png
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

那麼此時如果有一個 C 的物件 c 的話,c的記憶體佈局應該是怎麼樣的呢?

我們看看 c 的記憶體佈局:

這裡寫圖片描述

意料之中的佈局,但是有一個問題,c 從A繼承了一個 Base 的 base,又從B繼承了一個Base 的 base,那麼我們在呼叫的時候到底用的是哪一個? 而且如果base是一個非常大的結構體的話,每一個繼承都需要建立一個base,那麼豈不是很浪費空間,這就是多重繼承的二義性和冗餘的問題;

而虛繼承存在的目的便是為了解決這個問題,那麼它是如何解決的呢?

即使得A和B都虛繼承Base,那麼Base就是一個虛基類,只儲存一份,而A和B中關於base的儲存,都是儲存一個指標,指向一個base, 因為把Base的成員存放在了記憶體的最後,這樣根據相對偏移量就找到了這個共有的base;

說了這些都是紙上談兵,終覺淺,我們還是直接來檢視記憶體的佈局吧;

class A:virtual public Base
{
    int a;
public:
    A()
        :a(1)
    {}

    void fun()
    {
        cout<<"A"<<endl;
    }
};

class B : virtual public Base
{
    int b;
public:
    B()
        :b(2)
    {}
     void fun()
    {
        cout<<"B"<<endl;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
save_snippets.png
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

例項一個C 的物件 c ,看記憶體佈局:

這裡寫圖片描述

我們可以 看到原本應該是00 00 00 00的位置被指標代替了,而base只在最後存了一份;

實際上這兩個指標指向一個虛基表,記錄當前位置到最底下base的偏移量;這樣就達到了共享的目的;

如果對於虛基類只被構造了一次抱有懷疑,可以在前面的測試中的建構函式中加入測試語句檢視;

先複習到這吧,,,好累,後面繼續補充!

這裡寫圖片描述