程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++繼承詳解之三——菱形繼承+虛繼承內存對象模型詳解vbptr(1)

C++繼承詳解之三——菱形繼承+虛繼承內存對象模型詳解vbptr(1)

編輯:關於C++

在我個人學習繼承的過程中,在網上查閱了許多資料,這些資料中有關菱形繼承的知識都是加了虛函數的,也就是涉及了多態的問題,而我在那個時候並沒有學習到多態這一塊,所以看很多資料都是雲裡霧裡的,那麼這篇文章我想以我自己學習過程中的經驗,由簡到較難的先分析以下菱形繼承,讓初學者先對這個問題有一點概念,在後面會由淺入深的繼續剖析。
本篇文章不會涉及到多態也就是虛函數的菱形繼承,在後面的文章更新中,我會慢慢把這些內容都加進去。
菱形繼承(也叫鑽石繼承)是下面的這種情況:
這裡寫圖片描述
對應代碼如下:

#include 
using namespace std;

class B
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
private:
    int b;
};
class C1 :public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
private:
    int c1;
};
class C2 :public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
private:
    int c2;
};
class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
private:
    int d;
};

int main()
{
    cout << sizeof(B) << endl;
    cout << sizeof(C1) << endl;
    cout << sizeof(C2) << endl;
    cout << sizeof(D) << endl;
    return 0;
}

運行結果為:
這裡寫圖片描述
我們希望上面的代碼中D類所對應的對象模型如下:
這裡寫圖片描述
而實際上上面代碼中D類所對應的模型為
這裡寫圖片描述
菱形繼承會造成派生類的數據冗余,比如上例就有D類中包含兩個int b這種情況發生。
為了解決菱形繼承數據冗余的問題,下面我要引入虛繼承的概念。
1.虛繼承
虛繼承 是面向對象編程中的一種技術,是指一個指定的基類,在繼承體系結構中,將其成員數據實例共享給也從這個基類型直接或間接派生的其它類。
虛擬繼承是多重繼承中特有的概念。虛擬基類是為解決多重繼承而出現的。
下圖可以看出虛基類和非虛基類在多重繼承中的區別
這裡寫圖片描述
那麼為什麼要引入虛擬繼承呢?
我們已經剖析了一般非虛基類的多重繼承得到的派生類的對象模型,那麼看看下面的代碼會輸出什麼

#include 
using namespace std;

class B
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
    int b;
};
class C1 :public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
private:
    int c1;
};
class C2 :public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
private:
    int c2;
};
class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
    void FunTest()
    {
        b = 10;
    }
private:
    int d;
};

int main()
{
    D d;
    d.FunTest();
    return 0;
}

編譯出錯,輸出

Error   1   error C2385: ambiguous access of 'b'    e:\demo\繼承\blog\project1\project1\source.cpp    58  1   Project1
    2   IntelliSense: "D::b" is ambiguous   e:\DEMO\繼承\blog\Project1\Project1\Source.cpp    58  3   Project1

編譯器報錯為:不明確的b,即編譯器無法分清到底b是繼承自C1的還是繼承自C2的。
解決上面由於菱形繼承而產生二義性與數據冗余的問題,需要用到虛繼承。
虛繼承的提出就是為了解決多重繼承時,可能會保存兩份副本的問題,也就是說用了虛繼承就只保留了一份副本,但是這個副本是被多重繼承的基類所共享的,該怎麼實現這個機制呢?
下面我來一步一步的分析這個問題:
1.類中不加數據成員
看下面的代碼:

#include 
using namespace std;

class B //基類
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};
class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
};
class C2 :virtual public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
};
class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
};

int main()
{
    cout << sizeof(B) << endl;
    cout << sizeof(C1) << endl;
    cout << sizeof(C2) << endl;
    cout << sizeof(D) << endl;
    return 0;
}

輸出為:
這裡寫圖片描述
我們分析一下結果:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> class B //基類 { public: B() { cout << "B" << endl; } ~B() { cout << "~B()" << endl; } };

首先,基類中除了構造函數和析構函數沒有其他成員了,所以

sizeof(B) = 1;

有的初學者可能會問為什麼為1,首先類在內存中的存儲是這樣的:
如果有一個類B

class B
{
    public:
    int b;
    void fun();
};
int Test()
{
    B b1,b2,b3;
}

那麼在內存中模型如下圖
這裡寫圖片描述
所以成員函數是單獨存儲,並且所有類對象公用的。
那麼有人可能要說那sizeof(B)為什麼不為0,那是因為編譯器要給對象一個地址,就需要區分開所有的類對象,1只是一個占位符,表示這個對象存在,並且讓編譯器給這個對象分配地址。
現在sizeof(B)的問題解決,下面看C1與C2

class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
};
class C2 :virtual public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
};

由於C1與C2都是虛擬繼承,故會在C1,C2內存起始處存放一個vbptr,為指向虛基類表的指針。
那麼這個指針vbptr指向什麼呢?
我們在main函數中生成一個C1類對象c1

int main()
{
    C1 c1;
    return 0;
}

在內存中查看c1究竟存了什麼
這裡寫圖片描述
由上圖我們可以看出,c1占了四個字節,存了一個指針變量,指針變量的內容就是c1的vbptr指向的虛基類表的地址。
那我們去c1.vbptr指向的虛基類表中查看下究竟存了什麼。

