1. 程式人生 > >繼承*菱形繼承與菱形虛擬繼承(上)

繼承*菱形繼承與菱形虛擬繼承(上)

面向物件程式設計的核心思想是封裝(資料抽象)、繼承和多型(動態繫結)。
通過使用資料抽象,我們可以將類的介面與實現分離;
使用繼承,可以定義相似的型別並對其相似關係建模;
使用動態繫結,可以在一定程度上忽略相似型別的區別,而用統一方式使用它們的物件。

簡單的說,繼承的使用就是為了程式碼複用。
1.繼承

①繼承機制:是為了擴充套件原有類,增加新的功能。

②繼承的定義格式:
子類名:繼承方式 父類名

③繼承方式有三種:private(私有繼承) protected(保護繼承) public(公有繼承);
公有繼承:基類中公有成員和保護成員在派生類中的訪問許可權不發生改變,基類中的私有成員在派生中是不可訪問的,雖然繼承下來了;
保護繼承:基類中的公有成員和保護成員在派生類中都將修改為保護成員,基類中的私有成員在派生類中是不可訪問的;
私有繼承:基類中的公有成員和保護成員在派生類中都將修改為私有成員,基類中的私有成員在派生類中是不可訪問的。

2.繼承中建構函式和解構函式的呼叫順序:

當基類中顯式定義建構函式,派生類中也顯式定義了建構函式:
建構函式的呼叫順序:先調派生類的建構函式,在派生類的初始化列表中調基類的建構函式;

解構函式的呼叫順序:先調派生類的解構函式(將派生類的解構函式體的內容執行完畢,在將要結束派生類的解構函式時呼叫基類的解構函式,將基類的解構函式呼叫完成後會返回到派生類解構函式體的左花括號之前,繼而結束派生類的解構函式)。

解構函式的呼叫順序如下圖:
這裡寫圖片描述

3、總結:

①基類使用系統合成的建構函式(只有在需要時系統才會合成),這時的派生類也可以使用系統合成的建構函式(也是在需要時才會合成)。
當基類顯式定義了預設的建構函式,這時的派生類可以使用系統合成的(系統會在此時合成派生類的建構函式,在派生類的初始化列表會去調基類的建構函式)。

②當基類中定義了非預設的建構函式(需要引數),這時派生類中必須顯式定義建構函式,且在派生類建構函式的初始化列表需要呼叫基類的建構函式(傳參),這裡應該很容易明白,因為系統並不知道該給基類的建構函式傳什麼引數,所以你必須自己進行傳參。

③基類通常都應該定義一個虛解構函式,即使該函式不執行任何實際操作也是如此。

④基類的私有成員在派生類中是不可訪問的,如果基類不想在類外直接訪問,但又想在派生類中可以訪問時,需要定義為protected。可以看出,protected這個訪問許可權是為了繼承才出現的。

⑤派生類會繼承基類中的所有成員,只是訪問許可權有時會發生改變,導致基類的某些成員在派生類中不可見。

⑥每個類控制他自己的成員初始化過程。

⑦防止繼承的發生:c++11新標準提供了一種防止繼承發生的方法,即在類名後加上一個關鍵字final;

⑧使用關鍵字struct時—>預設公有繼承 使用關鍵字class時—–>預設私有繼承。

⑨公有繼承是一個介面繼承,每個子類物件都可以看成是一個父類物件。

4.派生類的物件模型(單繼承)
這裡寫圖片描述

5.同名隱藏:

當派生類中特有的成員和從基類所繼承的成員同名(包括成員變數和成員函式)時,在派生類中會隱藏基類中的成員,即使用派生類的物件直接只能訪問派生類中的(和基類同名)成員,但是有時候也需要可以訪問基類中的成員(同名),這時在派生類的成員函式可以使用形如 : 基類類名::成員名來訪問基類中的同名成員。

