- 后臺開發:核心技術與應用實踐
- 徐曉鑫
- 4087字
- 2019-01-03 20:55:34
2.2 繼承與派生
1.繼承與派生的一般形式
繼承與派生在C++中也是經常使用的,比如設計一個箱子類可以用以下代碼實現:
Class CBox{ public: int volume(){ return height*width*length; } void display(){ cout<<height<<endl; cout<<width<<endl; cout<<length<<endl; } private: int height,width,length; };
但假設現在有一批箱子比較特殊,有不同的顏色,且重量也都不一樣,此時可以重新聲明一個新的箱子的類,如下所示:
Class CBox_new{ public: int volume(){ return height*width*length; } void display(){ cout<<height<<endl; cout<<width<<endl; cout<<length<<endl; cout<<color<<endl; cout<<weight<<endl; } private: int height,width,length; int color,weight; };
可以看到上面的程序中有相當一部分是原來就已經有的,其實可以利用原來的CBox類作為基礎,再加上新的內容即可,如下所示:
Class CBox_new::public CBox{ // 類CBox_new繼承于類CBox public: void display1(){ // 新增加的成員函數 cout<<color<<endl; cout<<weight<<endl; } private: int color,weight; // 新增加的成員變量 };
這里,Box_new就是一個派生類。可見,聲明派生類的一般形式為:
class 派生類名:[繼承方式] 基類名{ 派生類新增加的成員 };
其中的繼承方式包括public(公用的)、private(私有的)和protected(受保護的),此項是可選的,如果不寫此項,則默認為private(私有的)。
派生類里有兩大部分內容:從基類繼承而來的和在聲明派生類時增加的部分。派生類中接受了基類的全部內容,這樣可能出現有些基類的成員,在派生類中是用不到的,但是也必須繼承過來的情況。這就會造成數據的冗余,尤其多次派生后,會在許多派生類對象中存在大量無用的數據,不僅浪費了大量的空間,而且在對象的建立、賦值、復制和參數的傳遞中,花費許多無謂的時間,從而降低了效率。因此,實際開發中要根據派生類的需要慎重選擇基類,使冗余量最小。
2.派生類的訪問屬性
(1)派生類中包含基類成員和派生類自己增加的成員,就產生了這兩部分成員的關系和訪問屬性的問題。在建立派生類的時候,并不是簡單地把基類的私有成員直接作為派生類的私有成員,把基類的公用成員直接作為派生類的公用成員;實際上,對基類成員和派生類自己增加的成員是按不同的原則處理的。具體來說,在討論訪問屬性時,要考慮以下幾種情況。
1)基類的成員函數只能訪問基類的成員,而不能訪問派生類的成員。
2)派生類的成員函數可以訪問基類的成員,具體見后面詳細描述;派生類的成員函數也可以訪問派生類成員。
3)在派生類外可以訪問基類的成員,具體見后面詳細描述;在派生類外也可以訪問派生類的公用成員,而不能訪問派生類的私有成員。
(2)派生類的成員函數訪問基類的成員和在派生類外訪問基類的成員涉及如何確定基類的成員在派生類中的訪問屬性的問題,不僅要考慮對基類成員所聲明的訪問屬性,還要考慮派生類所聲明的對基類的繼承方式,根據這兩個因素共同決定基類成員在派生類中的訪問屬性。在派生類中,對基類的繼承方式可以有public(公用的)、private(私有的)和protected(保護的)3種。不同的繼承方式決定了基類成員在派生類中的訪問屬性。簡單地說可以總結為以下幾點。
1)公用繼承(public inheritance):基類的公用成員和保護成員在派生類中保持原有訪問屬性,其私有成員仍為基類私有。
2)私有繼承(private inheritance):基類的公用成員和保護成員在派生類中成了私有成員,其私有成員仍為基類私有。
3)受保護的繼承(protected inheritance):基類的公用成員和保護成員在派生類中成了保護成員,其私有成員仍為基類私有。保護成員的意思是,不能被外界引用,但可以被派生類的成員引用。
在多級派生的情況下,各成員的訪問屬性仍按以上原則確定。假設類A為基類,類B是類A的派生類,類C是類B的派生類,則類C也是類A的派生類;類B稱為類A的直接派生類,類C稱為類A的間接派生類;類A是類B的直接基類,是類C的間接基類。派生關系如圖2-3所示。

圖2-3 類A、類B和類C的派生關系
如果聲明了以下的類:
class A{ public: int var_A_pub; protected: void func_A_pro(); int var_A_pro; private: int var_A_pri; }; class B:public A{ public: void func_B_pub(); protect: void func_B_pro(); private: int var_B_pri; }; class C:protected B{ public: void func_C_pub(); private: int var_C_pri; };
則類A是類B的公用基類,類B是類C的保護基類。各成員在不同類中的訪問屬性如表2-1所示。
表2-1 派生的各個成員的訪問屬性

