1. 程式人生 > >C++繼承(單繼承、多繼承、菱形繼承)記憶體模型的深入研究

C++繼承(單繼承、多繼承、菱形繼承)記憶體模型的深入研究

繼承的概念

繼承機制:可以利用已有的資料型別來定義新的資料型別,所定義的新的資料型別不僅擁有新定義的成員,而且還同時擁有舊的成員。

OOP強調軟體的可重用性(software reuseablility).C++提供類的繼承機制,解決了軟體中程式碼重用的問題:程式碼複用

繼承方式

繼承方式

基類的訪問限定

派生類的訪問限定

外部的訪問限定

Public:

public    

public

Yes

protected

protected

No

Private

不可見

No

Protect:

public    

protected

No

protected

protected

No

Private:

不可見

No

Private:

public    

Private:

No

protected

Private:

No

Private:

不可見

No

派生類物件的構造過程是什麼?

在派生類建構函式的初始化列表中,指定基類的構造方式(否則找預設建構函式構造)

構造:基類部分成員=====》派生類的部分

析構:派生類的成員=====》基類的成員析構

1.基類物件首先被建立

2.派生類建構函式應通過成員初始化列表將基類構造資訊傳遞給基類建構函式構造

派生類如何處理和基類同名的成員?

派生類物件訪問的時候,直接訪問的是派生類自己的;

訪問基類的同名成員,需要加上基類的作用域即可。派生類和基類同名的函式中,會出現三種不同的情況,覆蓋,過載,隱藏。

單繼承

單繼承是一般的單一繼承,一個子類只有一個直接父類時稱這個繼承關係為單繼承,這種關係比較簡單是一對一的關係。

B記憶體中包含A類資料成員:


多繼承

一個子類有兩個或以上直接父類時稱這個繼承關係為多繼承。

這種繼承方式使一個子類可以繼承多個父類的特性。

多繼承可以看作是單繼承的擴充套件,因為派生類具有多個基類,派生類與每個基類之間的關係仍可看作是一個單繼承。

多繼承下派生類的建構函式與單繼承下派生類建構函式相似,它必須同時負責該派生類所有基類建構函式的呼叫。

C記憶體中同時包含B和A


同時,派生類的引數個數必須包含完成所有基類初始化所需的引數個數。

多繼承派生類的記憶體模型與繼承列表類的順序是有關的

菱形繼承:

VS上對以上的記憶體模型進行實際測試:這裡的每個類只有一個整形變數


解決菱形繼承中出現的資料冗餘和資料的二義性問題

根本原因:

                B類和C類都同時繼承了A類,所以在B類和C類中都含有A類,造成了資料的冗餘和訪問時的資料二義性

有兩種解決方法:

一、  訪問時標識作用域:使用A資料成員前應該標識是C::中的A還是B::中的A。

二、  用到一種新的繼承方法:虛繼承--解決菱形繼承的二義性和資料冗餘的問題

虛繼承的提出就是為了解決多重繼承時會儲存兩份間接基類資料的問題,也就是說虛繼承機制就只保留了一份副本,但是這個副本是被多重繼承的基類所共享的,編譯器怎麼實現這個機制的?

虛繼承:

在虛繼承機制下繼承出一個D類,關係如圖所示:


以上繼承關係的構造析構順序

記憶體模型:

先通過VS在記憶體中檢視D類的記憶體模型。

對上圖中紅色框出來的資料換種方式進行顯示

會發現這是兩個相同的地址,也就是說在菱形虛繼承下,編譯器在虛繼承下的派生類中添加了一個指標來幫我們解決了資料的冗餘和二義性問題。

實際上這個vbptr指標為指向虛基類表起始地址的指標:

 


虛基類表有八個位元組,每四個位元組表示一個偏移地址。

前四個位元組:

表示該物件的資料成員起始地址相對於虛擬函式表指標vfptr的偏移量

後四個位元組:

表示該物件所繼承過來的基類物件相對於vbptr指標起始地址的偏移量

Is a繼承和has_a繼承的區別和聯絡

Is a 繼承:

整體來看,is-a表示了一種“是的”關係。比如白馬是馬,香蕉是水果,老師是人這種關係。

public繼承是一個介面繼承,保持is-a原則,每個父類可用的成員對子類也可用,因為每個子類物件也都是一個父類物件並且 Is a關係是不可逆的。

Has a繼承:

has-a體現了有這個思想。 
  比如,午餐有香蕉。但是午餐不是香蕉。 
  其實私有跟保護繼承體現了has-a原則。是因為私有跟保護繼承是實現繼承。 
  什麼是實現繼承呢? 
  實現繼承的主要目標是程式碼重用,我們發現類B和類C存在同樣的程式碼,因此我們設計了一個類 A,用於存放通用的程式碼,基於這種思路的繼承稱為實現繼承。 
  我們可以說,午餐中存在香蕉。

