本文主要講解C++物件模型中的菱形繼承的物件模型,分別討論基類物件變數和函式的繼承問題,
何為菱形繼承:
菱形繼承是指一個基類(Base)派生出兩個派生類(Derived1,Derived2),然后這兩個派生類(Derived1,Derived2)派生出一個最終的派生類,如1.1的下圖所示,
一、菱形繼承之非虛繼承
1.1類Base、派生類Derived1、派生類Derived2、最終派生類DDerived的UML結構圖

1.2類Base、派生類Derived1、派生類Derived2、最終派生類DDerived的代碼定義
NonVirtualDerivedDiamondClass.cpp
#include <iostream>
using namespace std;
class Base
{
public:
Base(int x) : x(x) {}
protected:
int x;
};
class Derived1 : public Base
{
public:
Derived1(int y1) : Base(1), y1(y1) {}
protected:
int y1;
};
class Derived2 : public Base
{
public:
Derived2(int y2) : Base(1), y2(y2) {}
protected:
int y2;
};
class DDerived : public Derived1, public Derived2
{
public:
DDerived(int z) : Derived1(11), Derived2(22), z(z) {}
void callX()
{
cout << this->x << endl;
}
protected:
int z;
};
1.3最終派生類DDerived物件模型
由于最終的派生類包含了基類Base、派生類Derived1,Derived2的物件模型,因此只分析最終派生類DDerived物件模型即可,
VS2017開發者模式查看C++物件模型方法可以參考這篇博客:C++單繼承類物件記憶體布局實戰講解和分析
- DDerived在記憶體中的布局

由上圖可知,菱形繼承派生類Derived1和Derived2物件的記憶體中各自繼承和保存基類Base的成員變數int x;,當我們在最終派生類DDerived上呼叫成員變數x時,會出現歧義,DDerived不知道呼叫那個類物件的x,此時編譯會報錯,成員變數x呼叫歧義,如下圖所示,

如果我們要在最終派生類DDerived呼叫繼承而來的x,那么就要顯示指定呼叫的作用域(“::”)限定符,指明是呼叫哪個基類繼承過來的x,即this->Derived2::x,如下代碼所示:
class DDerived : public Derived1, public Derived2
{
public:
DDerived(int z) : Derived1(11), Derived2(22), z(z) {}
void callX()
{
cout << this->Derived2::x << endl; // 顯示指定作用域Derived2::x,呼叫Derived2的成員變數x
}
protected:
int z;
};
從DDerived記憶體布局中可以看出,派生類Derived1和Derived2的類物件都各自保存了一份從基類Base繼承而來的成員變數int x;這樣不但會造成最終派生類DDerived獲取變數x出現歧義,同時也會造成記憶體浪費,那么,是否有辦法解決這些問題呢?答案是肯定的,那就是采用虛繼承,
二、菱形繼承之虛繼承
2.1類Base、派生類Derived1、派生類Derived2、最終派生類DDerived的UML結構圖

由上圖可知,只有派生類Derived1和派生類Derived2繼承類Base時采用虛繼承,而最終派生類DDerived繼承Derived1和Derived2時采用普通繼承,即
Derived1 : public virtual Base { ... };
Derived2 : public virtual Base { ... };
DDerived : public Derived1, public Derived2 { ... };
2.2類Base、派生類Derived1、派生類Derived2、最終派生類DDerived的代碼定義
VirtualDerivedDiamondClass.cpp
#include <iostream>
using namespace std;
class Base
{
public:
Base() = default;
Base(int x) : x(x) {}
protected:
int x;
};
class Derived1 : public virtual Base
{
public:
Derived1(int y1) : Base(1), y1(y1) {}
protected:
int y1;
};
class Derived2 : public virtual Base
{
public:
Derived2(int y2) : Base(1), y2(y2) {}
protected:
int y2;
};
class DDerived : public Derived1, public Derived2
{
public:
DDerived(int z) : Derived1(11), Derived2(22), z(z) {}
void callX()
{
cout << this->Derived2::x << endl;
}
protected:
int z;
};
2.3最終派生類DDerived物件模型
由于最終的派生類包含了基類Base、派生類Derived1,Derived2的物件模型,因此只分析最終派生類DDerived物件模型即可,
VS2017開發者模式查看C++物件模型方法可以參考這篇博客:C++單繼承類物件記憶體布局實戰講解和分析
- DDerived在記憶體中的布局

