歡樂C++ —— 15. 繼承

參考:

https://www.cnblogs.com/QG-whz/p/5132745.html

簡述

還記得我們之前寫的編譯期多態與運行期多態嗎?繼承是運行期多態的基石。下一節則是模板,編譯期多態的基礎。

運行期多態
  • 優點
    1. OO設計中重要的特性,對客觀世界直覺認識。
    2. 能夠處理同一個繼承體系下的異質類集合。
  • 缺點
    1. 運行期間進行虛函數綁定,提高了程序運行開銷。
    2. 龐大的類繼承層次,對接口的修改易影響類繼承層次。
    3. 由於虛函數在運行期在確定,所以編譯器無法對虛函數進行優化。
    4. 虛表指針增大了對象體積,類也多了一張虛函數表,當然,這是理所應當值得付出的資源消耗,列爲缺點有點勉強。

OOP核心思想在C++中表現是三部分:數據抽象,繼承和動態綁定。通過使用數據抽象,可以將類的接口和實現分離(也就是類);使用繼承,可以定義相似的對象類型,並對其關係建模;使用動態綁定,在一定程度上忽略了相似類型間的區別,統一了相似類的接口。

首先通過小例子來看看爲什麼繼承。

  • 通過前面的學習我們得知了類的概念,現在考慮這樣一種應用場景,要編寫一些管理學生的類,分別管理中學生,大學生,研究生。

    按照面對對象的思想,我們應該給這幾個不同的學生都要對應一個類。現在問題來了,學生這一個羣體,它有一些共同屬性,例如年齡,姓名,性別等等,這些屬性及其所對應的方法沒必要在每個類中單獨寫一遍,這樣既不利於編寫代碼(多個類萬一有的屬性沒打對),也不利於後期維護(如果發現哪個方法錯誤,要修改的話所有類都需要修改一遍)。

    到了使用繼承的時候了,我們可以將這些共同的屬性統一組織到一個學生類(基類),然後其它的中學生,大學生等(派生類)都可以通過繼承這個類,獲得它所有的屬性。這樣修改時只需要修改這個基類,減少代碼量和方便維護。

    後面我們會進一步介紹,這種表示一個基類是派生類共同屬性,派生類是基類的擴展 這種繼承關係是public 繼承 ,學生類是 抽象基類

  • 還有一些類它們間的關係和上面的例子又不太相似。考慮這樣一種關係:發動機和汽車。直接按上面的思想來繼承的話就會有根本思想的錯誤,發動機不具有汽車的通有屬性,發動機更像是汽車的一個組成部分,實際上對於這種關係我們也採用繼承。

    這種繼承關係是private 繼承,表達基類是派生類一部分的概念。

繼承的概念不是很難,但在實際應用中正確使用比較困難,本文只介紹基礎應用,後期會再做補充擴展。

繼承基礎

靜態類型和動態類型

表達式的靜態類型在編譯期間是已知的,它是變量聲明時的類型。動態類型則是內存中對象的類型,動態類型只有運行時才能確定。

只有基類指針或引用的動態類型和靜態類型 纔可能 不一致,從根本說,這纔是C++支持多態的基礎。

靜態綁定,動態綁定 所謂動態綁定是相對靜態綁定而言,靜態綁定就是我們要調用哪個函數是在編譯期就已經綁定決定好,而動態綁定是在運行時決定的。

可以將派生類的指針和引用傳遞給基類的指針和引用,當通過基類的指針或引用調用虛成員函數時,那麼會根據這個指針或引用所指向的對象是 基類對象還是 派生類對象,從而調動基類或派生類的虛成員函數。

引用的底層就是指針,所以在這它和指針的作用等價。

聲明

聲明一個派生類,不需要加上派生列表 class Deriver;

並且如果想繼承某個類,則基類必須在之前已經定義過。沒有定義則爲不完整類型,無法繼承。

繼承與靜態成員

如果基類定義了一個靜態成員,則在整個繼承體系中只有該成員的唯一定義。

拷貝和賦值

當派生類對象賦值給基類對象,或作爲參數拷貝構造基類對象,調用的都是基類的函數。因爲基類對象無法轉化爲派生類對象。而派生類對象可以只使用基類部分。

虛函數

基類必須區分兩類函數,一類是希望派生類直接繼承而不要改變的函數,一類是希望派生類對其進行覆蓋的函數。對於後者,基類應將其聲明爲虛函數(virtual)。當我們使用指針或引用調用虛函數時,調用將會被動態綁定。這樣帶來的好處是我們可以以同一種形式調用不同版本的函數。根據指針或引用的動態類型不同而調用不同的函數,簡化編程。