①這裡需注意:當父類和子類成員函式同名時,這時只需關注函式名,與引數列表和函式的返回值都無關;當引數列表不同時,可能會有同學認為它們可以構成過載,切記:它們不能構成過載,可以構成過載函式的首要條件是必須在同一作用域內,這裡的父類和子類很明顯是兩個作用域。

②一般在繼承體系中最好不要使用同名成員。

6.賦值相容規則(public繼承):
①子類物件可以賦值給父類物件(子類物件也可以看成一個父類物件);
這裡寫圖片描述

②父類物件不能賦值給子類物件;

這裡寫圖片描述
③子類物件的指標/引用不可以指向父類物件。(強制型別轉換可以完成)。

這裡寫圖片描述

7.友元與繼承:
友元函式不能繼承(因為友元函式不屬於類,還記得友元函式不受類中訪問許可權的限制)。

8.靜態成員變數可以繼承,而且整個繼承體系中只有一份靜態成員變數,是所有類物件所共享的。

9.單繼承多繼承菱形繼承*菱形虛擬繼承

①單繼承:一個子類只有一個直接父類時的這種繼承關係為單繼承。

這裡寫圖片描述
②多繼承:一個子類有兩個或兩個以上父類時的這種繼承關係成為多繼承。
多重繼承的派生類繼承了所有基類的屬性。
這裡寫圖片描述

這裡寫圖片描述

③菱形繼承(鑽石繼承)

這裡寫圖片描述

菱形繼承物件模型(物件成員在空間的佈局):
定義一個D類的物件的d1,

這裡寫圖片描述
從上圖可以看出,在D類物件中存放了兩份_b——>造成二義性(當使用D類物件去直接訪問_b時,變數不明確)。
由此:可知菱形繼承會造成二義性和資料冗餘的問題。
為了解決這個問題,我們引入了虛繼承。

④虛繼承(為了解決菱形繼承造成的二義性和資料冗餘等問題)
虛繼承的目的是令某個類做出宣告,承諾願意共享它的基類。1
注意:虛繼承和一般的繼承在呼叫建構函式時有一點差別,虛擬繼承會先將1壓棧,然後呼叫建構函式,而一般的繼承直接呼叫建構函式,可以看出將1壓棧只是為了區分虛擬繼承和一般的繼承。

通過示例分析虛擬繼承的底層實現:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class A
{
public :
    int _a;
};

class B:virtual public A
{
public :
    int _b;
};

void Test1()
{
    B b;
    b._a = 1;
    b._b = 2;
}

int main()
{
    Test1();
    return 0;
}

通過彙編剖析虛擬繼承是怎樣實現的?

這裡寫圖片描述

通過上圖可得在虛擬繼承中,在構建派生類的物件時,彙編程式碼首先會push 1(一般繼承中沒有),還可以得知:派生類建構函式完成的工作:僅僅是將存放偏移量的表格的地址存放到物件的前4個位元組,還可以得到派生類物件的物件模型。

剖析一般的公有繼承與虛擬繼承在彙編中的差異:
這裡寫圖片描述

⑤菱形虛擬繼承:
這裡寫圖片描述
菱形虛擬繼承物件的模型:
我們先來測試一下菱形虛擬繼承的程式,測試一下D類物件需要佔用多少位元組?
//下面只是程式碼段(沒有加標頭檔案)

class B
{
public:
     int _b;
};

class C1 :virtual public B
{
public :
     int _c1;
};

class C2 :virtual public B
{
public:
     int _c2;
};

class D :public C1, public C2
{
public :
     int _d;
};

void Test1()
{
     D d1;
     cout << sizeof(D) << endl;
     d1._c1 = 0;
     d1._c2 = 1;
     d1._d = 2;
     d1._b = 3;
}

int main()
{
     Test1();
     return 0;
}

結果:
這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

補充:
當不小心把菱形虛擬繼承記憶為下圖時,讓我們一起來看會發生什麼?

這裡寫圖片描述

首先會看到不能解決菱形繼承的二義性問題,
這裡寫圖片描述
D類物件模型建立:

這裡寫圖片描述

由上圖可分析出:這時只有一張存放偏移量的表。