protetced/private繼承是一個實現繼承,基類的部分成員並非完全成為子類介面的一部分,是 has-a 的關係原則。 

那麼基類的友元函式能不能被派生類繼承呢?

1.友元函式

  答案是:不能!

友元只是能訪問指定類的私有和保護成員的自定義函式,不是被指定類的成員,自然不能繼承。

並且使用友元類時注意:

(1) 友元關係不能被繼承。

(2) 友元關係是單向的,不具有交換性。若類B是類A的友元,類A不一定是類B的友元,要看在類中是否有相應的宣告。

(3)友元關係不具有傳遞性。若類B是類A的友元,類C是B的友元,類C不一定是類A的友元,同樣要看類中是否有相應的申明

注意事項:

1.友元可以訪問類的私有成員。

2.友元只能出現在類定義內部,友元宣告可以在類中的任何地方,一般放在類定義的開始或結尾。

3.友元可以是普通的非成員函式,或前面定義的其他類的成員函式,或整個類。

4.類必須將過載函式集中每一個希望設為友元的函式都宣告為友元。

5.友元關係不能繼承,基類的友元對派生類的成員沒有特殊的訪問許可權。如果基類被授予友元關係,則只有基類具有特殊的訪問許可權。該基類的派生類不能訪問授予友元關係的類。

靜態成員函式的繼承

基類與派生類的靜態成員函式與靜態成員是共用一段空間的,即靜態成員和靜態成員函式是可以繼承的。

父類的static變數和函式在派生類中依然可用,但是受訪問性控制(比如,父類的private域中的就不可訪問)。而且對static變數來說,派生類和父類中的static變數是共用記憶體空間的,這點在利用static變數進行引用計數的時候要特別注意

派生類的friend函式可以訪問派生類本身的一切變數,包括從父類繼承下來的protected域中的變數。但是對父類來說,他並不是friend的。

基類和派生類同名方法的三種關係是什麼?

函式覆蓋、函式隱藏、函式過載

先分別講一下函式覆蓋,隱藏和過載分別是什麼:

1.成員函式被過載的特徵

(1)相同的作用域(必須在同一個類中)。這點很重要
(2)函式名字相同
(3)引數不同
(4)virtual 關鍵字可有可無。 

2.覆蓋/重寫是指派生類函式覆蓋基類函式,特徵是

(1)不同的作用域(分別位於派生類與基類); 
(2)函式名字相同; 
(3)引數列表(不考慮this指標)相同,返回值相同
(4)基類函式必須有virtual關鍵字。 
  當派生類物件呼叫子類中該同名函式時會自動呼叫子類中的覆蓋版本,而不是父類中的被覆蓋函式版本,這種機制就叫做覆蓋。 
  派生類物件呼叫的是派生類的覆蓋函式 
  指向派生類的基類指標呼叫的也是派生類的覆蓋函式 
  基類的物件呼叫基類的函式

問題:覆蓋函式的訪問許可權問題?

看基類的訪問許可權,因為多型是通過基類指標根據指標型別去呼叫派生類的覆蓋函式,編譯的時候是檢查你基類指標的訪問限定符,也說在編譯時檢查訪問限定符的。

問題:覆蓋函式擁有形參預設值會發生什麼問題?

形參預設值是在編譯的時候確定,也就是說在壓棧的時候是壓入基類的預設值引數,因為你是通過基類指標去呼叫派生類的具體重寫函式。這個時候你在派生類函式引數中寫預設值是沒有意義的

3.隱藏是指派生類的函式遮蔽了與其同名的基類函式,規則如下

(1)    如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。

(2)   如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual 關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)。

(3)   如果想呼叫被隱藏的函式,呼叫處則加上基類的作用域。

(4)   如果想呼叫被隱藏的函式,派生類加上using::fun() 宣告到該類裡面。這樣就破壞隱藏的特徵,也就破壞了隱藏的語法。

6.基類和派生類之間的轉換關係是什麼?

將派生類引用或指標轉換為基類引用或指標被稱為向上強制轉換

編譯器預設支援繼承結構下到上的轉換關係,不支援上到下的轉換關係,從記憶體上來看,基類和派生類的記憶體大小可能不同。如果不使用顯示型別轉換,從上到下的轉換編譯器是不允許的,因為is-a關係是不可逆的。

基類物件                  賦值==》派生類物件    no

派生類指標(引用)  賦值==》基類物件        no

派生類物件               賦值==》基類物件      ok

基類指標(引用)     賦值==》派生類物件      ok