一、繼承的相關基本概念
1、繼承的定義
在C++中,可以使用繼承來使新類得到已定義的一些類中的特性,這就好比與孩子從父親母親得到遺傳類似,所以我們稱原有的類為基類或父類,用原有類來生成新的類的過程稱為派生,所以生成的新類稱之為派生類或者子類。
2、 繼承的聲明
在繼承中和上面所說的遺傳是有區別的,孩子只能遺傳其父母的一些基因,而在C++的繼承中,一個新的類是可以繼承多個不同的類,被稱為多重繼承。所以繼承分為單繼承和多繼承。
繼承的定義格式如下:


在一個類中,我們知道其成員的訪問類型分為三種,公有(public),保護(protected)和私有(private)。相應的繼承方式也分為這三種,他們的區別也就在於派生類的成員和類外對象在訪問它從基類繼承來的的成員的訪問權限不同。
關於三種繼承方式后的成員訪問權限變換關系如下表所示:


其實這個表我自己是看着很煩,其實很容易記的,根本就不需要看這個表的。你只要記得基類的private成員在派生類中始終是不可訪問的,然后記着private>protected>public,高訪問權限的繼承方式會改變低的訪問權限成員的訪問權限。說起來還是比較繞口,其實並不需要記,你看一遍肯定會牢記在心的,不信你試試看。
可以看出來,
不管是哪一種繼承方式,在派生類內都可以訪問基類的非私有成員,基類的私有成員雖然被繼承了,但是並不是可見的。而對於保護和私有繼承方式,類外對象並不能訪問基類的成員,公有繼承方式下可以訪問基類的公有成員。
二、構造函數和析構函數
在派生類中,從基類繼承而來的成員初始化需要調用基類的構造函數,派生類中新增的成員初始化則調用自己的構造函數。
1.繼承中的構造函數
這里,當基類沒有定義自己的構造函數,則在派生類中也可以不用定義,在構造對象時自行調用默認構造函數,但一般在編譯器里會認為構造函數並不做什么事情所以對代碼進行優化,並不生成默認構造函數,當你在轉到反匯編看匯編代碼的時候是看不到調用構造函數的語句的。
當基類顯式地定義了自己的構造函數,則編譯器為派生類生成默認構造函數,而且當基類沒有缺省的構造函數的時候,就必須為派生類定義構造函數且在初始化列表中顯式的給出基類名和其參數列表,不然在派生過程中編譯器就不知道如何調用基類構造函數。
上面這兩點其實與在類中包含另一個類的對象的情況差不多,我們也很容易理解。那么派生類到底能不能繼承基類的構造函數呢?
這一點很多人說法都不一樣,有人說派生類繼承了基類的構造函數,因為你在構造一個派生類對象的時候會調用基類的構造函數。其實准確的說應該是帶引號的繼承,之所以能夠調用基類的構造函數,是因為編譯器使基類的構造函數在派生類中可見,在創建派生類的對象時會先調用基類的構造函數。
構造函數的調用順序:
因為在創建派生類對象的時候,要先給它繼承基類的成員,所以當程序走到派生類的構造函數的時候會先調用基類的構造函數,所以對於構造函數的調用順序,是按照繼承列表的順序依次調用每一個基類的構造函數,然后調用派生類自己的構造函數,再執行它的函數體。當然如果派生類中還有別的類對象,則先調用此對象類的構造函數再調用派生類自己的構造函數。


2.繼承中的析構函數
析構函數是不能被繼承的,同時一般會把基類的析構函數定義成虛析構函數:
1 class Base 2 { 3 public: 4 Base(){} 5 virtual ~Base() 6 { 7 cout << "~Base()" << endl; 8 } 9 public: 10 int _pub1; 11 }; 12 class Derived:public Base 13 { 14 public: 15 Derived(int k=1) 16 { 17 buf = new char[k]; 18 } 19 ~Derived() 20 { 21 delete[] buf; 22 cout << "~Derived()" << endl; 23 } 24 public: 25 char* buf; 26 }; 27 28 void test() 29 { 30 31 Base* b = new Derived(5); 32 delete b; 33 } 34 int main() 35 { 36 test(); 37 return 0; 38 }
如果不定義成虛函數,在delete時只會調用基類的析構函數而不會調用派生類的析構函數而導致內存泄漏。虛函數在這里不細講了。
析構函數的調用順序:
析構函數的調用情況,我們一般認為和壓棧類似,所以析構函數的調用順序如下:


