1 繼承的引入
以往我們想分別實現描述學生、老師的類,可能會這樣子做:
class Student
{string _name;string _number;int _tel;int id;string _address;int _age;
};
class Teacher
{string _name;int _level;int _tel;int id;string _address;int _age;
};
但從這兩個類的結構可以看到它們的內容是高度重復的,有沒有什么辦法能將幾個類公共的部分提取出來,又能保留其獨有的特性呢?
這就要用到C++的繼承機制。
2 繼承的概念及定義
2.1 繼承的概念
繼承機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的復用都是函數復用,而繼承是類設計層次的復用。
例:
#include <iostream>
#include <string>
using namespace std;
class Person//父類Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; //姓名int _age = 18; //年齡
};class Student : public Person//子類Student
{
protected:int _stuid; //學號
};class Teacher : public Person//子類Teacher
{
protected:int _jobid; //工號
};
int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}
運行結果:
從運行結果可以看到,繼承后父類的Person
的成員(成員函數和成員變量)都會變成子類的一部分,這里體現出了Student
和Teacher
復用了Person
的成員。
2.2 繼承的定義
2.2.1 繼承的定義格式
我們以剛才的例子為例,講解一下繼承的定義格式。
// 派生類 繼承方式 基類| | || | |
class Student : public Person
{
protected:int _stuid;
};
2.2.2 繼承關系和訪問限定符
2.2.3 繼承基類成員訪問方式的變化
類成員/繼承方式 | public 繼承 | protected 繼承 | private 繼承 |
---|---|---|---|
基類的public 成員 | 派生類的public 成員 | 派生類的protected 成員 | 派生類的private 成員 |
基類的protected 成員 | 派生類的protected 成員 | 派生類的protected 成員 | 派生類的private 成員 |
基類的private 成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
總結:
- 基類
private
成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。 - 基類
private
成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected
。可以看出保護成員限定符是因繼承才出現的。 - 實際上面的表格我們進行一下總結會發現,基類的私有成員在子類都是不可見。基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
- 使用關鍵字
class
時默認的繼承方式是private
,使用struct
時默認的繼承方式是public
,不過最好顯式寫出繼承方式。 - 在實際運用中一般使用都是
public
繼承,幾乎很少使用protetced/private
繼承,也不提倡使用protetced/private
繼承,因為protetced/private
繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
例:
3 基類和派生類對象賦值轉換
3.1 基類和派生類對象賦值轉換引入
我們先來看下面這段代碼:
double d = 1.1;
int i = d;
以前我們說d
要賦值給i
會產生一個具有常性的臨時變量,這是隱式類型轉換。
那么現在有這樣一個問題:
Student s;
Person p = s;
在公有繼承下,一個派生類的對象能不能賦給一個基類的對象呢?
這就涉及到基類和派生類對象間的賦值轉換。
3.2 基類和派生類對象賦值轉換的規則
在公有繼承下,基類和派生類對象間賦值轉換需遵守以下規則:
-
派生類對象可以賦值給
基類的對象
、基類的指針
和基類的引用
,這里有個形象的說法叫切片或者切割,寓意把派生類中基類那部分切下來賦值過去。 -
基類對象不能賦值給派生類對象。
-
基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。這里基類如果是多態類型,可以使用
RTTI(RunTime Type Information)
的dynamic_cast
識別后進行安全轉換,這里先了解即可,后續會講解。
4 繼承中的作用域
- 在繼承體系中基類和派生類都有獨立的作用域。
- 如果子類和父類中有同名成員,那么子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數中,可以使用
基類::基類成員
來進行顯示訪問) - 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。
- 在實際中,在繼承體系里面最好不要定義同名的成員。
5 派生類的默認成員函數
之前我們說過,每一個類都有6個默認成員函數,這個“默認”指的是如果我們自己不寫編譯器就會自動生成。
而在派生類中,這幾個成員函數的生成遵循以下規則:
- 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
- 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
- 派生類的
operator=
必須要調用基類的operator=
完成基類的復制。 - 派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
- 派生類對象初始化先調用基類構造再調派生類構造。
- 派生類對象析構清理先調用派生類析構再調用基類的析構。
- 因為后續一些場景析構函數需要構成重寫,而重寫的條件之一是函數名相同,所以編譯器會對析構函數名進行特殊處理,處理成
destrutor()
,所以父類析構函數不加virtual
的情況下,子類析構函數和父類析構函數構成隱藏關系,這一點在后續會進行講解。