繼承的基本概念
繼承(inheritance)機制 是面向對象程序設計使代碼可以複用的最重要的手段,它允許程序員在 保持原有類特性的基礎上進行擴展 ,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象程序設計的層次結構, 體現了由簡單到複雜的認知過程。以前我們接觸的複用都是函數複用,繼承是類設計層次的複用。
繼承的定義
//基類
class Person
{
};
//派生類
//class 派生類名: 繼承方式 基類名
//public 繼承方式 默認爲私有方式
//:Person 繼承類列表
class Child:public Person
{
//派生類特有的成員變量與成員函數
};
//派生類的大小爲基類成員變量及自身的成員變量
//一個基類可以有多個派生類
繼承成員訪問權限的變化
- 基類private成員在派生類中無論以什麼方式繼承都是不可見的。這裏的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類裏面還是類外面都不能去訪問它。
- 基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義爲protected。可以看出保護成員限定符是因繼承纔出現的。
- 基類的私有成員在子類都是不可見。基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
- **使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,**不過最好顯示的寫出繼承方式。
- 在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡使用 protetced/private繼承,因爲protetced/private繼承下來的成員都只能在派生類的類裏面使用,實際中 擴展維護性不強。
基類對象與派生類對象賦值轉換
- 派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這裏有個形象的說法叫切片或者切割。
- 基類對象不能賦值給派生類對象
- 基類的指針可以通過強制類型轉換賦值給派生類的指針。但是必須是基類的指針是指向派生類對象時纔是安全的。
/Child 和 Person是繼承的關係 public
//公有制繼承is-a :可以將派生類對象看成是基類的對象,在所有使用基類對象的位置都可以使用派生類對象進行替換
Person person; //基類
Child child; //派生類
child.SetPerson(20); //賦值
//可以把派生類中基類的部分看成一個對象
//把派生類中基類對象的成員變量賦值給基類對象
person = child;
//派生類對象可以賦值給 基類的對象 / 基類的指針 / 基類的引用 作用
Person* pperson = &person;
pperson = &child;
pperson->SetPerson(30);
Person& rperson = child;
繼承的作用域
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數中,可以使用 基類::基類成員 顯示訪問)
- 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏(不是函數重載)。
**注:**在實際中在繼承體系裏面最好不要定義同名的成員
child._b = 20; //派生類賦值
child.Person::_b = 30; //派生類中同名基類成員賦值
派生類的默認成員函數
- 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
- 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
- 派生類的operator=必須要調用基類的operator=完成基類的複製。
- 派生類的析構函數會在被調用完成後自動調用基類的析構函數清理基類成員。因爲這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
- 派生類對象初始化先調用基類構造再調派生類構造(先調用當前對象的構造函數,但在初始化列表的位置先調用初始化列表中的構造函數)。
- 派生類對象析構清理先調用派生類析構再調基類的析構(在派生類析構函數的最後插入一條調用基類析構函數)。
//派生類構造函數
Child()
:Person(20)
{}
/*
編譯器生成構造函數
語法:如果一個類沒有顯示定義構造函數,編譯器就會生成一個默認的的構造函數
實際:如果一個類沒有顯示定義構造函數,假如編譯器需要構造函數,就會生成一個無參的構造函數
如果不需要,編譯器就不會生成
*/
/*
如果基類的構造函數存在
1.如果基類的構造函數是缺省的構造函數,
如果派生類沒有顯示定義構造函數,編譯器就會生成一個默認的構造函數,因爲要在生成默認構造函數初始化列表的位置完成基類成員的初始化
如果派生顯示定義構造函數,必須要在初始化列表的位置調用基類的構造函數,完成基類成員的初始化(編譯器可以自動調用)
2.如果基類的構造函數是非缺省的構造函數
用戶必須顯示給派生類定義構造函數,並且在其構造函數初始化列表的位置顯示調用基類的構造函數,完成基類成員的初始化
*/
//派生類拷貝構造
Child(const Child& d)
:Person(d)
{}
//派生類operator=
Child& operator=(const Child& d)
{
(*this).Preson::operator=(b); //調用父類的賦值
//子類拷貝
}
//派生類析構函數
~Child()
{
//子類的析構函數與父類的析構函數構成同名隱藏
//不顯示調用父類析構,子類析構完成後會自動調用父類析構函數(防止用戶無法保證析構的順序)
//符合棧,先定義,後析構
}
禁止繼承
禁止繼承關鍵字 ——final
class NonInherit final //c++11
繼承與友元
友元關係不能繼承,也就是說基類友元不能訪問子類私有和保護成員
繼承與靜態成員
基類定義了static靜態成員,則整個繼承體系裏面只有一個這樣的成員。無論派生出多少個子類,都只有一 個static成員實例
複雜的菱形繼承及菱形虛擬繼承
不同繼承體系下的對象模型——派生類的對象模型
單繼承: 基類部分+派生類部分
多繼承: 多個基類(按繼承列表的順序) + 派生類
多重繼承中可能會存在二義性問題
菱形繼承
class B
{
public :
int _b;
};
class C1 :public B
{
public:
int _c1;
};
class C2 :public B
{
public:
int _c2;
};
class D :public C1, public C2
{
public:
int _d;
};
//這種繼承方式,會導致在最後的D類中有兩份B類的數據,這就無法確切的確定數據的值,這樣就會造成數據的二義性和數據冗餘。
//解決
//1.添加作用域限定符::
//2.菱形虛擬繼承
菱形虛擬繼承
菱形虛擬繼承
虛擬繼承關鍵字:virtual
class d : virtual public B //在菱形的上半邊添加虛擬關鍵字
{
public:
int _d;
};
//編譯器會生成默認構造函數
菱形虛擬繼承與普通菱形繼承的區別
爲了更方便解析,我們使用一個更加完整的過程來實現