繼承和派生
繼承的前提:分類
聚集(aggregation)
對于那些有明顯層次之分的類,在構造後代類對象的時候,不是在其中包含一個前驅類對象,而是在前驅類對象的基礎上,在其後面直接添加新的特征。這使得後代對象完全将前驅對象融合在自己内部,而且,每一個後代的前面部分就是一個完整的上一代前驅的對象
在C++和其它面向對象的語言中,聚集的實作過程被稱為“繼承(inheritance)”和“派生(derivation)”
繼承的優點
使程式員可以不必重頭開始編寫下層類,而隻是在上層類的基礎上進行修改和擴充。這使得類的可重用性和可擴充性得到充分的展現。
繼承和派生的概念
1.基本概念:基類和派生類
在最簡單的情況下,如果說,一個類felid繼承自類carnivore,那麼通常将類carnivore稱為“基類(base class) ”,或者“父類(parent class) ”;類felid稱為carnivore類的“派生類(derived class) ”,或者“子類(child class) ”。
class 派生類名:access 基類類名//accsee為通路控制
{
…
};
繼承基類的所有成員
派生類實際上包含了它所有基類中除了構造函數、析構函數和指派運算符之外的所有成員(思考:why?)
2.改造基類成員
方式一 通過不同的派生方式改造基類成員
方式二 就是在派生類中聲明與基類成員同名的成員來覆寫基類成員
3.增加新的成員
根據派生類的實際特征,增加不同于基類的成員
從編碼的角度,派生類從基類中以較低的代價換來了較大的靈活性。一旦産生了可靠的基類,隻需要調試派生類中所做的修改即可。派生類從基類繼承屬性時,可使派生類對其進行限制,也可以改變或隐藏。
eg:
class carnivore
{
};
class felid : public carnivore
{
public:
bool slitPupil;
};
class tiger : public felid
{
public:
void roar() { std::cout << name << " roars" << std::endl; }
//other members
};
在上述的三代繼承中,幾個類構成了繼承的類等級。其中,felid是tiger的“直接基類”,carnivore是tiger的“間接基類”,或者稱為“祖先類”
這便就是類等級。
如果要在派生類的成員函數中使用祖先類的成員,可以用如下文法:
祖先類名::祖先類成員
但這樣做是不能通路基類的私有成員的。(如果一定要通路呢?——用protected)
4.通路控制
正如在上面的代碼中示意的那樣,C++中,派生類的一般文法形式為
class 派生類名 : <access> 基類類名
{
成員定義
};
通路控制(access control),可以是以下三者之一:
public、private、protected
基類的通路控制描述符和類中的通路控制描述符(段描述符)用的是相同的關鍵字,差別:
• 段描述符用于控制類的成員在類外的可通路性,例如私有成員在類外是不可通路的;
• 而繼承中的通路控制描述符則描述了基類的成員将被放在派生類的哪個段中,例如私有繼承将基類的所有成員放在了派生類的私有段中
5.基類的protected成員
基類的某些私有成員必須對派生類是可見的,或者可通路的,而在派生類外卻又是不可見的。
解決方案:定義成protected成員
特點:無論在基類還是在派生類外都無法被通路到,但在派生類中卻可以被直接通路。
基類的protected成員是這樣一類成員:它們無論在基類還是在派生類外都無法被通路到,但在派生類中卻可以被直接通路。
class carnivore
{
protected:
std::string name;
//other members
};
6.通路聲明
如果考慮Rectangle是私有派生的情況,那麼其代碼可能如下:
如此一來,felid的所有成員都将成為tiger的私有成員。這帶來了一個問題:原來felid的公有成員通過tiger的對象中無法通路。
為了讓被私有化的成員在派生類外可見,那麼可以使用通路聲明來恢複其公有屬性。
class tiger : private felid
{
public:
using felid::prey;
};
注意:這裡僅有名字而沒有任何類型規格說明。
通路聲明隻能恢複成員原有的通路屬性,而不能提升或降低它們的可通路性。
但也有如下定義:
class A
{
private: int f(int);
public: int f();
};
class B : private A
{
public: A::f;
};
這個通路聲明回複的是哪個版本?
編譯器無法判定,是以是個錯誤。
思:如何解決?
7.基類靜态成員的派生
class carnivore
{
public:
//other members
static int counter;
};
現在的問題是,它派生類将以什麼樣的方式繼承其基類的靜态成員呢?
C++規定,在整個繼承樹中,所有後代都和基類共享唯一的靜态成員。換句話說,就是無論存在多少個基類和/或派生類對象,隻要其中一個改變了靜态成員的值,那麼這個改變将反映到其它所有的對象中。這嚴格遵循了靜态成員是屬于類而非對象的原則。
正是由于這個原因,對靜态成員的通路都采用如下限定方式:
基類名::靜态成員名
這種通路方式受到該靜态成員通路控制的限制。
8.開閉原則
當一個基類被設計出來,那麼它對修改是封閉的,隻對擴充開放。這就是OOD原則中的“開閉原則(OCP, Open-Close Principle)”。
基于開閉原則,不能因為派生類需要某些功能就去修改基類的源碼。派生類隻能通過繼承的方法去擴充基類
如果在設計繼承關系時,發現一個派生類需要大量修改基類的成員,或者抵消基類的責任,那麼這種繼承關系就不應該成立。
基類與派生類的關系
1.基類對象的初始化
在派生過程,派生類首先繼承基類的成員,然後再增加派生類具有自己特性的成員。那麼可以設想,派生類對象建立的時候,在調用其構造函數之前,一定會先調用其基類構造函數來構造基類子對象部分。
在 C++ 中,派生類構造函數的聲明為:
派生類構造函數(參數清單):基類(參數清單),成員(參數清單),…
{ … }
在派生類的構造函數執行過程中,遵循**先父輩(基類),再客人(對象成員),後自己(派生類)**的順序。如果基類使用預設構造函數或不帶參數的構造函數,那麼派生類構造函數聲明中“:”後面的“基類(參數清單)”一項可以省去,但是派生類構造函數執行時仍然隐含地調用基類的構造函數
可不可以将child的構造函數定義成如下形式呢?
答案是否定的。因為i不是child的基類,也不是其直接成員,是以上述語句在編譯時會出現錯誤報告。是以,要初始化基類子對象(的成員),必須将這項任務交給基類的構造函數去完成。
2.派生類對象和基類對象的互相轉換
前面提到過,派生類對象中包含一個基類子對象。是以,在這個基礎上,派生類對象可以和基類對象之間進行規定的轉換。
-
派生類對象和基類對象間的直接指派
設有如下對象定義:
felid f;
carnivore c;
那麼指派語句
c = f;
是合法的。
這種指派将派生類對象中屬于基類的部分賦給了指定的基類對象,而隻屬于派生類對象的部分被舍棄了。這種現象稱為**“切片(slicing)”**
如果将兩個對象位置互換,那麼指派就是非法的:
f = c; //非法(思:why?——空間記憶體)
-
引用作用于派生類和基類對象
設有如下定義:
felid f;
carnivore &cr = f;
此時,引用cr的初始化是合法的,且cr成為了f的别名。
注意:派生類對象賦給基類的引用不會引起派生類對象到基類對象的轉換。
同一個對象在f和cr“眼中”,記憶體布局是不一樣的。通過名字cr看不到這部分記憶體。
反過來,例如:
carnivore c;
felid &fr = c;
這樣的初始化是非法的。
如果一定要這麼做,那麼隻能這樣:
felid &fr = dynamic_cast<felid&>©;
這要求felid類必須是個多态類。
-
指針作用于派生類和基類對象
設有如下定義:
felid f;
carnivore *p = &f;
與引用的情況類似,以上對p的初始化是合法的。此時,指針p隻“看到”了屬于基類子對象的部分,而其餘部分在被它忽略。
基類指針直接指派給派生類指針是非法的:
carnivore c;
felid *q = &c; //非法
如果一定要這麼做,那麼也可以使用類型強制轉換運算符:
q = dynamic_cast<felid *>(&c);
總的來說,派生類對象可以直接指派給其基類對象或者基類引用;派生類對象的指針(位址)可以直接指派給其基類指針。這種現象稱為**“up-casting”,不必使用任何的強制類型轉換。這是一種非常重要的機制,面向對象技術的核心概念之一“多态”就是依賴于這個機制。
而反過來,我們稱之為“down-casting”,是有條件的,并且應當使用dynamic_cast**運算符完成轉換,以保證類型的安全。
3. 派生類中重新定義基類的成員
- 派生類中重新定義基類的資料成員
class carnivore
{
protected:
string name;
};
class felid : public carnivore
{
protected:
string name;
public:
string who() const { return name; }
};
派生類中就擁有兩個同名的name成員。
問題是,這裡通路的name成員屬于派生類自己還是基類?
這涉及到名字查找(name lookup)問題。每一個類都定義了一個作用域,即使是派生類也不例外。名字查找機制首先在類自己的作用域中查找指定的名字,如果找到,那麼肯定是使用這個名字;如果沒有找到,那麼就會到該類的外圍基類中查找,如此類推,直到找到,或者失敗(這會産生一個編譯時錯誤)。簡而言之,就是派生類的資料成員将會屏蔽基類中的同名資料成員。
屏蔽并不意味着基類的成員就被删除了,它們還是可以被通路到的,隻不過需要使用名字限定。例如,我們可以将who()改成如下形式:
string felid::who()
{
return "base name:" + carnivore::name + ", my name:" + name;
}
基類成員的通路屬性描述符仍然會發揮作用:私有的基類成員在其派生類中是不可通路的。
-
派生類中重載基類的成員函數
與重新定義資料成員一樣,派生類中可以重新定義基類的成員函數。顯然,這是典型意義上的函數重載。
class carnivore
{
public:
string who() const { return name; }
};
class felid : public carnivore
{
public:
string who() const { return carnivore::name + “::”+name; }
};
who()在派生類中被原型一緻地重載。
函數重載規則:
• 在相同的作用域中(例如同一個類中),重載的函數原型必須不同;
• 在不同的作用域中(例如不同的類中),重載的函數原型可以相同。
無論如何,派生類的函數名将屏蔽基類中重名的函數,即使它們的原型不一緻。
4. 派生類繼承基類重載的運算符函數
作為基類的特殊成員,重載的運算符函數也能被派生類繼承,除了指派運算符。這種限制是可以了解的,因為派生類的内部結構與基類不同,而基類的指派操作不可能覆寫到派生類中新增的成員。
基類與派生類在類型上的相容性為繼承運算符函數提供了基礎。
一個派生類對象内部嵌入了一個基類子對象
這樣一來,派生類就擁有了基類的功能。這是類之間合作關系的一種展現。而這種合作關系是一種“is a”的關系。
“has a”或“is part of” 的關系
這是一種屬于複合的合作關系,但并不是嚴格意義上的繼承關系
例如:轎車有四個輪胎
判斷類A和類B之間是何關系可以應用如下兩個斷言,二者隻能有一個成立,或者都不成立:
A是一種B(或反過來)
A包含B(或反過來)
何時使用繼承
1. 類/對象之間的關系
在設計類的過程中,判斷類A和類B之間是否構成繼承關系應用如下斷言:
B是一種A
如果上述斷言成立,那麼類A和B之間的繼承關系成立,并且A是B的基類。
2. 組合/聚集複用原則
組合和聚合都是對象模組化中關聯(Association)關系的一種.聚合表示整體與部分的關系,表示“含有”
原則描述:
• 分類是在分類學上有意義的
• 分類不是按照角色進行的
• 類之間的關系是Is-a
• 永遠不會出現需要将派生類的對象換成另一類對象的情況
• 派生類具有擴充基類的責任,而不是具有修改或抵消基類的責任
如果以上條件不能同時滿足,那麼首先考慮類之間應該使用組合/聚集。這就是OOD原則中的另一條原則:組合/聚集複用原則(CARP,Composition/Aggregation Reuse Principle)。
在繼承發生時,派生類雖然接受了基類的所有成員,但有些基類成員在派生類中是不能被繼承的。這些基類的成員是:
• 構造函數
• 析構函數
• 重載的operator=
此外,基類授權的友元關系也不能被派生類繼承。
提問:為什麼上述機制不能被繼承?
繼承性是對象之間合作的另一種方式(另兩種方式是友元類和對象作成員),派生類繼承了基類,一個派生類對象除了可以包含基類對象,這一點和對象作成員類似,派生類還可以繼承基類中的成員, 派生類對象可以在類外直接使用繼承的基類公有成員
繼承的意義
利用面向對象方法,一個操作的特殊實作的變化将僅僅影響實作所應用的類。加一個新類型在許多情況下不影響其它類。再者,分布性是關鍵:類管理自己的事務,不幹涉其他内政,各人自掃門前雪,這是獲得分布式結構的要求。同時,虛函數提供了多種操作的界面一緻性。
這樣,類、繼承、重載、虛函數的動态比對和泛型在一起,提供了對可重用性和可擴充性需要卓越的表達能力。