通過以上分析,可以看到:無論哪一種繼承方式,在派生類中是不能訪問基類的私有成員的,私有成員只能被本類的成員函數所訪問,畢竟派生類與基類不是同一個類。如果在多級派生時都采用公用繼承方式,那么直到最后一級派生類都能訪問基類的公用成員和保護成員。如果采用私有繼承方式,經過若干次派生之后,基類的所有成員就會變成不可訪問的了。如果采用保護繼承方式,在派生類外是無法訪問派生類中的任何成員的;而且經過多次派生后,人們很難清楚地記住哪些成員可以訪問,哪些成員不能訪問,很容易出錯。因此,在實際中,常用的是公用繼承。
3.派生類的構造函數與析構函數
派生類的數據成員由所有基類的數據成員與派生類新增的數據成員共同組成,如果派生類新增成員中包括其他類的對象(子對象),派生類的數據成員中實際上還間接地包括了這些對象的數據成員。因此,構造派生類的對象時,必須對基類數據成員、新增數據成員和成員對象的數據成員進行初始化。派生類的構造函數必須要以合適的初值作為參數,隱含調用基類和新增對象成員的構造函數,來初始化它們各自的數據成員,然后再加入新的語句對新增普通數據成員進行初始化。
派生類構造函數的一般格式如下:
<派生類名>::<派生類名>(<參數表>) : <基類名1>(<參數表1>), ……, <基類名n>(<參數表n>), <子對象名1>(<參數表n+1>), ……, <子對象名m>(<參數表n+m>) { <派生類構造函數體> // 派生類新增成員的初始化 }
對派生類的構造函數有以下幾點說明:
(1)對基類成員和子對象成員的初始化必須在成員初始化列表中進行,新增成員的初始化既可以在成員初始化列表中進行,也可以在構造函數體中進行。
(2)派生類構造函數必須對這3類成員進行初始化,其執行順序是這樣的:①先調用基類構造函數;②再調用子對象的構造函數;③最后調用派生類的構造函數體。
(3)當派生類有多個基類時,處于同一層次的各個基類的構造函數的調用順序取決于定義派生類時聲明的順序(自左向右),而與在派生類構造函數的成員初始化列表中給出的順序無關。
(4)如果派生類的基類也是一個派生類,則每個派生類只需負責其直接基類的構造,依次上溯。
(5)當派生類中有多個子對象時,各個子對象構造函數的調用順序也取決于在派生類中定義的順序(自前至后),而與在派生類構造函數的成員初始化列表中給出的順序無關。
(6)派生類構造函數提供了將參數傳遞給基類構造函數的途徑,以保證在基類進行初始化時能夠獲得必要的數據。因此,如果基類的構造函數定義了一個或多個參數時,派生類必須定義構造函數。
(7)如果基類中定義了默認構造函數或根本沒有定義任何一個構造函數(此時由編譯器自動生成默認構造函數)時,在派生類構造函數的定義中可以省略對基類構造函數的調用,即省略"<基類名>(<參數表>)"這個語句。
(8)子對象的情況與基類相同。
(9)當所有的基類和子對象的構造函數都可以省略時,可以省略派生類構造函數的成員初始化列表。
(10)如果所有的基類和子對象構造函數都不需要參數,派生類也不需要參數時,派生類構造函數可以不定義。派生類構造函數的使用可以參考例2.26的程序。
【例2.26】 派生類構造函數的使用舉例。
#include<iostream> #include<string> using namespace std; class CStudent{ // 聲明基類Student public: CStudent(int n,string nam,char s){ // 基類構造函數 num=n; name=nam; sex=s; } ~CStudent(){} // 基類析構函數 protected: // 保護部分 int num; string name; char sex ; }; class CStudent1: public CStudent{ // 聲明派生類Student1 public : // 派生類的公用部分 CStudent1 (int n,string nam,char s,int a,string ad): CStudent (n,nam,s){ // 派生類構造函數 age=a; // 在函數體中只對派生類新增的數據成員初始化 addr=ad; } void show(){ cout<<"num: "<<num<<endl; cout<<"name: "<<name<<endl; cout<<"sex: "<<sex<<endl; cout<<"age: "<<age<<endl; cout<<"address: "<<addr<<endl<<endl; } ~CStudent1(){ } // 派生類析構函數 private : // 派生類的私有部分 int age; string addr; }; int main(){ CStudent1 stud1(10010,"Wang-li",'f',19,"115 Beijing Road,Shanghai"); CStudent1 stud2(10011,"Zhang-fun",'m',21,"213 Shanghai Road,Beijing"); stud1.show(); // 輸出第一個學生的數據 stud2.show(); // 輸出第二個學生的數據 return 0; }
程序的執行結果是:
num: 10010 name: Wang-li sex: f age: 19 address: 115 Beijing Road,Shanghai num: 10011 name: Zhang-fun sex: m age: 21 address: 213 Shanghai Road,Beijing
例2.26中定義了類CStudent、類CStudent1,其中類CStudent1繼承了類CStudent。類CStudent1比基類新增了兩個數據成員。基類CStudent有自己的帶參構造函數,而派生類的構造函數,則只須對派生類新增的數據成員初始化,不過需要把基類的參數也給帶進去。
析構函數的作用是在對象撤銷之前,進行必要的清理工作。當對象被刪除時,系統會自動調用析構函數。
析構函數比構造函數簡單,沒有類型,也沒有參數。在派生時,派生類是不能繼承基類的析構函數的,也需要通過派生類的析構函數去調用基類的析構函數。在派生類中可以根據需要定義自己的析構函數,用來對派生類中所增加的成員進行清理工作;基類的清理工作仍然由基類的析構函數負責。在執行派生類的析構函數時,系統會自動調用基類的析構函數和子對象的析構函數,對基類和子對象進行清理。
4.派生類的構造函數與析構函數的調用順序
前面已經提到,構造函數和析構函數的調用順序是先構造的后析構,后構造的先析構。那么基類和派生類中的構造函數和析構函數的調用順序是否也是如此呢?
構造函數的調用順序規則如下所述。
1)基類構造函數。如果有多個基類,則構造函數的調用順序是某類在類派生表中出現的順序,而不是它們在成員初始化表中的順序。
2)成員類對象構造函數。如果有多個成員類對象,則構造函數的調用順序是對象在類中被聲明的順序,而不是它們出現在成員初始化表中的順序。
3)派生類構造函數。
而析構函數的調用順序與構造函數的調用順序正好相反,將上面3點內容中的順序反過來用就可以了,即:首先調用派生類的析構函數;其次再調用成員類對象的析構函數;最后調用基類的析構函數。析構函數在下面3種情況時被調用。
1)對象生命周期結束被銷毀時(一般類成員的指針變量與引用都不自動調用析構函數)。
2)delete指向對象的指針時,或delete指向對象的基類類型指針,而其基類虛構函數是虛函數時。
3)對象i是對象o的成員,o的析構函數被調用時,對象i的析構函數也被調用。
下面用例2.27來說明構造函數的調用順序。
【例2.27】 構造函數的調用順序。
#include<iostream> using namespace std; class CBase{ public: CBase (){ std::cout<<"CBase::CBase()"<<std::endl; } ~ CBase (){ std::cout<<"CBase::~CBase()"<<std::endl; } }; class CBase1:public CBase { public: CBase1 (){ std::cout<<"CBase::Base1()"<<std::endl; } ~ CBase1 (){ std::cout<<"CBase::~Base1()"<<std::endl; } }; class CDerive{ public: CDerive (){ std::cout<<"CDerive::CDerive()"<<std::endl; } ~ CDerive (){ std::cout<<"CDerive::~CDerive()"<<std::endl; } }; class CDerive1:public CBase1{ private: CDerive m_derive; public: CDerive1(){ std::cout<<"CDerive1::CDerive1()"<<std::endl; } ~CDerive1(){ std::cout<<"CDerive1::~CDerive1()"<<std::endl; } }; int main(){ CDerive1 derive; return 0; }
程序的執行結果是:
CBase::CBase() CBase::Base1() CDerive::CDerive() CDerive1::CDerive1() CDerive1::~CDerive1() CDerive::~CDerive() CBase::~Base1() CBase::~CBase()
例2.27中聲明了4個類,CBase1繼承于CBase、CDerive1繼承于CBase1、CDerive1中有CDerive的成員變量,最后定義一個CDerive1對象,用來確定各種基類、派生類的構造函數和析構函數的執行順序。執行結果與上文中描述的調用順序相符。
總的來說,構造函數的調用順序是:①如果存在基類,那么先調用基類的構造函數,如果基類的構造函數中仍然存在基類,那么程序會繼續進行向上查找,直到找到它最早的基類進行初始化(如上例中類Derive1,繼承于類Base與Base1);②如果所調用的類中定義的時候存在對象被聲明,那么在基類的構造函數調用完成以后,再調用對象的構造函數(如上例在類Derive1中聲明的對象Derive m_derive);③調用派生類的構造函數(如上例最后調用的是Derive1類的構造函數)。