虛函數適用於公有繼承,因爲跟公有繼承的思想符合。

任何構造函數之外的非靜態函數都可以是虛函數。virtual 聲明只能出現在類內。

如果派生類想覆蓋基類的虛函數,就必須函數名,形參,返回類型 都相同(而函數重載不要求返回類型)。

必須爲每一個虛函數提供定義,因爲編譯器在編譯時不知道將來會運行哪個版本的虛函數。

override 和final 可以控制成員函數在繼承中關係,都是添加在形參後

  • final 指定一個函數(包括虛函數與非虛函數)不可在派生類中被覆蓋

    假如有學生類(基類,有年齡屬性),那麼不管是哪個階段的學生(不同的派生類),其查詢年齡的函數都是一樣的,爲了避免派生類不小心覆蓋這個函數,就可以在學生類中將此函數聲明爲final

  • override 指定這個函數是覆蓋基類的虛函數

    主要用於排除一些人爲的錯誤,如果指定一個函數爲override 說明想用這個函數覆蓋基類的虛函數,假如此時不小心形參沒寫對,或者返回類型錯誤等等,則編譯器會直接指出該函數未能正確覆蓋。

虛函數的默認實參:通常使用 引用或指針的靜態類型中虛函數默認實參,所以儘量不要有同一個虛函數基類子類默認實參不同的情況。

處於派生體系中間的類即是基類也是派生類,如果指針或引用的靜態類型是該類,則虛函數默認實參是該類指定的默認實參,

class Base {
public:
	virtual void show( int x=10) {
		cout <<x<< "666" << endl;
	}
};
class D1 :public Base{
public:
	void show(int x=20 )override {
		cout << x<<"D1" << endl;
	}
};
class D2 :public D1 {
public:
	void show(int x = 30)override {
		cout << x<<"D3" << endl;
	}
};
int main( ) {
	D1 * x = new D1( );
	D1 * y = new D2( );
	x->show( );
	y->show( );
	return 0;
}
/*結果
20D1
30D3
*/

如果想避免虛函數動態綁定機制,指定域名訪問父類中的虛函數

BaseClass *p = new DerivedClass();	//基類有fun 虛函數,並且派生類已覆蓋
p->fun();				//調用派生類fun
p->BaseClass::fun();	//調用基類fun
虛析構函數

只要有虛函數,則析構函數應該設置爲虛函數,這樣當delete 動態類型和靜態類型不一致的指針對象時,可以正常先調用派生類的析構函數,再調用基類。

底層實現

其底層實現是通過虛函數指針和虛函數表來實現,具體感興趣可進一步搜索研究

純虛函數

純虛函數就是沒有定義的虛函數,具有純虛函數的類是抽象基類,通過 =0 聲明一個虛函數使其變爲純虛函數。

= 0 實際上將該函數的指針指向NULL

抽象基類

類是描述對象的模板,而有些東西,它是一個抽象的概念,例如汽車,遊戲,電腦,這種表示一類物品的抽象概念。例如遊戲,遊戲都具有發佈時間,運行平臺等屬性(這些屬性就應該放在基類)。當我們說遊戲,世界上所有的遊戲產品都是遊戲這個概念下的實體,例如王者榮耀,它是遊戲這個類的派生類的一個具體對象。當我說創建一個遊戲時,無法直接創建遊戲這兩個字所代表的特定對象。

所以,這些抽象類,它不對應現實世界中的實體,我們無法直接獲得它的對象,只能通過子類進一步描述後再獲取對象。

爲了實現這樣的功能,c++提供了抽象基類,抽象基類不可以定義對象。從實現的角度來說具有純虛函數的類稱爲抽象基類。子類中沒有實現純虛函數的類依然是抽象基類。

一個抽象基類中所有函數都是純虛函數稱爲接口類。

訪問權限與繼承關係

訪問權限

在前面 類與對象的基礎 中我們已經看到了類內兩種訪問權限 public private ,這塊,我們看一個新的訪問限制符 受保護的成員protected,這個訪問限制只有在繼承中表現與private不同。

如果基類中成員定義爲protected 則基類,派生類中都可以訪問它,而限制通過基類對象和派生類對象訪問。

繼承限制

類內的訪問限制符是限制用戶通過對象訪問類 ,而在繼承中訪問限制符是 控制派生類用戶對基類的訪問權限

如果想更改繼承的權限可以在派生類中使用 using

繼承關係
  • 公有繼承表示派生類是基類,派生類是對基類更詳細的描述的概念。(如動物類爲基類,人類爲派生類)
  • 私有繼承表示基類是派生類一部分的概念。(如發動機類是基類,汽車類是派生類)
  • 保護繼承不太常用,它的特點不是很鮮明。
