程序員成長之旅——繼承和多態

程序員成長之旅——繼承和多態


C++的三大特性有:封裝、繼承、多態
今天我們來探索一下繼承和多態。

繼承和多態

先簡單理解一下繼承和多態:繼承相當於子類繼承了父類的數據和方法,子類父類我們也稱爲派生類和基類,繼承一般我們在子類中添加的是父類沒有的成員。而多態是建立在繼承之上的,它使用了C++編譯器最核心的技術,即動態綁定技術。其核心思想是父類對象調用子類對象的方法。

接下來我們通過一些問題加深這塊的理解:
C++類繼承的三種關係
C++中繼承主要有三種關係:public、protected、private。

public繼承子類也是public,可以替代父類完成父類接口所聲明的行爲;
protected繼承子類也是protected,子類不能自動轉換成爲父類的接口。從語法來說,子類還是可以調用父類的protected成員,也就是只能在內部訪問,不能在外部訪問;
private繼承子類是private,雖然子類還可以調用父類中的public和private成員,但是子類的子類就不可以調用了。

在這裏插入圖片描述
私有繼承和組合有什麼相同點和不同點
相同點:都可以表示“有一個”關係
不用點:私有繼承中派生類能訪問基類的protected成員,並且可以重寫基類的虛函數,甚至當基類是抽象類的情況。組合不具有這些功能。
什麼是多態
多態性的定義:同一操作作用於不同的對象,可以有不同的解釋,產生不同的執行結果。有兩種類型的多態性:
(1)編譯時的多態性。編譯時的多態性是通過重載來實現的。對於非虛的成員來說,系統在編譯的時候,根據傳遞的參數,返回的類型等信息決定實現何種操作。
(2)運行時的多態性。運行時的多態性就是指直到系統運行時,才根據實際情況決定何種操作。C++中,運行時的多態性是通過虛成員實現。
虛函數是怎麼實現的
簡單的來說的話,虛函數是通過虛函數表來實現的。
事實上,如果一個類中含有虛函數,則系統會爲這個類分配一個指針成員指向一張虛函數表,表中每一項指向一個虛函數的地址,實現上就是一個函數指針的數組。
構造函數調用虛函數
在構造函數中,虛擬機制不會發生作用,因爲基類的構造函數在派生類構造函數之前執行,當基類構造函數運行時,派生類數據成員還沒有被初始化。
爲什麼需要多重繼承,優缺點是什麼
實際生活中,一些事物往往會擁有兩個或兩個以上事物的屬性,爲了解決這個問題,C++引入了多重繼承的概念。
優點:對象可以調用多個基類中的接口
缺點:容易出現繼承向上的二義性
多重繼承二義性的消除
(1)加上全局符確定調用那一份的拷貝。
(2)使用虛擬繼承就可以。
多重繼承和虛擬繼承
(1)任何虛擬基類的構造函數按照它們被繼承的順序構造
(2)任何非虛擬基類的構造函數按照它們被構造的順序構造
(3)任何成員對象的構造按照它們聲明的順序調用
(4)類自身的構造函數
繼承和組合的區別?什麼時候用繼承?什麼時候用組合
(1)public繼承是一個is_a的關係,也就是每個派生類對象都是一個基類對象
(2)組合是一個has_a的關係,假設B組合了A,每個B對象中都有一個A對象
(3)繼承一定程度會破壞基類的封裝,依賴關係強,耦合度高
(4)組合是繼承之外另一種複用選擇,它的依賴關係不強,耦合度低。
(5)實際儘量去用組合。它的維護性高,但是要呈現多態的話就必須用繼承。類之間的關係的換一般用組合就好。
(6)私有繼承中派生類能夠訪問基類的protected成員,並且可以重寫基類的虛函數,甚至當基類是抽象類的情況。組合不具有這些功能。
爲什麼要引入抽象基類和純虛函數
(1)爲了方便使用多態特性
(2)在很多情況下,基類本身生成對象時不合情理的。例如:動物可以作爲一個基類派生出老虎、獅子等子類,但動物本身生成對象明顯不合常理。抽象基類不能夠實例化,它定義的純虛函數相當於接口,能把派生類的共同行爲提取出來。
虛函數和純虛函數有什麼區別
(1)類中如果聲明瞭虛函數,這個函數是實現的,哪怕是空實現,它的作用就是爲了能讓這個函數在它的子類裏面可以被覆蓋,這樣編譯器就可以使用後期綁定來達到多態了。純虛函數只是一個接口,是個函數的聲明而已,它要留到子類裏去實現。
(2)虛函數在子類裏面也可以不重寫,但是純虛函數必須在子類中實現,它就是一個接口。
(3)虛函數的類用於“實作繼承”,也就是繼承接口的同時也繼承了父類的實現。當然,大家也可以完成自己的實現。純虛函數的類用於“介面繼承”,即純虛函數關注的是接口的統一性,實現由子類完成。
(4)但純虛函數的類叫做虛基類,這種基類不能直接生成對象,而只有被繼承,並重寫其虛函數後,才能使用。這樣的類也叫做抽象類。
虛函數存在哪?虛表存在哪?
虛函數和普通函數一樣,都是存在代碼段的,只是它的指針存在了虛表中。另外對象存的不是虛表,而是虛表指針。驗證VS下存的也是代碼段的。
inline函數可以是虛函數嗎?
不能,因爲inline函數沒有地址,無法將地址放到虛函數表中。
靜態成員可以是虛函數嗎?
不能,因爲靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。
構造函數可以是虛函數嗎?
不能,因爲對象的虛函數指針是在構造函數初始化列表階段才初始化的。
析構函數可以是虛函數嗎?
可以,並且最好把析構函數定義成虛函數。原因:將可能會被繼承的父類的析構函數設置爲虛函數,可以保證當我們new一個子類,然後使用基類指針指向該子類對象,釋放基類指針時可以釋放掉子類的空間,防止內存泄漏。
對象訪問普通函數快還是虛函數快
首先如果是普通對象,是一樣快的。如果是指針對象或者是
引用對象,則調用的普通函數快,因爲構成多態,運行時調用虛函數需要到虛函數表中去查找。
虛函數表是什麼階段生成的,一般存在哪
虛函數表是在編譯階段就生成的,一般情況下存在代碼段(常量區)的。
什麼是DLL HELL
DLL HELL 主要是指DLL(動態鏈接庫)版本衝突的問題。一般情況下,DLL新版本會覆蓋舊版本,那麼原來的舊版本的DLL的應用程序就不能繼續正常工作了。
虛函數表
大家都知道,虛函數是通過一張虛函數表來實現的。在這個表中,主要是一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,其內容真實反映了實際的函數。這樣,在有虛函數的類的實例化中,這個表被分配在了這個實例的內存中,所以當用父類的指針來操作一個子類的時候,這個虛函數表就非常重要了,它就像一個地圖一樣,指明瞭實際所應該調用的函數。
C++的標準規格說明書中說到,編譯器必須保證虛函數表的指針存在於對象實例中最前面的位置(這個是爲了保證正確取到虛函數的偏移量)。這意味着通過實例地址得到這張虛函數表,然後就可以遍歷其中的函數指針,並調用相應的函數。
舉個例子大家就懂了

#include<iostream>
using namespace std;

class Base
{
public:
	virtual void fun1() {cout << "Base::fun1" << endl;}
	virtual void fun2() {cout << "Base::fun2" << endl;}
	virtual void fun3() {cout << "Base::fun3" << endl;}
private:
	int num1;
	int num2;
};
typedef void (*fun)(void);

int main()
{
	Base b;
	Fun pFun;
	
	pFun = (Fun)*((int*)*(int*)(&b)+0);
	pFun();
	pFun = (Fun)*((int*)*(int*)(&b)+1);
	pFun();
	pFun = (Fun)*((int*)*(int*)(&b)+2);
	pFun();
}

執行結果是:

Base::fun1
Base::fun2
Base::fun3

在這裏插入圖片描述
一個類中會有多少張虛函數表呢?
對於一個單繼承的類,如果它有虛擬函數,則只有一張虛函數表。對於多重繼承的類,它可能有多張虛函數表。

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