C++面向對象:派生類與繼承
會有點長,不過讀過就全學會喽!!!!!!
會有點長,不過讀過就全學會喽!!!!!!
會有點長,不過讀過就全學會喽!!!!!!
1.派生類的概念
繼承是面向對象程式設計的一個重要特性,它允許在既有類的基礎上建立新的類, 新 類可以從一個或多個既有類中繼承函數和資料, 而且可以重新定義或加進新的資料和函 數,進而形成類的層次或等級。既有類稱為基類或父類, 在它基礎上建立的新類稱為派生 類、導出類或子類。
1.1 為什麼要使用繼承
繼承性是一個非常自然的概念, 現實世界中的許多事物是具有繼承性的。
現有一個 person 類,它包含有 name( 姓名)、age( 年齡) 、sex (性别 )等資料成員與成員 函數 print( ) , 如下所示:
class person{
private :
char name [10] ;
int age;
char sex;
public:
void print( ) ;
} ;
假如現在要聲明一個 employee 類, 它包含有 name ( 姓名 )、age( 年齡 )、sex( 性别 )、de- partment(部門) 及 salary(工資) 等資料成員與成員函數 print( ) , 如下所示:
class employee{
private :
char name [10] ;
int age;
char sex;
char department [20] ;
float salary;
public:
print( ) ;
} ;
從以上兩個類的聲明中看出,這兩個類中的資料成員和成員函數有許多相同的地方。 隻要在 person 類的基礎上再增加成員 department 和 salary, 再對 print ( )成員函數稍加修 改就可以定義出 employee 類。像現在這樣定義兩個類, 代碼重複太嚴重。為了提高代碼 的可重用性,就必須引入繼承性, 将 employee 類說明成 person 類的派生類,那些相同的成 員在 employee 類中就不需要再定義了。
繼承性也是程式設計中的一個非常有用的、有力的特性, 它可以讓程式員在既有類的 基礎上,通過增加少量代碼或修改少量代碼的方法得到新的類, 進而較好地解決了代碼重 用的問題。
1.2 派生類的聲明
為了了解 一個類 如何繼 承另 一個類, 我們看 一下 employee 類是 如何繼 承 person 類的。
聲明一個派生類的一般格式為:
class 派生類名∶派生方式 基類名 {
/ / 派生類新增的資料成員和成員函數
} ;
這裡“, 派生類名”就是要聲明的新類名“, 基類名”是一個已經定義的類。“派生方 式”可以是關鍵字 private 或 public。如果使用了 private, 則稱派生類從基類私有派生; 如 果使用了 public,則稱派生類從基類公有派生。派生方式可以預設, 這時派生方式預設為 private ,即私有派生。是以, 由類 person 派生出 類 employee 可以 采用下面 的兩種格 式 之一:
-
公有派生
class employee∶public person{ / / … } ;
-
私有派生
class employee∶private person{ / / … } ;
這兩種派生方式的特點如下:
- 無論哪種派生方式, 基類中的私有成員既不允許外部函數通路, 也不允許派生類 中的成員函數通路,但是可以通過基類提供的公有成員函數通路。
-
公有派生與私有派生的不同點在于基類中的公有成員在派生類中的通路屬性。
公有派生時, 基類中的所有公有成員在派生類中也都是公有的。
私有派生時, 基類中的所有公有成員隻能成為派生類中的私有成員。
下面我們分别讨論私有派生和公有派生的一些特性。
1 . 私有派生
-
私有派生類對基類成員的通路
由私有派生得到的派生類, 對它的基類的公有成員隻能是私有繼承。也就是說基類的所有公有成員都隻能成為私有派生類的私有成員, 這些私有成員能夠被派生類的成員 函數通路,但是基類私有成員不能被派生類成員函數通路。
-
外部函數對私有派生類繼承來的成員的通路
私有派生時,基類的公有成員在派生類中都成為私有成員, 外部函數不能通路。
2 . 公有派生
在公有派生中,基類中的私有成員不允許外部函數和派生類中的成員函數直接通路, 但是可以通過基類提供的公有成員函數通路。基類中的公有成員在派生類中仍是公有成 員,外部函數和派生類中的成員函數可直接通路。
說明:
- 派生類以公有派生的方式繼承了基類, 并不意味着派生類可以通路基類的私有 成員。
- 在派生類中聲明的名字支配基類中聲明的同名的名字, 即如果在派生類的成員 函數中直接使用該名字的話,則表示使用派生類中聲明的名字
1.3 保護成員的作用
為了讓派生類通路基類的私有成 員, C + + 還提供了具有另一種通路屬性的成員———protected 成員, 這種成員稱為保護成 員。
protected 說明符可以放在類聲明的任何地方,通常将它放在私有成員聲明之後, 公有 成員聲明之前。
class 類名 {
[ private: ]
私有資料成員和成員函數
protected:
保護資料成員和成員函數
public :
公有資料成員和成員函數
} ;
保護成員可以被派生類的成員函數通路,但是對于外界是隐藏起來的, 外部函數不能 通路它。是以,為了便于派生類的通路, 可以将基類私有成員中需要提供給派生類通路的成員定義為保護成員。
在公有派生情況下, 基類中所有成員的通路特性在派生類中維持 不變;在私有派生情況下, 基類中所有成員在派生類中成為私有成員。
2.派生類的構造函數和析構函數
基類都有顯式的或隐式的構造函數和析構函數。當建立一個派生類對象時, 如何調 用基類的構造函數對基類資料初始化,以及在撤消派生類對象時, 又如何調用基類的析構 函數來對基類對象的資料成員進行善後處理
2.1 派生類構造函數和析構函數的執行順序
通常情況下,當建立派生類對象時, 首先執行基類的構造函數, 随後再執行派生類的 構造函數; 當撤消派生類對象時, 則先執行派生類的析構函數, 随後再執行基類的析構 函數。
# include < iostream .h >
class base{
public:
base( ){ cout < < ”Constructing base class \ n”; } / / 基類的構造函數
~base ( ) { cout < <”Destructing baes class \ n”; } / / 基類的析構函數
} ;
class derive :
public base{ public: derive( ) / / 派生類的構造函數
{ cout < < ”Constructing derived class \ n”; }
derive ( ) / / 派生類的析構函數
{ cout < < ”Destructing derived class \ n”; }
} ;
main ( ) {
derive op;
return 0 ;
}
程式運作結果如下:
Constructing base class
Constructing derived class
Destructing derived class
Destructing base class
從程式運作的結果可以看出:構造函數的調用嚴格地按照先調用基類的構造函數, 後 調用派生類的構造函數的順序執行。析構函數的調用順序與構造函數的調用順序正好相 反,先調用派生類的析構函數, 後調用基類的析構函數。
2.2 派生類構造函數和析構函數的構造規則
當派生類中含有對象成員時,其構造函數的一般形式為:
派生類構造函數名(參數表) :基類構造函數名( 參數表) ,對象成員名 1 (參數表) , …, 對象成員名 n (參數表)
{
/ / …
}
在定義派生類對象時,構造函數的執行順序如下:
- 基類的構造函數
- 對象成員的構造函數
- 派生類的構造函數
撤消對象時,析構函數的調用順序與構造函數的調用順序正好相反。
說明:
- 當基類構造函數不帶參數時, 派生類不一定需要定義構造函數, 然而當基類的構 造函數哪怕隻帶有一個參數,它所有的派生類都必須定義構造函數, 甚至所定義的派生類 構造函數的函數體可能為空,僅僅起參數的傳遞作用。
- 若基類使用預設構造函數或不帶參數的構造函數, 則在派生類中定義構造函數 時可略去“∶基類構造函數名(參數表)”; 此時若派生類也不需要構造函數, 則可不定義構 造函數。
- 如果派生類的基類也是一個派生類, 則每個派生類隻需負責其直接基類的構造, 依次上溯
- 由于析構函數是不帶參數的, 在派生類中是否要定義析構函數與它所屬的基類 無關,故基類的析構函數不會因為派生類沒有析構函數而得不到執行, 它們各 自是獨 立的
3.多 重 繼 承
前面我們介紹的派生類隻有一個基類, 這種派生方法稱為單基派生或單一繼承。當 一個派生類具有多個基類時,這種派生方法稱為多基派生或多重繼承。
3.1 多重繼承的聲明
在 C + + 中,聲明具有兩個以上基類的派生類與聲明單基派生類的形式相似, 隻需将 要繼承的多個基類用逗号分隔即可,其聲明的一般形式如下:
class 派生類名: 派生方式 1 基類名 1, …,派生方式 n 基類名 n {
/ / 派生類新增的資料成員和成員函數
} ;
在多重繼承中,公有派生和私有派生對于基類成員在派生類中的可通路性與單繼承 的規則相同。
說明:對基類成員的通路必須是無二義的, 例如下列程式段對基類成員的通路是二義 的,必須想法消除二義性。
class X{
public:
int f( ) ;
} ;
class Y{
public: int f( ) ;
int g( ) ;
} ;
class Z∶public X, public Y {
public:
int g( ) ;
int h ( ) ;
} ;
假如定義類 Z 的對象 obj:
Z obj;
則以下對函數 f( )的通路是二義的:
obj .f( ) ; / / 二義性錯誤,不知調用的是類 X 的 f( ) ,還是類 Y 的 f( )
使用成員名限定可以消除二義性,例如:
obj .X∷f( ) ; / / 調用類 X 的 f( )
obj .Y∷f( ) ; / / 調用類 Y 的 f( )
3.2 多重繼承的構造函數與析構函數
多重繼承構造函數的定義形式與單繼承構造函數的定義形式相似, 隻是 n 個基類的 構造函數之間用“,”分隔。多重繼承構造函數定義的一般形式如下:
派生類構造函數名(參數表) :基類 1 構造函數名 ( 參數表) , 基類 2 構造函數名 (參數 表) , …,基類 n 構造函數名(參數表)
{
/ / …
}
類 X 和類 Y 是基類, 類 Z 是類 X 和類 Y 共同派生出來 的,請注意類 Z 的構造函數的定義方法。
# include < iostream .h >
class X{
int a ;
public:
X(int sa ) / / 基類 X 的構造函數
{ a = sa; }
int getX( ) { return a ; }
} ;
class Y{
int b;
public:
Y(int sb) / / 基類 Y 的構造函數
{b = sb; }
int getY( )
{ return b; }
} ;
class Z: public X, private Y{ / / 類 Z 為基類 X 和基類 Y 共同的派生類
int c;
public:
Z(int sa, int sb, int sc ) :X(sa) , Y(sb) / / 派生類 Z 的構造函數,綴上
{ c = sc ; } / / 基類 X 和 Y 的構造函數
int getZ( ) { return c; }
int getY( ) { return Y∷getY( ) ; }
} ;
main ( ) {
Z obj( 2, 4, 6) ;
int ma = obj .getX( ) ;
cout < < ”a = ”< < ma < < endl;
int mb = obj .getY( ) ;
cout < < ”b = ”< < mb < < endl;
int mc = obj .getZ( ) ;
cout < < ”c = ”< < mc < < endl;
return 0 ;
}
多重繼承的構造函數的執行順序與單繼承構造函數的執行順序相同, 也是遵循先執 行基類的構造函數,再執行對象成員的構造函數, 最後執行派生類構造函數的原則。在多 個基類之間, 則嚴格按照派生類聲明時從左到右的順序來排列先後。而析構函數的執行 順序則剛好與構造函數的執行順序相反。
3.3 虛基類
1 . 為什麼要引入虛基類
當引用派生類的成員時, 首先在派生類自身的作用域中尋找這個成員, 如果沒有找 到,則到它的基類中尋找。如果一個派生類是從多個基類派生出來的, 而這些基類又有一 個共同的基類,則在這個派生類中通路這個共同的基類中的成員時, 可能會産生二義性。
2 . 虛基類的概念
不難了解,如果在上例中類 base 隻存在一個拷貝, 那麼對 a 的引用就不會産生二義性。在 C + + 中,如果想使這個公共的基類隻産生一個拷貝,則可 以将這個基類說明為虛基類。這就要求從類 base 派生新類時, 使用關鍵字 virtual 将類 base 說明為虛基類。
# include < iostream .h >
class base{
protected:
int a;
public:
base ( ) { a = 5; } } ;
class base1∶virtual public base{
public:
base1 ( ) { cout < < ”base1 a = ”< < a < < endl; }
} ;
class base2∶virtual public base{
public:
base2 ( ) { cout < < ”base2 a = ”< < a < < endl; }
} ;
class derived∶public base1, public base2{
public:
derived( ) { cout < < ”derived a = ”< < a < < endl; }
} ;
main ( ) {
derived obj;
return 0 ;
}
在上述程式中,從類 base 派生出類 base1 和類 base2 時,使用了關鍵字 virtual ,把類 圖 4 .3 虛基類的類層次圖 base 聲明為 base1 和 base2 的虛基類。這樣, 從 base1 和 base2 派生出的類 derived 隻有一個基類 base, 進而可以消 除二義性。
3 . 虛基類的初始化
虛基類的初始化與一般的多重繼承的初始化在文法 上是一樣的,但構造函數的調用順序不同。虛基類構造函 數的調用順序是這樣規定的:
- 若同一層次中包含多個虛基類, 這些虛基類的構造函數按對它們說明的先後次 序調用。
- 若虛基類由非虛基類派生而來, 則仍然先調用基類構造函數, 再調用派生類的構 造函數。
- 若同一層次中同時包含虛基類和非虛基類, 應先調用虛基類的構造函數, 再調用 非虛基類的構造函數,最後調用派生類構造函數, 例如:
class X∶public Y, virtual public Z{ / / … } ;
X one;
定義類 X 的對象 one 時,将産生如下的調用次序:
Z( ) ;
Y( ) ;
X( ) ;
顯然, 當 derived 的構 造函數調用了虛基類 base 的構造函數之後, 類 base1 和類 base2 對 base 構造函數的調用 被忽略了。這也是初始化虛基類和初始化非虛基類不同的地方。
說明:
- 關鍵字 virtual 與派生方式關鍵字 ( public 或 private ) 的先後順序無關緊要, 它隻 說明是“虛拟派生”。
class derived: virtual public base{ / / … } ;
class derived: public virtual base{ / / … } ;
- 一個基類在作為某些派生類虛基類的同時, 又作為另一些派生類的非虛基類, 這 種情況是允許存在的
class B{ / / … } ;
class X∶virtual public B{
/ / … } ;
class Y∶virtual public B{
/ / … } ;
class Z∶public B{ / / … } ;
class AA∶public X, public Y , public Z{
/ / … } ;
此例中,派生類 AA 由類 X、類 Y 和類 Z 派生而來。AA 與它的間接基類 B 之間的對 應關系是:類 B 既是 X、Y 繼承路徑上的一個虛基類, 也是 Z 繼承路徑上的一個非虛基類。