天天看點

初夏小談:C++繼承(二)之菱形虛拟繼承

子類/派生類的六大預設成員函數如何生成?

(一)在說子類/派生類的成員函數生成之前,都有哪六大成員函數?

即1.負責對象初始化和最後清理的。有構造函數和析構函數。

    2.負責拷貝和複制的。有拷貝構造函數和運算符重載函數。

    3.負責取位址重載的。有針對普通對象的和const對象的。

(二)派生類的成員函數的生成規則:

1.子類/派生類初始化時,必須調父類/基類的構造函數來初始化父類/基類的對象。如果父類/基類沒有預設的構造函數,這就需要在子類/派生類的構造函數的初始化清單中進行自定義調用。

說明:什麼是預設構造函數:有兩種就是編譯器自己生成的,再一個就是自定義全預設的構造函數。其它自定義構造函數都是非預設構造函數。需要在派生類的構造函數初始化清單出進行指派。

2.如果派生類/子類想要完成父類/基類的拷貝構造時,這就必須在派生類的拷貝構造函數調用基類的拷貝構造函數。

3.如果派生類想要完成基類的operator=。就必須在自己的operator=中進行調用。

4.派生類/子類在最後的清理資源時,在清理完自己所有資源後,會自動調用基類的析構函數,完成基類對象的資源清理工作。為什麼在最後調用呢?這是因為有可能在子類清理時會用到基類的對象是以待子類清理完後再去清理基類對象資源。

5.派生類的對象初始化是先調用基類的構造函數再調自己的構造函數。

***經典小題:如何設計一個類使之不能被繼承?

有兩種方法來解決:第一種是将該類的析構函數設定為私有。即private。則将析構函數私有化。

                                第二種是在該類名的後面加上final,說明該類為最終類不可被繼承。這是C++11中新添的。

如果将該類的析構函數私有化後仍舊想執行個體化該類對象。則在類中給一個共有的靜态方法來調用析構函數,給成靜态的目的是在類外可以通過作用域+方法的方式調用。如果沒有static那麼就必須建立好對象才能進行調用。

代碼如下:

#include<iostream>

using namespace std;

//設計一個類使得它不能被繼承

//兩種方法:

// 1.将父類的構造函數給成私有化

// 2.在父類類名後加final表明禁止繼承(C++11)

class BASE //final

{

public:

static BASE GetBASE()

{

return BASE();

}

void SetInfo(int data)

{

_data = data;

}

void PrintBASE()

{

cout << "BASE::_data = " << _data << endl;

}

private:

//public:

BASE()

{}

private:

int _data;

};

class CHILD : public BASE

{};

int main()

{

//CHILD c;

BASE b = BASE::GetBASE();

b.SetInfo(6);

b.PrintBASE();

//CHILD c;

system("pause");

return 0;

}

運作結果:

初夏小談:C++繼承(二)之菱形虛拟繼承

6.友元關系不能被繼承,就是基類的友元函數不能通路子類的保護成員和私有成員。

7.在基類中定義了static成員,則的子類中無論執行個體化多少個對象都隻有這一個static成員。

(三)菱形繼承是什麼?

由于菱形繼承是多繼承和單繼承的組合。是以在說菱形繼承之前,先來說說多繼承。

1.多繼承顧名思義就是一個類繼承了多個類。就是不同繼承體系下的對象模型(對象成員在記憶體中的布局情況)。需要注意的是這個布局是和繼承的類的先後順序一緻。

2.知道了多繼承後就來看看菱形繼承,顧名思義就是類似于菱形的繼承。

頂級父類A,子類B1,B2均繼承于A,而C有繼承于B1,B2.的這種繼承。就是菱形繼承,可以結合下圖了解:

初夏小談:C++繼承(二)之菱形虛拟繼承

在菱形繼承的類中有以下幾個問題:

1.在C類的大小是多少?

2.在C類對象中能不能繼承A的成員,由于B1,B2均繼承,如果C可以繼承那麼将繼承誰的?或者又是什麼?

3.如果通過B1,B2設定了A類的成員,那麼A類成員中的值到底是誰的?

解決辦法:

針對問題一:

1.想要得知C類對象的大小那麼必須知道它所繼承的所有類的大小。來分析以下:在32位作業系統下A類的大小是4個位元組,B1,B2,由于繼承了A類是以都是8個位元組,而C類繼承了B1和B2那麼就是2*8,16個位元組。再加上自己的4個位元組就是20個位元組。正因為是20個位元組而不是16個位元組,就引發了一個問題即下面問題。

針對問題二:

隻要繼承了的類成員都是public和protected修飾的。都可以再子類中通路。說明C類可以繼承A的成員。但是是C對象通路A類對象時通路的是B1,還是B2的呢?這就存在二義性的問題? 這個坑編譯器表示我不背。那就報錯,你們自己處理。是以要想通路A類的成員,

有兩種方法:第一種是C對象想通路A的可通路成員時,需要提供作用域說明是誰的。

                     第二種方法是進行虛拟繼承。在後面将詳細說明。

兩種方法的差別:第一種隻是明确了是誰的。但是還是存在兩份。而第二種将兩者合一,隻有一份去除二義性。

針對問題三:

如果在C中通路設定了B1類繼承的A類的成員,也在C中通路設定了B2類中繼承的A類的成員。這時就必須看。C類繼承的順序,如果C先繼承B1類再繼承B2類,則A的成員将設定和B1一樣,否則和B2一樣。