這裡寫圖片描述
可以看到,這個虛基類表有八個字節,分別存的為0和4。
那麼0和4代表的都是什麼呢?
虛基類表存放的為兩個偏移地址,分別為0和4。
其中0表示c1對象地址相對與存放vptr指針的地址的偏移量
可以用&c1->vbptr_c1表示。
其中vptr指的是指向虛表的指針,而虛表是定義了虛函數後才有的,由於我們這裡沒有定義虛函數,當然也就沒有vptr指針,所以偏移量為0.
8表示c1對象中基類對象部分相對於存放vbptr指針的地址的偏移量
可以用&c1(B)-&vbpt表示,其中&c1(B)表示對象c1中基類B部分的地址。
c2的內存布局與c1一樣,因為C1,C2都是虛繼承自B基類,且C1,C2都沒有加數據成員。
這裡寫圖片描述
現在大家都對

sizeof(C1) = 4;
sizeof(C2) = 4;

沒有什麼疑慮了吧。
總結一下,因為C1,C2是虛繼承自基類B,所以編譯器會給C1,C2中生成一個指針vbptr指向一個虛基類表,即指針vbptr的值是虛基類表的地址。
而這個虛基類表中存儲的是偏移量。
這個表中分兩部分,第一部分存儲的是對象相對於存放vptr指針的偏移量,可以用&(對象名)->vbptr_(對象名)來表示。對c1對象來說,可以用&c1->vbprt_c1來表示。
vptr指針是指向虛表的指針,而只有在類中定義了虛函數才會有虛表,因為我們這個例子中沒有定義虛函數,所以沒有vptr指針,所以第一部分偏移量均為0。
表的第二部分存儲的是對象中基類對象部分相對於存放vbptr指針的地址的偏移量,我們知道在本例中基類對象與指針偏移量就是指針的大小。

在內存中看d究竟存了什麼
這裡寫圖片描述
如上圖所示,d的內存中存了兩個指針,我們進入指針存放的地址看裡面究竟是什麼:
這裡寫圖片描述
如上圖所示,d中存放了兩個虛基類指針,每個虛基類表中存儲了偏移量。
說了這麼多,還是太抽象了。
現在看一下內存布局:
這裡寫圖片描述

2.類中加數據成員
上面我們剖析了不加數據成員的菱形繼承,下面剖析一下加數據成員的,這樣可以更清晰的看出內存布沮喎?/kf/yidong/wp/" target="_blank" class="keylink">WPC9wPg0KPHByZSBjbGFzcz0="brush:java;"> #include using namespace std; class B { public: B() { cout << "B" << endl; } ~B() { cout << "~B()" << endl; } int b; }; class C1 :virtual public B { public: C1() { cout << "C1()" << endl; } ~C1() { cout << "~C1()" << endl; } int c1; }; class C2 :virtual public B { public: C2() { cout << "C2()" << endl; } ~C2() { cout << "~C2()" << endl; } int c2; }; class D :public C1, public C2 { public: D() { cout << "D()" << endl; } ~D() { cout << "~D()" << endl; } void fun() { b = 0; c1 = 1; c2 = 2; d = 3; } int d; }; int main() { cout << sizeof(B) << endl; cout << sizeof(C1) << endl; cout << sizeof(C2) << endl; cout << sizeof(D) << endl; D d; d.fun(); return 0; }

這次的輸出為:
這裡寫圖片描述
這次我們再剖析下各個類的輸出大小

class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
    int c1;
};

首先C1,C2都是虛繼承自基類B的,所以我就一起剖析了。
首先B占四個字節沒有問題,因為B類中有int b數據成員,所以B類占四個字節。
那麼C1,C2是虛繼承自B類的,所以C1,C2的內存布局是相似的,在這裡我只剖析一下C1。
我們在C1類中加一個Fun成員函數,為了更清楚的看到內存布局

class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
    void Fun()
    {
        b = 5;
        c1 = 6;
    }
    int c1;
};
int main()
{
    C1 c1;
    c1.Fun();
    return 0;
}

在main函數中生成對象c1,那麼在內存中的c1是什麼樣呢?
這裡寫圖片描述
我們通過vbptr指針進入c1的虛基類表中
這裡寫圖片描述
由上面兩圖我們可以畫出c1的內存布局
這裡寫圖片描述
C2跟C1一樣。
所以

sizeof(C1) == 12;
sizeof(C2) == 12;

現在來看看D類的內存布局

class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
    void fun()//fun()函數主要幫助我們看D類的內存布局
    {
        b = 0;//基類數據成員
        c1 = 1;//C1類數據成員
        c2 = 2;//C2類數據成員
        d = 3;//D類自己的數據成員
    }
    int d;
};

我們進入內存中看d
這裡寫圖片描述
可以看出,前四個字節是vbptr指針,然後是c1類,+另一個vbptr指針+c2類+D類數據成員d+基類B這樣的結構
我們進入第一個vbptr指針中看
這裡寫圖片描述
可得出偏移量分別為0(因為沒有虛函數),14
再進入第二個vbptr指針中
這裡寫圖片描述
可以看出偏移量分別為0(因為沒有虛函數),12
好了,到這裡我們可以畫出D類的內存布局了
這裡寫圖片描述
所以,

sizeof(D) == 24;

這就是不帶虛函數的菱形繼承,關於帶虛函數的菱形繼承因為涉及到多態的知識。 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved