C++第二大特性---繼承
什麼是繼承?
繼承(inheritance)機制是面向物件程式設計使程式碼可以複用的最重要的手段,它允許程式設計師在保持原有類特 性的基礎上進行擴充套件,增加功能。這樣產生新的類,稱派生類。繼承呈現了面向物件程式設計的層次結構, 體現了由簡單到複雜的認知過程。
簡單地說,繼承就是對程式碼的複用。
繼承許可權&訪問限定符
繼承的方式共有public、protected、private三種方式,不同的繼承方式會導致子類的父類的訪問屬性不同。如下表:
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成 員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的 protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 不可見,只能通過基類介面訪問 | 不可見,只能通過基類介面訪問 | 不可見,只能通過基類介面訪問 |
總結:可以將這個表總結為兩點來記憶。
- 父類的private成員不管通過什麼繼承方式,其在子類都是不可見,也就是說子類就訪問不父類的這個私有成員,若強行在子類中呼叫,那麼程式碼在編譯的時候就會報錯;只能通過基類的介面訪問。
- 若規定他們三者的許可權大小為:public>protected>private;那麼要確定基類成員繼承後該成員在子類中的訪問屬性時,我們把繼承方式和該成員在基類中的訪問限定符作比較,許可權小的訪問限定符就是該成員在子類的訪問限定符。
注意: - 一般使用都是public繼承,極少場景下才會使用protected /private繼承 。
- 使用關鍵字class時預設的繼承方式是private,使用struct時預設的繼承方式是public,不過最好顯示的寫出繼承方式 。
繼承中的has-a和is-a關係原則
- is-a:繼承
class Plant
{
//這裡是包含Plant的成員變數,函式等等,來描述Plant這個類的屬性和功能等等
};
class Tree:public Plant //Tree這個類繼承Plant這個類
{
/這裡是包含Tree的成員變數,函式等等,來描述Tree這個類的屬性和功能等等
};
這種模型的特點就是:Tree這個類繼承了Plant類後,不但有自己的一些獨特的屬性或功能(Plant這個類沒有的屬性或功能),而且擁有和Plant相同的屬性或功能。這種關係就屬於is-a關係,也就是說“樹”是一種植物,但"植物"不是“樹“。當我們要設計的時候,遇到一個具有類似這種關係的模型,就可以用is-a關係。
- has-a: 組合
class Tire
{
//這裡是包含Tire的成員變數,函式等等,來描述Tire這個類的屬性和功能等等
};
class Car:public Tire
{
//這裡是包含Car的成員變數,函式等等,來描述Car這個類的屬性和功能等等
private:
Tire a; //Car的私有成員裡面有Tire的物件,因為Tire是Car的一部分
};
has-a關係模型的特點就是:父類的子類一部分,這樣我們就可以在子類中定義基類的物件來構成完整子類應該具有的屬性或功能;例如,我們在子類”汽車“中的成員變數定義基類“輪胎“的物件,這樣做就可以將汽車這個實體包裝完整,因為輪胎就是汽車的一個部件,汽車是由許許多多個部件構成。以後我們設計的時候就可以分析具體的例項來選擇是否用它。
注意:當我們分析具體例項時,以上兩種關係都存在時,那麼有限使用的是 has-a關係,因為總軟體工程的角度來講,has-a的耦合度更低。
賦值相容規則–public繼承
class Person
{
public:
void Display()
{
cout << _name << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
int _num;
};
void Test()
{
Person p;
Student s;
p = s; // 1.子類物件可以賦值給父類物件(切割 /切片)
//s = p; // 2.父類物件不能賦值給子類物件
Person* p1 = &s; // 3.父類的指標/引用可以指向子類物件
Person& r1 = s;
//4.子類的指標/引用不能指向父類物件(可以通過強制型別轉換完成)
Student* p2 = (Student*)&p;
Student& r2 = (Student&)p;
//5. 這裡會發生什麼?
p2->_num = 10;
r2._num = 20;
}
因為我們要在類外面通過test函式來完成一些賦值的操作,所以這裡採用public繼承;
我們將上面的5個問題逐個分析;
- p = s; 子類物件可以賦值給父類物件,這就是我們所謂的切割/切片。相當於將子類中和父類相同的部分賦值過去。
- //s = p; 父類物件不能賦值給子類物件 。因為父類是子類的一部分,子類多出的那一部分無法賦值。
- Person* p1 = &s; Person& r1 = s; 父類的指標/引用可以指向子類物件 。原因很簡單,父類指標指向子類,訪問時也就只能訪問到子類的一部分成員,很合理。引用底層其實也是指標,所以他們兩都可以。
- Student* p2 = (Student*)&p; Student& r2 = (Student&)p;子類的指標/引用不能指向父類物件(可以通過強制型別轉換完成),子類指標指向父類,在訪問時很明顯會越界訪問,但是VS編譯器可以通過強轉來實現,這裡要注意,雖然這樣編譯可以通過,但是是存在隱患的。第五點就可以說明。
- p2->_num = 10; r2._num = 20; 這裡會發生什麼? 當通過強轉將父類指標轉換為子類指標時,執行第五點的兩行程式碼在編譯時也不會報錯,但是執行時就會崩潰掉,因為越界訪問;但非要訪問的話,也是有辦法的,這裡就要涉及到另外一個知識點——RTTI(dynamic_cast),我們可以借用它來完成強轉,並且在讓程式不會崩潰的前提下。這會在我的下個部落格中會詳細說明。
RTTI — dynamic_cast操作符
繼承中的作用域
- 隱藏(重定義) :子類和父類中有同名成員(同名成員包括成員變數和成員函式,成員函式只要函式名相同,就屬於同名成員),子類成員將遮蔽父類對同名成員的直接訪問。(在子類成員函式中,可以使用 基類::基類成員 訪問),也就是說當不指明作用域訪問時,預設是先訪問子類的同名成員。
- 注意在實際中在繼承體系裡面最好不要定義同名的成員
- 隱藏和過載的區別:隱藏不同於過載,過載函式必須有相同的作用域,而構成隱藏的成員變數或函式必須是一個在基類,一個在子類。
派生類預設成員函式
繼承體系下,派生類中沒有顯示定義這六個預設成員函式,編譯器會合成 (區別於生成)這六個預設成員函式。
- 建構函式
- 拷貝建構函式
- 解構函式
- 賦值操作符過載
- 取地址操作符過載
- cons修飾的取地址操作符過載
生成:不依賴於任何東西,只是編譯器根據類的定義簡單生成基於基礎型別的預設成員函式。
合成:必須依賴於基類,編譯器根據基類的相應成員函式的行為來合成派生類的預設成員函式。
注意:
- 基類沒有預設建構函式,派生類必須要在初始化列表中顯式給出基類名和引數列表。
- 基類沒有定義建構函式,則派生類也可以不用定義,全部使用預設建構函式 。
- 基類定義了帶有形參表建構函式,派生類就一定定義建構函式。
面試題:實現一個不能被繼承的類: 將基類的建構函式宣告為private.
繼承與友元
友元關係不能繼承,也就是說基類友元不能訪問子類私有成員和保護成員。
繼承與static靜態成員
基類定義了static靜態成員,則整個繼承體系裡面只有一個這樣的成員。無論派生出多少個子類,都只有一個static 成員例項 。