在我個人學習繼承的過程中,在網上查閱了許多資料,這些資料中有關菱形繼承的知識都是加了虛函數的,也就是涉及了多態的問題,而我在那個時候並沒有學習到多態這一塊,所以看很多資料都是雲裡霧裡的,那麼這篇文章我想以我自己學習過程中的經驗,由簡到較難的先分析以下菱形繼承,讓初學者先對這個問題有一點概念,在後面會由淺入深的繼續剖析。
本篇文章不會涉及到多態也就是虛函數的菱形繼承,在後面的文章更新中,我會慢慢把這些內容都加進去。
菱形繼承(也叫鑽石繼承)是下面的這種情況:
對應代碼如下:
#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.類中加數據成員 這次的輸出為: 首先C1,C2都是虛繼承自基類B的,所以我就一起剖析了。 在main函數中生成對象c1,那麼在內存中的c1是什麼樣呢? 現在來看看D類的內存布局 我們進入內存中看d 這就是不帶虛函數的菱形繼承,關於帶虛函數的菱形繼承因為涉及到多態的知識。
上面我們剖析了不加數據成員的菱形繼承,下面剖析一下加數據成員的,這樣可以更清晰的看出內存布沮喎?/kf/yidong/wp/" target="_blank" class="keylink">WPC9wPg0KPHByZSBjbGFzcz0="brush:java;">
#include
這次我們再剖析下各個類的輸出大小
class C1 :virtual public B
{
public:
C1()
{
cout << "C1()" << endl;
}
~C1()
{
cout << "~C1()" << endl;
}
int c1;
};
首先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;
}
我們通過vbptr指針進入c1的虛基類表中
由上面兩圖我們可以畫出c1的內存布局
C2跟C1一樣。
所以
sizeof(C1) == 12;
sizeof(C2) == 12;
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;
};
可以看出,前四個字節是vbptr指針,然後是c1類,+另一個vbptr指針+c2類+D類數據成員d+基類B這樣的結構
我們進入第一個vbptr指針中看
可得出偏移量分別為0(因為沒有虛函數),14
再進入第二個vbptr指針中
可以看出偏移量分別為0(因為沒有虛函數),12
好了,到這裡我們可以畫出D類的內存布局了
所以,
sizeof(D) == 24;