派生類向基類的轉換的可訪問性

當操作者是用戶時,只有派生類公有繼承基類纔可以轉換。

當操作者是派生類時,派生類向基類的轉換都是可以的。

其它相關知識點

類作用域

重載 指的是函數名相同,形參不同。在繼承中,派生類只要和基類中 **函數名相同 **就會發生 同名隱藏 現象。

而通過using 可以解除同名隱藏現象。此時可進行 基類與派生類 函數重載。

class Base {
public:
	void show( int x) {
		cout << "666" << endl;
	}
};
class D1 :public Base{
public:
	void show(string str) {
		cout << str << endl;
	}
};
class D2 :public D1 {
public:
    //using Base::show;
	void show( ) {
		cout << "7777" << endl;
	}
};
int main( ) {
	D2 d;
	d.show(2);  //x	D2 沒有接受int 的成員函數,Base 和 D1 的show 方法都被隱藏了。如果添加第15行,則正常
    
    D1 dd;
    dd.show();  //x D1 沒有無參的show 函數
    
	return 0;
}

除了覆蓋基類的虛函數之外,不要定義和基類同名的成員。

構造函數與拷貝控制

派生類構造函數必須調用基類構造函數

派生類賦值運算符也需要調用基類賦值運算,控制基類的賦值。

派生類析構函數不用調用基類析構函數,其會自動調用。

繼承構造函數

可以通過using 聲明來繼承 基類的構造函數

繼承與智能指針與容器

當在容器中存放派生類或基類對象時,通過存放指針,可以混用。這種集合稱爲異質對象集合。

多重繼承與虛繼承

多重繼承

在派生類的派生類列表中繼承多個基類。例如以下繼承體系,Panda 既繼承Bear 又繼承 Endangered ,就稱爲多重繼承。

image-20200705094320763
class ZooAnimal {
public:
	int z = 1;
};

class Bear :public ZooAnimal {
	int b = 2;
};

class Endangered {
	int e = 4;
};

class Panda :public Bear, public Endangered {
	int p = 5;
};

int main( ) {
	Panda temp;

	return 0;
}

構造順序

在多重繼承的情況下,派生類會在構造函數初始化列表中,按照派生列表中基類出現的順序,初始化所有的基類。(當有虛繼承時,存在例外)

虛繼承

多重繼承體系中,有些繼承體系得到的結果卻不是我們所期望的。假如如下繼承體系:

image-20200705092305415

先看看多重繼承:

class ZooAnimal {
public:
	int z = 1;
};

class Bear :public ZooAnimal {
	int b = 2;
};
class Raccoon :public ZooAnimal {
	int r = 3;
};
class Endangered { 
	int e = 4;
};

class Panda :public Bear, public Raccoon, public Endangered { 
	int p = 5;
};

int main( ) {
	Panda temp;
	temp.z = 6;	//error z 不明確
    temp.Raccoon::z = 6;
    
	return 0;
}

這樣的思路是沒有問題的,但是實際上實現的過程中有點問題。但爲什麼 z 不明確呢?實際上只要明白這種情況下的內存分佈就很好理解。

可以看到,對於 Panda 對象來說,它實際上有兩個 ZooAnimal 成員,一個是由 Bear 繼承而來,另一個是由Raccoon 繼承而來。兩個ZooAnimal 對象的數據沒有關係。

這顯然不是我們想要表示的繼承關係,在我們想表示的繼承關係中,熊貓 就是屬於 動物園動物,而不是說它屬於兩個動物園動物。這種情況下,就要考慮使用虛繼承。

虛繼承的就是令某個類願意共享它繼承的基類(被共享的基類稱爲虛基類),所以虛繼承一般出現在在繼承體系的中間部分(即除過最底層的基類和最頂層的派生類)。無論一個派生類的派生體系中出現過多少次虛基類,它都只有一份虛基類對象。

//... 其它部分都不改變,只是添加兩個virtual 關鍵字
class Bear :virtual public ZooAnimal {	//表示願意與其它類共享ZooAnimal
	int b = 2;
};
class Raccoon :virtual public ZooAnimal {
	int r = 3;
};
//...

image-20200705100236395

底層實現

其底層是通過虛繼承表 和虛繼承指針 實現,具體感興趣可進一步搜索研究

構造順序

由最底層的派生類,按照繼承列表的順序,先構造所有虛基類,無論是直接還是間接。之後再按照順序構造普通基類,最後再構造自身。而析構順序與構造順序相反。虛基類總會優先於普通基類構造。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章