三、繼承中的同名隱藏
在C++中我們知道有重載,當在同一作用域內函數名相同且函數的參數列表不同,就會構成重載,這樣就可以根據傳參的不同來調用相應的函數,而不會存在二義性。
但是在派生類中如果有一個和基類中同名的函數,那么在派生類中基類的這個函數就是被屏蔽的,當你用派生類對象調用這個函數一定是調用派生類中的函數,即使兩個函數的參數列表不同。但其實基類的這個函數還是被繼承了的,要想調用可以使用作用域解析符進行調用。(對於一個同名的成員也是同樣的道理)。例子如下:
1 class A 2 { 3 public: 4 void test(int) 5 { 6 cout << "test1()" << endl; 7 } 8 9 public: 10 int _pub1; 11 protected: 12 int _pro1; 13 private: 14 int _pri1; 15 }; 16 17 class B :public A 18 { 19 public: 20 void test() 21 { 22 cout << "test2()" << endl; 23 } 24 public: 25 int _pub2; 26 protected: 27 int _pro2; 28 private: 29 int _pri2; 30 }; 31 int main() 32 { 33 B b; 34 b.test(); //編譯錯誤 35 b.test(3); //編譯錯誤 36 b.A::test(3); //正確 37 return 0; 38 }
四、繼承中的賦值兼容規則
在此之前,我們看看基類和派生類的對象模型:
1 class Base 2 { 3 public: 4 Base() { cout << "Base()" << endl; } 5 ~Base() { cout << "~Base()" << endl; } 6 public: 7 int _pub1; 8 protected: 9 int _pro1; 10 11 }; 12 class Derived :public Base 13 { 14 public: 15 Derived() { cout << "Derived()" << endl; } 16 ~Derived() { cout << "~Derived()" << endl; } 17 public: 18 int _pub2; 19 protected: 20 int _pro2; 21 }; 22 int main() 23 { 24 return 0; 25 }
假如就這樣定義基類和派生類,那么派生類繼承了基類的_pub1和pro1成員


賦值兼容規則如下:
1、派生類對象可以直接賦值給基類對象
基類對象不能賦值給派生類對象
2、基類類型指針可以指向派生類對象(派生類對象可以初始化基類的引用)
派生類類型指針不可以指向基類對象
這里也比較容易理解(當然是在public繼承下),一個派生類對象本來就是從它的基類繼承而來的,向上圖中的對象模型,在派生類中有與基類對應的一個模塊是繼承來的成員,那么在賦值過程中編譯器是可以把相應的基類部分賦值給基類對象。而對於把一個基類對象賦值給派生類對象的話,
五、理解“is a”和“has a”
我一開始就很不理解為啥要總結這么樣的關系,還這么抽象,什么東西啊。后來寫寫代碼也確實慢慢領會了一點,還是有那嗎一絲絲韻味在其中的。
is a:
is a就是有一個,對於public繼承,就有着is a特性。在上面的賦值兼容規則中也說到了,一個派生類對象時可以賦值給基類對象的,所以派生類可以代替任何需要直接基類的地方。is a就是代表了這種繼承關系。對於多繼承或者在派生類對象中有新增加的東西,這種關系相當於is like。
has a:
has a一般用來描述是組合這種關系的,就是在某一個類中有另一個類。那么在這個類中就可以用它包含的類的成員及成員函數,有時候我們可以把保護和私有繼承也看成是一種has a關系,因為只有在類內才可以訪問基類的成員。
is a相當於父親在兒子家干活,而has a相當於雇別人在家里干活。大概就是這么個意思,大家能理解就行了,對於組合和繼承這里就不展開講了,它們各有優缺點和用武之地。在我們利用繼承的時候,並不是說我們需要在這個類中使用另一個類的某些東西就繼承它,要保證這個類是和基類是有is a關系的,比如說老虎是動物的一種,這才叫繼承。