由上圖可知,最終派生類DDerived的物件模型中,派生類Derived1和派生類Derived2都沒有產生一份基類Base的成員變數int x;的記憶體,而是多了一個虛指標,該虛指標分別指向各自的虛函式表,虛函式表中存放了變數x的偏移地址,通過該偏移地址派生類Derived1和派生類Derived2就可以獲取變數x,此時最終派生類可以直接用this指標呼叫變數x而不會產生歧義,如下圖所示,

因此,虛擬繼承主要是繼承基類成員變數的偏移地址,該偏移地址是保存在虛指標指向的虛函式表上,排列順序為按照變數的宣告順序進依次排列,如下圖所示:

該虛繼承的類都沒有虛函式,那么假如基類存在虛函式,那么虛繼承后的菱形繼承最終派生類的類物件模型是怎么樣的呢?接下來繼續分析和討論,
三、基類有虛函式的菱形繼承之虛繼承
3.1類Base、派生類Derived1、派生類Derived2、最終派生類DDerived的UML結構圖

由上圖可知,基類Base和派生類Derived1、Derived2都有虛解構式和一個虛函式vfun1();,說明這是一個繼承中有虛函式的類,即非POD型別的類,記憶體物件不可逐位元組拷貝memcpy(…),
3.2類Base、派生類Derived1、派生類Derived2、最終派生類DDerived的代碼定義
#include <iostream>
using namespace std;
class Base
{
public:
Base() = default;
virtual ~Base() {}
Base(int x) : x(x) {}
protected:
int x;
private:
virtual void vfun1() = 0;
};
class Derived1 : public virtual Base
{
public:
Derived1(int y1) : Base(1), y1(y1) {}
virtual ~Derived1() {}
virtual void vfun1() override
{
cout << "virtual Derived1::vfun1()" << endl;
}
protected:
int y1;
};
class Derived2 : public virtual Base
{
public:
Derived2(int y2) : Base(1), y2(y2) {}
virtual ~Derived2() {}
virtual void vfun1() override
{
cout << "virtual Derived2::vfun1()" << endl;
}
protected:
int y2;
};
class DDerived : public Derived1, public Derived2
{
public:
DDerived(int z) : Derived1(11), Derived2(22), z(z) {}
virtual void vfun1() override
{
cout << "virtual DDerived::vfun1()" << endl;
}
void callX()
{
cout << this->x << endl;
}
protected:
int z;
};
3.3最終派生類DDerived物件模型

圖3-1 有虛函式的菱形繼承之虛繼承圖

圖3-2 沒有虛函式的菱形繼承之虛繼承圖
由上圖3-1和對比圖3-2可知,有虛函式的菱形繼承之虛繼承的最終派生類DDerived物件模型跟沒有虛函式的菱形繼承之虛繼承的最終派生類DDerived基本一樣,差別只有一個,那就是基類Base多了一個虛指標,該虛指標指向DDerived自身的虛函式表,這個虛函式表跟單繼承的虛函式表一樣,里面存放的都是DDerived自身的虛函式或者繼承而來的虛函式,虛函式表的定義規則是,先將基類虛函式表內容拷貝一份到DDerived自身虛函式表中,然后用DDerived自身的虛函式覆寫虛函式表中同名的虛函式,
同理,當有靜態成員函式和靜態成員變數、普通成員函式時,DDerived的類記憶體模型也同樣不受影響,具體代碼博主就不貼出來了,留一個小作業各位讀者自己驗證,
四、總結
- 菱形虛繼承后基類的成員變數只有一份記憶體,不會在派生類中拷貝一份同樣的成員變數占記憶體;
- 虛繼承后派生類不會拷貝基類成員變數,而是產生一個虛指標指向自身的虛函式表,該虛函式表存放獲取基類成員變數的偏移地址;
- 虛繼承中的類存在虛函式,跟沒有虛函式的虛繼承只有一個差別,那就是產生當前類的虛指標,該虛指標指向最終的派生類的虛函式表,該虛函式表存放最終派生類的所有替換后的虛函式或者繼承而來的虛函式地址;
- 只有非靜態成員才占物件模型的記憶體;
- 類物件的靜態變數和靜態函式都不占用物件模型的記憶體,存放在靜態儲存區;
- 類物件的普通成員函式也不占用物件模型的記憶體,存放在普通資料區
五、參考內容
c++之菱形繼承問題
C++物件模型和布局(三種經典類物件記憶體布局)
C++中菱形繼承的基本概念及記憶體占用問題
C++之繼承(多重繼承+多繼承+虛繼承+虛解構式+重定義)
《深度探索C++物件模型》 侯捷 page:83-134
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/264760.html
標籤:其他