4.菱形繼承代碼執行個體:

#include<iostream>

using namespace std;

class A

{

protected:

int _a;

};

class B1 : public A

{

void SetAInfo(int a)

{

_a = a;

}

protected:

int _b1;

};

class B2 : public A

{

public:

void SetAInfo(int a)

{

_a = a;

}

protected:

int _b2;

};

class C : public B2, public B1

{

public:

void SetAInfo(int a1, int a2)

{

B1::_a = a1;

B2::_a = a2;

}

void SetAB1Info(int a1)

{

B1::_a = a1;

}

void SetAB2Info(int a2)

{

B2::_a = a2;

}

void PrintSizeof()

{

cout << "sizeof(C) = " << sizeof(C) << endl;

}

void PrintAB1B2a()

{

cout << "A::_a = " << A::_a << endl;

cout << "B1::_a = " << B1::_a << endl;

cout << "B2::_a = " << B2::_a << endl;

}

protected:

int _c;

};

//不能再c中直接去設定a的成員的值,存在二義性,務必要區分是設定B1B2中的哪一個

//方法一:(加類名作用域限定符區分),明确是哪一份(根本上沒有解決)隻存一份就可以了

//方法二:菱形虛拟繼承 --- > 可以解決菱形繼承中存在的二義性問題

int main()

{

C c;

c.PrintSizeof();

c.SetAInfo(2, 6);

//c.SetAB2Info(8);

//c.SetAB1Info(1);

//在頂級父類中的成員變量的值取決于第一個繼承的類中設定該成員的值

c.PrintAB1B2a();

system("pause");

return 0;

}

運作結果:

初夏小談:C++繼承(二)之菱形虛拟繼承
結果驗證上述所說。A的成員_a是B2設定的,因為C繼承時是先繼承B2,後繼承B1的。

(四)什麼是虛拟繼承?

1.虛拟繼承是如何實作?

虛拟繼承就是在被繼承的類的繼承方式前面加上關鍵字virtual。

2.虛拟繼承與普通繼承的差別?

1.對象模型倒立(成員變量在記憶體中基類成員在最下面。)

2.對象中多了四個位元組--最上面是編譯器自己維護。

3.編譯器為派生類生成預設的構造函數--2個參數  空間首位址 1代表虛拟繼承的标志

3.剖析虛拟繼承的過程

初夏小談:C++繼承(二)之菱形虛拟繼承
說明:在B類虛拟繼承A類時。此時在B中執行個體化A類中的成員變量時。在記憶體中将取B對象的前四個位元組内容為位址--->在這個位址上+4取到裡面的内容data--->從對象起始位址向後偏移data和位元組将1指派給基類成員_a.B對象前四個位元組的内容為位址所映射的就是虛基表,第一個位元組存目前類對象的偏移量0個位元組。第二個位元組是目前對象所繼承的基類的成員的位置的偏移量。

(五)虛拟繼承解決菱形繼承的問題

以上面的菱形繼承圖為例根本原因是虛拟繼承将B1和B2繼承的A的成員變成了一份。具體做法是B1對象的前四個位元組内容為位址所映射的就是虛基表,前四個位元組記錄了B1對象所在的偏移量。而後四個位元組記錄了繼承A成員的偏移位置。B2也是一樣都将指向同一塊基類成員。

代碼執行個體:

#include<iostream>

using namespace std;

class A

{

public:

void PrintA()

{

cout << "&A = " << this << endl;

cout << "&A::_a = " << &(*this)._a << endl;

}

public:

int _a;

};

class B1 : virtual public A

{

public:

void PrintB1()

{

cout << "&B1 = " << this << endl;

cout << "&B1::_a = " << &(*this)._a << endl;

}

public:

int _b1;

};

class B2 : virtual public A

{

public:

void PrintB2()

{

cout << "&B2 = " << this << endl;

cout << "&B2::_a = " << &(*this)._a << endl;

}

public:

int _b2;

};

class C : public B2, public B1

{

public:

int _c;

};

int main()

{

C c;

cout << "sizeof(C) = " << sizeof(C) << endl; //24

c._a = 5;

c._b1 = 6;

c._b2 = 7;

c._c = 10;

c.PrintA();

c.PrintB1();

c.PrintB2();

system("pause");

return 0;

}

運作結果:

初夏小談:C++繼承(二)之菱形虛拟繼承
​從結果中可以看到虛拟繼承可以将存儲的兩份基類成員變為一份。A類的成員和B1,B2類所從A繼承的成員将會是同一份。​

(六)繼承與組合比較:

1.public繼承是一種is-a的關系。也就是說每個派生類對象都可以看作一個基類對象。

2.組合是一種has-a的關系。假設B組合了A,每個B對象中都有一個A對象,但不能把B看作A的基類對象。

​3.​能使用組合時,盡量使用組合,不使用繼承。

​4.​繼承是更關注裡面的實作細節,俗稱白箱操作。破壞了類的封裝。子類和父類依賴關系很強,耦合度高。

​5.​組合也是複用的一種手段。需要時拿過來,再加上自己需要的特性形成新的對象。組合是黑箱複用,不用關注裡面實作了什麼,隻要它滿足我的需求,就直接拿來使用。組合類之間沒有很強的依賴關系,耦合度低。

6.盡量使用組合其代碼維護性好,耦合度低,少使用繼承。具體适合哪一種就使用哪一種。二者都可以時使用組合。