一起来学C++:C++中的代码重用
目錄
14.1 包含對(duì)象成員的類
14.1.1 valarray類簡(jiǎn)介
14.1.2 Student類的設(shè)計(jì)
14.1.3 Student類示例
1.初始化被包含的對(duì)象
2.使用被包含對(duì)象的接口
3.使用新的Student類
14.2 私有繼承
14.2.1 Student類示例(新版本)
1.初始化基類組件
2.訪問(wèn)基類的方法
3.訪問(wèn)基類對(duì)象
4.訪問(wèn)基類的友元函數(shù)
5.使用修改后的Student類
14.2.2 使用包含還是私有繼承
14.2.3 保護(hù)繼承
14.2.4 使用using重新定義訪問(wèn)權(quán)限
14.3 多重繼承
14.3.1 有多少Worker
1.虛基類
2.新的構(gòu)造函數(shù)規(guī)則
本章內(nèi)容包括:
- has-a關(guān)系;
- 包含對(duì)象成員的類;
- 模板類valarray;
- 私有和保護(hù)繼承;
- 多重繼承;
- 虛基類;
- 創(chuàng)建類模板;
- 使用類模板;
- 模板的具體化。
C++的一個(gè)主要目標(biāo)是促進(jìn)代碼重用。公有繼承是實(shí)現(xiàn)這種目標(biāo)的機(jī)制之一,但并不是唯一的機(jī)制。本章將介紹其他方法,其中之一是使用這樣的類成員:本身是另一個(gè)類的對(duì)象。這種方法稱為包含(containment)、組合(composition)或?qū)哟位?#xff08;layering)。另一種方法是使用私有或保護(hù)繼承。通常,包含、私有繼承和保護(hù)繼承用于實(shí)現(xiàn)has-a關(guān)系,即新的類將包含另一個(gè)類的對(duì)象。例如,HomeTheater類可能包含一個(gè)BluRayPlayer對(duì)象。多重繼承使得能夠使用兩個(gè)或更多的基類派生出新的類,將基類的功能組合在一起。
第10章介紹了函數(shù)模板,本章將介紹類模板——另一種重用代碼的方法。類模板使我們能夠使用通用術(shù)語(yǔ)定義類,然后使用模板來(lái)創(chuàng)建針對(duì)特定類型定義的特殊類。例如,可以定義一個(gè)通用的棧模板,然后使用該模板創(chuàng)建一個(gè)用于表示int值棧的類和一個(gè)用于表示double值棧的類,甚至可以創(chuàng)建一個(gè)這樣的類,即用于表示由棧組成的棧。
14.1 包含對(duì)象成員的類
首先介紹包含對(duì)象成員的類。有一些類(如string類和第16章將介紹的標(biāo)準(zhǔn)C++類模板)為表示類中的組件提供了方便的途徑。下面來(lái)看一個(gè)具體的例子。
學(xué)生是什么?入學(xué)者?參加研究的人?殘酷現(xiàn)實(shí)社會(huì)的避難者?有姓名和一系列考試分?jǐn)?shù)的人?顯然,最后一個(gè)定義完全沒(méi)有表示出人的特征,但非常適合于簡(jiǎn)單的計(jì)算機(jī)表示。因此,讓我們根據(jù)該定義來(lái)開(kāi)發(fā)Student類。
將學(xué)生簡(jiǎn)化成姓名和一組考試分?jǐn)?shù)后,可以使用一個(gè)包含兩個(gè)成員的類來(lái)表示它:一個(gè)成員用于表示姓名,另一個(gè)成員用于表示分?jǐn)?shù)。對(duì)于姓名,可以使用字符數(shù)組來(lái)表示,但這將限制姓名的長(zhǎng)度。當(dāng)然,也可以使用char指針和動(dòng)態(tài)內(nèi)存分配,但正如第12章指出的,這將要求提供大量的支持代碼。一種更好的方法是,使用一個(gè)由他人開(kāi)發(fā)好的類的對(duì)象來(lái)表示。例如,可以使用一個(gè)String類(參見(jiàn)第12章)或標(biāo)準(zhǔn)C++ string類的對(duì)象來(lái)表示姓名。較簡(jiǎn)單的選擇是使用string類,因?yàn)镃++庫(kù)提供了這個(gè)類的所有實(shí)現(xiàn)代碼,且其實(shí)現(xiàn)更完美。要使用String類,您必須在項(xiàng)目中包含實(shí)現(xiàn)文件string1.cpp。
對(duì)于考試分?jǐn)?shù),存在類似的選擇。可以使用一個(gè)定長(zhǎng)數(shù)組,這限制了數(shù)組的長(zhǎng)度;可以使用動(dòng)態(tài)內(nèi)存分配并提供大量的支持代碼;也可以設(shè)計(jì)一個(gè)使用動(dòng)態(tài)內(nèi)存分配的類來(lái)表示該數(shù)組;還可以在標(biāo)準(zhǔn)C++庫(kù)中查找一個(gè)能夠表示這種數(shù)據(jù)的類。
自己開(kāi)發(fā)這樣的類一點(diǎn)問(wèn)題也沒(méi)有。開(kāi)發(fā)簡(jiǎn)單的版本并不那么難,因?yàn)閐ouble數(shù)組與char數(shù)組有很多相似之處,因此可以根據(jù)String類來(lái)設(shè)計(jì)表示double數(shù)組的類。事實(shí)上,本書以前的版本就這樣做過(guò)。
當(dāng)然,如果C++庫(kù)提供了合適的類,實(shí)現(xiàn)起來(lái)將更簡(jiǎn)單。C++庫(kù)確實(shí)提供了一個(gè)這樣的類,它就是valarray。
14.1.1 valarray類簡(jiǎn)介
valarray類是由頭文件valarray支持的。顧名思義,這個(gè)類用于處理數(shù)值(或具有類似特性的類),它支持諸如將數(shù)組中所有元素的值相加以及在數(shù)組中找出最大和最小的值等操作。valarray被定義為一個(gè)模板類,以便能夠處理不同的數(shù)據(jù)類型。本章后面將介紹如何定義模板類,但就現(xiàn)在而言,您只需知道如何使用模板類即可。
模板特性意味著聲明對(duì)象時(shí),必須指定具體的數(shù)據(jù)類型。因此,使用valarray類來(lái)聲明一個(gè)對(duì)象時(shí),需要在標(biāo)識(shí)符valarray后面加上一對(duì)尖括號(hào),并在其中包含所需的數(shù)據(jù)類型:
valarray<int> q_values; // an array of int valarray<double> weights; // an array of double第4章介紹vector和array類時(shí),您見(jiàn)過(guò)這種語(yǔ)法,它非常簡(jiǎn)單。這些類也可用于存儲(chǔ)數(shù)字,但它們提供的算術(shù)支持沒(méi)有valarray多。
這是您需要學(xué)習(xí)的唯一新語(yǔ)法,它非常簡(jiǎn)單。
類特性意味著要使用valarray對(duì)象,需要了解這個(gè)類的構(gòu)造函數(shù)和其他類方法。下面是幾個(gè)使用其構(gòu)造函數(shù)的例子:
double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3}; valarray<double> v1; // an array of double, size 0 valarray<int> v2(8); // an array of 8 int elements valarray<int> v3(10,8); // an array of 8 int elements,// each set to 10 valarray<double> v4(gpa, 4); // an array of 4 elements// initialized to the first 4 elements of gpa從中可知,可以創(chuàng)建長(zhǎng)度為零的空數(shù)組、指定長(zhǎng)度的空數(shù)組、所有元素度被初始化為指定值的數(shù)組、用常規(guī)數(shù)組中的值進(jìn)行初始化的數(shù)組。在C++11中,也可使用初始化列表:
valarray<int> v5 = {20, 32, 17, 9}; // C++11下面是這個(gè)類的一些方法。
- operator?:讓您能夠訪問(wèn)各個(gè)元素。
- size():返回包含的元素?cái)?shù)。
- sum():返回所有元素的總和。
- max():返回最大的元素。
- min():返回最小的元素。
還有很多其他的方法,其中的一些將在第16章介紹;但就這個(gè)例子而言,上述方法足夠了。
14.1.2 Student類的設(shè)計(jì)
至此,已經(jīng)確定了Student類的設(shè)計(jì)計(jì)劃:使用一個(gè)string對(duì)象來(lái)表示姓名,使用一個(gè)valarray<double>來(lái)表示考試分?jǐn)?shù)。那么如何設(shè)計(jì)呢?您可能想以公有的方式從這兩個(gè)類派生出Student類,這將是多重公有繼承,C++允許這樣做,但在這里并不合適,因?yàn)閷W(xué)生與這些類之間的關(guān)系不是is-a模型。學(xué)生不是姓名,也不是一組考試成績(jī)。這里的關(guān)系是has-a,學(xué)生有姓名,也有一組考試分?jǐn)?shù)。通常,用于建立has-a關(guān)系的C++技術(shù)是組合(包含),即創(chuàng)建一個(gè)包含其他類對(duì)象的類。例如,可以將Student類聲明為如下所示:
class Student { private:string name; // use a string object for namevalarray<double> scores; // use a valarray<double> object for scores... };同樣,上述類將數(shù)據(jù)成員聲明為私有的。這意味著Student類的成員函數(shù)可以使用string和valarray<double>類的公有接口來(lái)訪問(wèn)和修改name和scores對(duì)象,但在類的外面不能這樣做,而只能通過(guò)Student類的公有接口訪問(wèn)name和score(請(qǐng)參見(jiàn)圖14.1)。對(duì)于這種情況,通常被描述為Student類獲得了其成員對(duì)象的實(shí)現(xiàn),但沒(méi)有繼承接口。例如,Student對(duì)象使用string的實(shí)現(xiàn),而不是char * name或char name [26]實(shí)現(xiàn)來(lái)保存姓名。但Student對(duì)象并不是天生就有使用函數(shù)string operator+=()的能力。
圖14.1 對(duì)象中的對(duì)象:包含
?
接口和實(shí)現(xiàn)
使用公有繼承時(shí),類可以繼承接口,可能還有實(shí)現(xiàn)(基類的純虛函數(shù)提供接口,但不提供實(shí)現(xiàn))。獲得接口是is-a關(guān)系的組成部分。而使用組合,類可以獲得實(shí)現(xiàn),但不能獲得接口。不繼承接口是has-a關(guān)系的組成部分。
對(duì)于has-a關(guān)系來(lái)說(shuō),類對(duì)象不能自動(dòng)獲得被包含對(duì)象的接口是一件好事。例如,string類將+運(yùn)算符重載為將兩個(gè)字符串連接起來(lái);但從概念上說(shuō),將兩個(gè)Student對(duì)象串接起來(lái)是沒(méi)有意義的。這也是這里不使用公有繼承的原因之一。另一方面,被包含的類的接口部分對(duì)新類來(lái)說(shuō)可能是有意義的。例如,可能希望使用string接口中的operator<()方法將Student對(duì)象按姓名進(jìn)行排序,為此可以定義Student::operator<()成員函數(shù),它在內(nèi)部使用函數(shù)string::operator<()。下面介紹一些細(xì)節(jié)。
14.1.3 Student類示例
現(xiàn)在需要提供Student類的定義,當(dāng)然它應(yīng)包含構(gòu)造函數(shù)以及一些用作Student類接口的方法。程序清單14.1是Student類的定義,其中所有構(gòu)造函數(shù)都被定義為內(nèi)聯(lián)的;它還提供了一些用于輸入和輸出的友元函數(shù)。
程序清單14.1 studentc.h
// studentc.h -- defining a Student class using containment #ifndef STUDENTC_H_ #define STUDENTC_H_#include <iostream> #include <string> #include <valarray> class Student { private:typedef std::valarray<double> ArrayDb;std::string name; // contained objectArrayDb scores; // contained object// private method for scores outputstd::ostream & arr_out(std::ostream & os) const; public:Student() : name("Null Student"), scores() {}explicit Student(const std::string & s): name(s), scores() {}explicit Student(int n) : name("Nully"), scores(n) {}Student(const std::string & s, int n): name(s), scores(n) {}Student(const std::string & s, const ArrayDb & a): name(s), scores(a) {}Student(const char * str, const double * pd, int n): name(str), scores(pd, n) {}~Student() {}double Average() const;const std::string & Name() const;double & operator[](int i);double operator[](int i) const; // friends// inputfriend std::istream & operator>>(std::istream & is,Student & stu); // 1 wordfriend std::istream & getline(std::istream & is,Student & stu); // 1 line// outputfriend std::ostream & operator<<(std::ostream & os,const Student & stu); };#endif為簡(jiǎn)化表示,Student類的定義中包含下述typedef:
typedef std::valarray<double> ArrayDb;這樣,在以后的代碼中便可以使用表示ArrayDb,而不是std::valarray<double>,因此類方法和友元函數(shù)可以使用ArrayDb類型。將該typedef放在類定義的私有部分意味著可以在Student類的實(shí)現(xiàn)中使用它,但在Student類外面不能使用。
請(qǐng)注意關(guān)鍵字explicit的用法:
explicit Student(const std::string & s): name(s), scores() {} explicit Student(int n) : name("Nully"), scores(n) {}本書前面說(shuō)過(guò),可以用一個(gè)參數(shù)調(diào)用的構(gòu)造函數(shù)將用作從參數(shù)類型到類類型的隱式轉(zhuǎn)換函數(shù);但這通常不是好主意。在上述第二個(gè)構(gòu)造函數(shù)中,第一個(gè)參數(shù)表示數(shù)組的元素個(gè)數(shù),而不是數(shù)組中的值,因此將一個(gè)構(gòu)造函數(shù)用作int到Student的轉(zhuǎn)換函數(shù)是沒(méi)有意義的,所以使用explicit關(guān)閉隱式轉(zhuǎn)換。如果省略該關(guān)鍵字,則可以編寫如下所示的代碼:
Student doh("Homer", 10); // store "Homer", create array of 10 elements doh = 5; // reset name to "Nully", reset to empty array of 5 elements在這里,馬虎的程序員鍵入了doh而不是doh[0]。如果構(gòu)造函數(shù)省略了explicit,則將使用構(gòu)造函數(shù)調(diào)用Student(5)將5轉(zhuǎn)換為一個(gè)臨時(shí)Student對(duì)象,并使用“Nully”來(lái)設(shè)置成員name的值。因此賦值操作將使用臨時(shí)對(duì)象替換原來(lái)的doh值。使用了explicit后,編譯器將認(rèn)為上述賦值運(yùn)算符是錯(cuò)誤的。
C++和約束
C++包含讓程序員能夠限制程序結(jié)構(gòu)的特性——使用explicit防止單參數(shù)構(gòu)造函數(shù)的隱式轉(zhuǎn)換,使用const限制方法修改數(shù)據(jù),等等。這樣做的根本原因是:在編譯階段出現(xiàn)錯(cuò)誤優(yōu)于在運(yùn)行階段出現(xiàn)錯(cuò)誤。
1.初始化被包含的對(duì)象
構(gòu)造函數(shù)全都使用您熟悉的成員初始化列表語(yǔ)法來(lái)初始化name和score成員對(duì)象。在前面的一些例子中,構(gòu)造函數(shù)用這種語(yǔ)法來(lái)初始化內(nèi)置類型的成員:
Queue::Queue(int qs) : qsize(qs) {...} // initialize qsize to qs上述代碼在成員初始化列表中使用的是數(shù)據(jù)成員的名稱(qsize)。另外,前面介紹的示例中的構(gòu)造函數(shù)還使用成員初始化列表初始化派生對(duì)象的基類部分:
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs) {...}對(duì)于繼承的對(duì)象,構(gòu)造函數(shù)在成員初始化列表中使用類名來(lái)調(diào)用特定的基類構(gòu)造函數(shù)。對(duì)于成員對(duì)象,構(gòu)造函數(shù)則使用成員名。例如,請(qǐng)看程序清單14.1的最后一個(gè)構(gòu)造函數(shù):
Student(const char * str, const double * pd, int n): name(str), scores(pd, n) {}因?yàn)樵摌?gòu)造函數(shù)初始化的是成員對(duì)象,而不是繼承的對(duì)象,所以在初始化列表中使用的是成員名,而不是類名。初始化列表中的每一項(xiàng)都調(diào)用與之匹配的構(gòu)造函數(shù),即name(str)調(diào)用構(gòu)造函數(shù)string(const char *),scores(pd, n)調(diào)用構(gòu)造函數(shù)ArrayDb(const double *, int)。
如果不使用初始化列表語(yǔ)法,情況將如何呢?C++要求在構(gòu)建對(duì)象的其他部分之前,先構(gòu)建繼承對(duì)象的所有成員對(duì)象。因此,如果省略初始化列表,C++將使用成員對(duì)象所屬類的默認(rèn)構(gòu)造函數(shù)。
初始化順序
當(dāng)初始化列表包含多個(gè)項(xiàng)目時(shí),這些項(xiàng)目被初始化的順序?yàn)樗鼈儽宦暶鞯捻樞?#xff0c;而不是它們?cè)诔跏蓟斜碇械捻樞颉@?#xff0c;假設(shè)Student構(gòu)造函數(shù)如下:
Student(const char * str, const double * pd, int n): scores(pd, n), name(str) {}則name成員仍將首先被初始化,因?yàn)樵陬惗x中它首先被聲明。對(duì)于這個(gè)例子來(lái)說(shuō),初始化順序并不重要,但如果代碼使用一個(gè)成員的值作為另一個(gè)成員的初始化表達(dá)式的一部分時(shí),初始化順序就非常重要了。
2.使用被包含對(duì)象的接口
被包含對(duì)象的接口不是公有的,但可以在類方法中使用它。例如,下面的代碼說(shuō)明了如何定義一個(gè)返回學(xué)生平均分?jǐn)?shù)的函數(shù):
double Student::Average() const {if (scores.size() > 0)return scores.sum()/scores.size();elsereturn 0; }上述代碼定義了可由Student對(duì)象調(diào)用的方法,該方法內(nèi)部使用了valarray的方法size()和sum()。這是因?yàn)閟cores是一個(gè)valarray對(duì)象,所以它可以調(diào)用valarray類的成員函數(shù)。總之,Student對(duì)象調(diào)用Student的方法,而后者使用被包含的valarray對(duì)象來(lái)調(diào)用valarray類的方法。
同樣,可以定義一個(gè)使用string版本的<<運(yùn)算符的友元函數(shù):
// use string version of operator<<() ostream & operator<<(ostream & os, const Student & stu) {os << "Scores for " << stu.name << ":\n";... }因?yàn)閟tu.name是一個(gè)string對(duì)象,所以它將調(diào)用函數(shù)operatot<<(ostream &, const string &),該函數(shù)位于string類中。注意,operator<<(ostream & os, const Student & stu)必須是Student類的友元函數(shù),這樣才能訪問(wèn)name成員。另一種方法是,在該函數(shù)中使用公有方法Name(),而不是私有數(shù)據(jù)成員name。
同樣,該函數(shù)也可以使用valarray的<<實(shí)現(xiàn)來(lái)進(jìn)行輸出,不幸的是沒(méi)有這樣的實(shí)現(xiàn);因此,Student類定義了一個(gè)私有輔助方法來(lái)處理這種任務(wù):
// private method ostream & Student::arr_out(ostream & os) const {int i;int lim = scores.size();if (lim > 0){for (i = 0; i < lim; i++){os << scores[i] << " ";if (i % 5 == 4)os << endl;}if (i % 5 != 0)os << endl;}elseos << " empty array ";return os; }通過(guò)使用這樣的輔助方法,可以將零亂的細(xì)節(jié)放在一個(gè)地方,使得友元函數(shù)的編碼更為整潔:
// use string version of operator<<() ostream & operator<<(ostream & os, const Student & stu) {os << "Scores for " << stu.name << ":\n";stu.arr_out(os); // use private method for scoresreturn os; }輔助函數(shù)也可用作其他用戶級(jí)輸出函數(shù)的構(gòu)建塊——如果您選擇提供這樣的函數(shù)的話。
程序清單14.2是Student類的類方法文件,其中包含了讓您能夠使用[ ]運(yùn)算符來(lái)訪問(wèn)Student對(duì)象中各項(xiàng)成績(jī)的方法。
程序清單14.2 student.cpp
// studentc.cpp -- Student class using containment #include "studentc.h" using std::ostream; using std::endl; using std::istream; using std::string; //public methods double Student::Average() const {if (scores.size() > 0)return scores.sum()/scores.size();elsereturn 0; }const string & Student::Name() const {return name; }double & Student::operator[](int i) {return scores[i]; // use valarray<double>::operator[]() }double Student::operator[](int i) const {return scores[i]; }// private method ostream & Student::arr_out(ostream & os) const {int i;int lim = scores.size();if (lim > 0){for (i = 0; i < lim; i++){os << scores[i] << " ";if (i % 5 == 4)os << endl;}if (i % 5 != 0)os << endl;}elseos << " empty array ";return os; }// friends // use string version of operator>>() istream & operator>>(istream & is, Student & stu) {is >> stu.name;return is; }// use string friend getline(ostream &, const string &) istream & getline(istream & is, Student & stu) {getline(is, stu.name);return is; }// use string version of operator<<() ostream & operator<<(ostream & os, const Student & stu) {os << "Scores for " << stu.name << ":\n";stu.arr_out(os); // use private method for scoresreturn os; }除私有輔助方法外,程序清單14.2并沒(méi)有新增多少代碼。使用包含讓您能夠充分利用已有的代碼。
3.使用新的Student類
下面編寫一個(gè)小程序來(lái)測(cè)試這個(gè)新的Student類。出于簡(jiǎn)化的目的,該程序?qū)⑹褂靡粋€(gè)只包含3個(gè)Student對(duì)象的數(shù)組,其中每個(gè)對(duì)象保存5個(gè)考試成績(jī)。另外還將使用一個(gè)不復(fù)雜的輸入循環(huán),該循環(huán)不驗(yàn)證輸入,也不讓用戶中途退出。程序清單14.3列出了該測(cè)試程序,請(qǐng)務(wù)必將該程序與Student.cpp一起進(jìn)行編譯。
程序清單14.3 use_stuc.cpp
// use_stuc.cpp -- using a composite class // compile with studentc.cpp #include <iostream> #include "studentc.h" using std::cin; using std::cout; using std::endl;void set(Student & sa, int n); const int pupils = 3; const int quizzes = 5;int main() {Student ada[pupils] ={Student(quizzes), Student(quizzes), Student(quizzes)};int i;for (i = 0; i < pupils; ++i)set(ada[i], quizzes);cout << "\nStudent List:\n";for (i = 0; i < pupils; ++i)cout << ada[i].Name() << endl;cout << "\nResults:";for (i = 0; i < pupils; ++i){cout << endl << ada[i];cout << "average: " << ada[i].Average() << endl;}cout << "Done.\n";return 0; }void set(Student & sa, int n) {cout << "Please enter the student's name: ";getline(cin, sa);cout << "Please enter " << n << " quiz scores:\n";for (int i = 0; i < n; i++)cin >> sa[i];while (cin.get() != '\n')continue; }下面是程序清單14.1~程序清單14.3組成的程序的運(yùn)行情況:
Please enter the student's name: Gil Bayts Please enter 5 quiz scores: 92 94 96 93 95 Please enter the student's name: Pat Roone Please enter 5 quiz scores: 83 89 72 78 95 Please enter the student's name: Fleur O’Day Please enter 5 quiz scores: 92 89 96 74 64 Student List: Gil Bayts Pat Roone Fleur O'DayResults: Scores for Gil Bayts: 92 94 96 93 95 average: 94Scores for Pat Roone: 83 89 72 78 95 average: 83.4Scores for Fleur O'Day: 92 89 96 74 64 average: 83 Done.14.2 私有繼承
C++還有另一種實(shí)現(xiàn)has-a關(guān)系的途徑——私有繼承。使用私有繼承,基類的公有成員和保護(hù)成員都將成為派生類的私有成員。這意味著基類方法將不會(huì)成為派生對(duì)象公有接口的一部分,但可以在派生類的成員函數(shù)中使用它們。
下面更深入地探討接口問(wèn)題。使用公有繼承,基類的公有方法將成為派生類的公有方法。總之,派生類將繼承基類的接口;這是is-a關(guān)系的一部分。使用私有繼承,基類的公有方法將成為派生類的私有方法。總之,派生類不繼承基類的接口。正如從被包含對(duì)象中看到的,這種不完全繼承是has-a關(guān)系的一部分。
使用私有繼承,類將繼承實(shí)現(xiàn)。例如,如果從String類派生出Student類,后者將有一個(gè)String類組件,可用于保存字符串。另外,Student方法可以使用String方法來(lái)訪問(wèn)String組件。
包含將對(duì)象作為一個(gè)命名的成員對(duì)象添加到類中,而私有繼承將對(duì)象作為一個(gè)未被命名的繼承對(duì)象添加到類中。我們將使用術(shù)語(yǔ)子對(duì)象(subobject)來(lái)表示通過(guò)繼承或包含添加的對(duì)象。
因此私有繼承提供的特性與包含相同:獲得實(shí)現(xiàn),但不獲得接口。所以,私有繼承也可以用來(lái)實(shí)現(xiàn)has-a關(guān)系。接下來(lái)介紹如何使用私有繼承來(lái)重新設(shè)計(jì)Student類。
14.2.1 Student類示例(新版本)
要進(jìn)行私有繼承,請(qǐng)使用關(guān)鍵字private而不是public來(lái)定義類(實(shí)際上,private是默認(rèn)值,因此省略訪問(wèn)限定符也將導(dǎo)致私有繼承)。Student類應(yīng)從兩個(gè)類派生而來(lái),因此聲明將列出這兩個(gè)類:
class Student : private std::string, private std::valarray<double> { public:... };使用多個(gè)基類的繼承被稱為多重繼承(multiple inheritance,MI)。通常,MI尤其是公有MI將導(dǎo)致一些問(wèn)題,必須使用額外的語(yǔ)法規(guī)則來(lái)解決它們,這將在本章后面介紹。但在這個(gè)示例中,MI不會(huì)導(dǎo)致問(wèn)題。
新的Student類不需要私有數(shù)據(jù),因?yàn)閮蓚€(gè)基類已經(jīng)提供了所需的所有數(shù)據(jù)成員。包含版本提供了兩個(gè)被顯式命名的對(duì)象成員,而私有繼承提供了兩個(gè)無(wú)名稱的子對(duì)象成員。這是這兩種方法的第一個(gè)主要區(qū)別。
1.初始化基類組件
隱式地繼承組件而不是成員對(duì)象將影響代碼的編寫,因?yàn)樵僖膊荒苁褂胣ame和scores來(lái)描述對(duì)象了,而必須使用用于公有繼承的技術(shù)。例如,對(duì)于構(gòu)造函數(shù),包含將使這樣的構(gòu)造函數(shù):
Student(const char * str, const double * pd, int n): name(str), scores(pd, n) {} // use object names for containment對(duì)于繼承類,新版本的構(gòu)造函數(shù)將使用成員初始化列表語(yǔ)法,它使用類名而不是成員名來(lái)標(biāo)識(shí)構(gòu)造函數(shù):
Student(const char * str, const double * pd, int n): std::string(str), ArrayDb(pd, n) {} // use class names for inheritance在這里,ArrayDb是std::valarray<double>的別名。成員初始化列表使用std::string(str),而不是name(str)。這是包含和私有繼承之間的第二個(gè)主要區(qū)別。
程序清單14.4列出了新的類定義。唯一不同的地方是,省略了顯式對(duì)象名稱,并在內(nèi)聯(lián)構(gòu)造函數(shù)中使用了類名,而不是成員名。
程序清單14.4 studenti.h
// studenti.h -- defining a Student class using private inheritance #ifndef STUDENTC_H_ #define STUDENTC_H_#include <iostream> #include <valarray> #include <string> class Student : private std::string, private std::valarray<double> { private:typedef std::valarray<double> ArrayDb;// private method for scores outputstd::ostream & arr_out(std::ostream & os) const; public:Student() : std::string("Null Student"), ArrayDb() {}explicit Student(const std::string & s): std::string(s), ArrayDb() {}explicit Student(int n) : std::string("Nully"), ArrayDb(n) {}Student(const std::string & s, int n): std::string(s), ArrayDb(n) {}Student(const std::string & s, const ArrayDb & a): std::string(s), ArrayDb(a) {}Student(const char * str, const double * pd, int n): std::string(str), ArrayDb(pd, n) {}~Student() {}double Average() const;double & operator[](int i);double operator[](int i) const;const std::string & Name() const; // friends// inputfriend std::istream & operator>>(std::istream & is,Student & stu); // 1 wordfriend std::istream & getline(std::istream & is,Student & stu); // 1 line// outputfriend std::ostream & operator<<(std::ostream & os,const Student & stu); };#endif2.訪問(wèn)基類的方法
使用私有繼承時(shí),只能在派生類的方法中使用基類的方法。但有時(shí)候可能希望基類工具是公有的。例如,在類聲明中提出可以使用average()函數(shù)。和包含一樣,要實(shí)現(xiàn)這樣的目的,可以在公有Student::average()函數(shù)中使用私有Student::Average()函數(shù)(參見(jiàn)圖14.2)。包含使用對(duì)象來(lái)調(diào)用方法:
圖14.2 對(duì)象中的對(duì)象:私有繼承
double Student::Average() const {if (scores.size() > 0)return scores.sum()/scores.size();elsereturn 0; }然而,私有繼承使得能夠使用類名和作用域解析運(yùn)算符來(lái)調(diào)用基類的方法:
double Student::Average() const {if (ArrayDb::size() > 0)return ArrayDb::sum()/ArrayDb::size();elsereturn 0; }總之,使用包含時(shí)將使用對(duì)象名來(lái)調(diào)用方法,而使用私有繼承時(shí)將使用類名和作用域解析運(yùn)算符來(lái)調(diào)用方法。
3.訪問(wèn)基類對(duì)象
使用作用域解析運(yùn)算符可以訪問(wèn)基類的方法,但如果要使用基類對(duì)象本身,該如何做呢?例如,Student類的包含版本實(shí)現(xiàn)了Name()方法,它返回string對(duì)象成員name;但使用私有繼承時(shí),該string對(duì)象沒(méi)有名稱。那么,Student類的代碼如何訪問(wèn)內(nèi)部的string對(duì)象呢?
答案是使用強(qiáng)制類型轉(zhuǎn)換。由于Student類是從string類派生而來(lái)的,因此可以通過(guò)強(qiáng)制類型轉(zhuǎn)換,將Student對(duì)象轉(zhuǎn)換為string對(duì)象;結(jié)果為繼承而來(lái)的string對(duì)象。本書前面介紹過(guò),指針this指向用來(lái)調(diào)用方法的對(duì)象,因此*this為用來(lái)調(diào)用方法的對(duì)象,在這個(gè)例子中,為類型為Student的對(duì)象。為避免調(diào)用構(gòu)造函數(shù)創(chuàng)建新的對(duì)象,可使用強(qiáng)制類型轉(zhuǎn)換來(lái)創(chuàng)建一個(gè)引用:
const string & Student::Name() const {return (const string &) *this; }上述方法返回一個(gè)引用,該引用指向用于調(diào)用該方法的Student對(duì)象中的繼承而來(lái)的string對(duì)象。
4.訪問(wèn)基類的友元函數(shù)
用類名顯式地限定函數(shù)名不適合于友元函數(shù),這是因?yàn)橛言粚儆陬悺H欢?#xff0c;可以通過(guò)顯式地轉(zhuǎn)換為基類來(lái)調(diào)用正確的函數(shù)。例如,對(duì)于下面的友元函數(shù)定義:
ostream & operator<<(ostream & os, const Student & stu) {os << "Scores for " << (const string &) stu << ":\n"; ... }如果plato是一個(gè)Student對(duì)象,則下面的語(yǔ)句將調(diào)用上述函數(shù),stu將是指向plato的引用,而os將是指向cout的引用:
cout << plato;下面的代碼:
os << "Scores for " << (const string &) stu << ":\n";顯式地將stu轉(zhuǎn)換為string對(duì)象引用,進(jìn)而調(diào)用函數(shù)operator<<(ostream &, const string &)。
引用stu不會(huì)自動(dòng)轉(zhuǎn)換為string引用。根本原因在于,在私有繼承中,未進(jìn)行顯式類型轉(zhuǎn)換的派生類引用或指針,無(wú)法賦值給基類的引用或指針。
然而,即使這個(gè)例子使用的是公有繼承,也必須使用顯式類型轉(zhuǎn)換。原因之一是,如果不使用類型轉(zhuǎn)換,下述代碼將與友元函數(shù)原型匹配,從而導(dǎo)致遞歸調(diào)用:
os << stu;另一個(gè)原因是,由于這個(gè)類使用的是多重繼承,編譯器將無(wú)法確定應(yīng)轉(zhuǎn)換成哪個(gè)基類,如果兩個(gè)基類都提供了函數(shù)operator<<()。程序清單14.5列出了除內(nèi)聯(lián)函數(shù)之外的所有Student類方法。
程序清單14.5 studenti.cpp
// studenti.cpp -- Student class using private inheritance #include "studenti.h" using std::ostream; using std::endl; using std::istream; using std::string;// public methods double Student::Average() const {if (ArrayDb::size() > 0)return ArrayDb::sum()/ArrayDb::size();elsereturn 0; }const string & Student::Name() const {return (const string &) *this; }double & Student::operator[](int i) {return ArrayDb::operator[](i); // use ArrayDb::operator[]() } double Student::operator[](int i) const {return ArrayDb::operator[](i); }// private method ostream & Student::arr_out(ostream & os) const {int i;int lim = ArrayDb::size();if (lim > 0){for (i = 0; i < lim; i++){os << ArrayDb::operator[](i) << " ";if (i % 5 == 4)os << endl;}if (i % 5 != 0)os << endl;}elseos << " empty array ";return os; }// friends // use String version of operator>>() istream & operator>>(istream & is, Student & stu) {is >> (string &)stu;return is; }// use string friend getline(ostream &, const string &) istream & getline(istream & is, Student & stu) {getline(is, (string &)stu);return is; }// use string version of operator<<() ostream & operator<<(ostream & os, const Student & stu) {os << "Scores for " << (const string &) stu << ":\n";stu.arr_out(os); // use private method for scoresreturn os; }同樣,由于這個(gè)示例也重用了string和valarray類的代碼,因此除私有輔助方法外,它包含的新代碼很少。
5.使用修改后的Student類
接下來(lái)也需要測(cè)試這個(gè)新類。注意到兩個(gè)版本的Student類的公有接口完全相同,因此可以使用同一個(gè)程序測(cè)試它們。唯一不同的是,應(yīng)包含studenti.h而不是studentc.h,應(yīng)使用studenti.cpp而不是studentc.cpp來(lái)鏈接程序。程序清單14.6列出列該程序,請(qǐng)將其與studenti.cpp一起編譯。
程序清單14.6 use_stui.cpp
// use_stui.cpp -- using a class with private inheritance // compile with studenti.cpp #include <iostream> #include "studenti.h" using std::cin; using std::cout; using std::endl;void set(Student & sa, int n);const int pupils = 3; const int quizzes = 5;int main() {Student ada[pupils] ={Student(quizzes), Student(quizzes), Student(quizzes)};int i;for (i = 0; i < pupils; i++)set(ada[i], quizzes);cout << "\nStudent List:\n";for (i = 0; i < pupils; ++i)cout << ada[i].Name() << endl;cout << "\nResults:";for (i = 0; i < pupils; i++){cout << endl << ada[i];cout << "average: " << ada[i].Average() << endl;}cout << "Done.\n";return 0; } void set(Student & sa, int n) {cout << "Please enter the student's name: ";getline(cin, sa);cout << "Please enter " << n << " quiz scores:\n";for (int i = 0; i < n; i++)cin >> sa[i];while (cin.get() != '\n')continue; }下面是該程序的運(yùn)行情況:
Please enter the student's name: Gil Bayts Please enter 5 quiz scores: 92 94 96 93 95 Please enter the student's name: Pat Roone Please enter 5 quiz scores: 83 89 72 78 95 Please enter the student's name: Fleur O’Day Please enter 5 quiz scores: 92 89 96 74 64Student List: Gil Bayts Pat Roone Fleur O'DayResults: Scores for Gil Bayts: 92 94 96 93 95 average: 94Scores for Pat Roone: 83 89 72 78 95 average: 83.4Scores for Fleur O'Day: 92 89 96 74 64 average: 83 Done.輸入與前一個(gè)測(cè)試程序相同,輸出也相同。
14.2.2 使用包含還是私有繼承
由于既可以使用包含,也可以使用私有繼承來(lái)建立has-a關(guān)系,那么應(yīng)使用種方式呢?大多數(shù)C++程序員傾向于使用包含。首先,它易于理解。類聲明中包含表示被包含類的顯式命名對(duì)象,代碼可以通過(guò)名稱引用這些對(duì)象,而使用繼承將使關(guān)系更抽象。其次,繼承會(huì)引起很多問(wèn)題,尤其從多個(gè)基類繼承時(shí),可能必須處理很多問(wèn)題,如包含同名方法的獨(dú)立的基類或共享祖先的獨(dú)立基類。總之,使用包含不太可能遇到這樣的麻煩。另外,包含能夠包括多個(gè)同類的子對(duì)象。如果某個(gè)類需要3個(gè)string對(duì)象,可以使用包含聲明3個(gè)獨(dú)立的string成員。而繼承則只能使用一個(gè)這樣的對(duì)象(當(dāng)對(duì)象都沒(méi)有名稱時(shí),將難以區(qū)分)。
然而,私有繼承所提供的特性確實(shí)比包含多。例如,假設(shè)類包含保護(hù)成員(可以是數(shù)據(jù)成員,也可以是成員函數(shù)),則這樣的成員在派生類中是可用的,但在繼承層次結(jié)構(gòu)外是不可用的。如果使用組合將這樣的類包含在另一個(gè)類中,則后者將不是派生類,而是位于繼承層次結(jié)構(gòu)之外,因此不能訪問(wèn)保護(hù)成員。但通過(guò)繼承得到的將是派生類,因此它能夠訪問(wèn)保護(hù)成員。
另一種需要使用私有繼承的情況是需要重新定義虛函數(shù)。派生類可以重新定義虛函數(shù),但包含類不能。使用私有繼承,重新定義的函數(shù)將只能在類中使用,而不是公有的。
提示:
通常,應(yīng)使用包含來(lái)建立has-a關(guān)系;如果新類需要訪問(wèn)原有類的保護(hù)成員,或需要重新定義虛函數(shù),則應(yīng)使用私有繼承。
14.2.3 保護(hù)繼承
保護(hù)繼承是私有繼承的變體。保護(hù)繼承在列出基類時(shí)使用關(guān)鍵字protected:
class Student : protected std::string,protected std::valarray<double> {...};使用保護(hù)繼承時(shí),基類的公有成員和保護(hù)成員都將成為派生類的保護(hù)成員。和私有繼承一樣,基類的接口在派生類中也是可用的,但在繼承層次結(jié)構(gòu)之外是不可用的。當(dāng)從派生類派生出另一個(gè)類時(shí),私有繼承和保護(hù)繼承之間的主要區(qū)別便呈現(xiàn)出來(lái)了。使用私有繼承時(shí),第三代類將不能使用基類的接口,這是因?yàn)榛惖墓蟹椒ㄔ谂缮愔袑⒆兂伤接蟹椒?#xff1b;使用保護(hù)繼承時(shí),基類的公有方法在第二代中將變成受保護(hù)的,因此第三代派生類可以使用它們。
表14.1總結(jié)了公有、私有和保護(hù)繼承。隱式向上轉(zhuǎn)換(implicit upcasting)意味著無(wú)需進(jìn)行顯式類型轉(zhuǎn)換,就可以將基類指針或引用指向派生類對(duì)象。
表14.1 各種繼承方式
?
14.2.4 使用using重新定義訪問(wèn)權(quán)限
使用保護(hù)派生或私有派生時(shí),基類的公有成員將成為保護(hù)成員或私有成員。假設(shè)要讓基類的方法在派生類外面可用,方法之一是定義一個(gè)使用該基類方法的派生類方法。例如,假設(shè)希望Student類能夠使用valarray類的sum()方法,可以在Student類的聲明中聲明一個(gè)sum()方法,然后像下面這樣定義該方法:
double Student::sum() const // public Student method {return std::valarray<double>::sum(); // use privately-inherited method }這樣Student對(duì)象便能夠調(diào)用Student::sum(),后者進(jìn)而將valarray<double>::sum()方法應(yīng)用于被包含的valarray對(duì)象(如果ArrayDb typedef在作用域中,也可以使用ArrayDb而不是std::valarray<double>)。
另一種方法是,將函數(shù)調(diào)用包裝在另一個(gè)函數(shù)調(diào)用中,即使用一個(gè)using聲明(就像名稱空間那樣)來(lái)指出派生類可以使用特定的基類成員,即使采用的是私有派生。例如,假設(shè)希望通過(guò)Student類能夠使用valarray的方法min()和max(),可以在studenti.h的公有部分加入如下using聲明:
class Student : private std::string, private std::valarray<double> { ... public:using std::valarray<double>::min;using std::valarray<double>::max;... };上述using聲明使得valarray<double>::min()和valarray<double>::max()可用,就像它們是Student的公有方法一樣:
cout << "high score: " << ada[i].max() << endl;注意,using聲明只使用成員名——沒(méi)有圓括號(hào)、函數(shù)特征標(biāo)和返回類型。例如,為使Student類可以使用valarray的operator?方法,只需在Student類聲明的公有部分包含下面的using聲明:
using std::valarray<double>::operator[];這將使兩個(gè)版本(const和非const)都可用。這樣,便可以刪除Student::operator[] ()的原型和定義。using聲明只適用于繼承,而不適用于包含。
有一種老式方式可用于在私有派生類中重新聲明基類方法,即將方法名放在派生類的公有部分,如下所示:
class Student : private std::string, private std::valarray<double> { public:std::valarray<double>::operator[]; // redeclare as public, just use name... };這看起來(lái)像不包含關(guān)鍵字using的using聲明。這種方法已被摒棄,即將停止使用。因此,如果編譯器支持using聲明,應(yīng)使用它來(lái)使派生類可以使用私有基類中的方法。
14.3 多重繼承
MI描述的是有多個(gè)直接基類的類。與單繼承一樣,公有MI表示的也是is-a關(guān)系。例如,可以從Waiter類和Singer類派生出SingingWaiter類:
class SingingWaiter : public Waiter, public Singer {...};請(qǐng)注意,必須使用關(guān)鍵字public來(lái)限定每一個(gè)基類。這是因?yàn)?#xff0c;除非特別指出,否則編譯器將認(rèn)為是私有派生:
class SingingWaiter : public Waiter, Singer {...}; // Singer is a private base正如本章前面討論的,私有MI和保護(hù)MI可以表示has-a關(guān)系。Student類的studenti.h實(shí)現(xiàn)就是一個(gè)這樣的示例。下面將重點(diǎn)介紹公有MI。
MI可能會(huì)給程序員帶來(lái)很多新問(wèn)題。其中兩個(gè)主要的問(wèn)題是:從兩個(gè)不同的基類繼承同名方法;從兩個(gè)或更多相關(guān)基類那里繼承同一個(gè)類的多個(gè)實(shí)例。為解決這些問(wèn)題,需要使用一些新規(guī)則和不同的語(yǔ)法。因此,與使用單繼承相比,使用MI更困難,也更容易出現(xiàn)問(wèn)題。由于這個(gè)原因,很多C++用戶強(qiáng)烈反對(duì)使用MI,一些人甚至希望刪除MI;而喜歡MI的人則認(rèn)為,對(duì)一些特殊的工程來(lái)說(shuō),MI很有用,甚至是必不可少的;也有一些人建議謹(jǐn)慎、適度地使用MI。
下面來(lái)看一個(gè)例子,并介紹有哪些問(wèn)題以及如何解決它們。要使用MI,需要幾個(gè)類。我們將定義一個(gè)抽象基類Worker,并使用它派生出Waiter類和Singer類。然后,便可以使用MI從Waiter類和Singer類派生出SingingWaiter類(參見(jiàn)圖 14.3)。這里使用兩個(gè)獨(dú)立的派生來(lái)使基類(Worker)被繼承,這將導(dǎo)致MI的大多數(shù)麻煩。首先聲明Worker、Waiter和Singer類,如程序清單14.7所示。
圖14.3 祖先相同的MI
程序清單14.7 Worker0.h
// worker0.h -- working classes #ifndef WORKER0_H_ #define WORKER0_H_#include <string>class Worker // an abstract base class { private:std::string fullname;long id; public:Worker() : fullname("no one"), id(0L) {}Worker(const std::string & s, long n): fullname(s), id(n) {}virtual ~Worker() = 0; // pure virtual destructorvirtual void Set();virtual void Show() const; };class Waiter : public Worker { private:int panache; public:Waiter() : Worker(), panache(0) {}Waiter(const std::string & s, long n, int p = 0): Worker(s, n), panache(p) {}Waiter(const Worker & wk, int p = 0): Worker(wk), panache(p) {}void Set();void Show() const; };class Singer : public Worker { protected:enum {other, alto, contralto, soprano,bass, baritone, tenor};enum {Vtypes = 7}; private:static char *pv[Vtypes]; // string equivs of voice typesint voice; public:Singer() : Worker(), voice(other) {}Singer(const std::string & s, long n, int v = other): Worker(s, n), voice(v) {}Singer(const Worker & wk, int v = other): Worker(wk), voice(v) {}void Set();void Show() const; };#endif程序清單14.7的類聲明中包含一些表示聲音類型的內(nèi)部常量。一個(gè)枚舉用符號(hào)常量alto、contralto等表示聲音類型,靜態(tài)數(shù)組pv存儲(chǔ)了指向相應(yīng)C-風(fēng)格字符串的指針,程序清單14.8初始化了該數(shù)組,并提供了方法的定義。
程序清單14.8 worker0.cpp
// worker0.cpp -- working class methods #include "worker0.h" #include <iostream> using std::cout; using std::cin; using std::endl; // Worker methods// must implement virtual destructor, even if pure Worker::~Worker() {}void Worker::Set() {cout << "Enter worker's name: ";getline(cin, fullname);cout << "Enter worker's ID: ";cin >> id;while (cin.get() != '\n')continue; }void Worker::Show() const {cout << "Name: " << fullname << "\n";cout << "Employee ID: " << id << "\n"; }// Waiter methods void Waiter::Set() {Worker::Set();cout << "Enter waiter's panache rating: ";cin >> panache;while (cin.get() != '\n')continue; }void Waiter::Show() const {cout << "Category: waiter\n";Worker::Show();cout << "Panache rating: " << panache << "\n"; }// Singer methodschar * Singer::pv[] = {"other", "alto", "contralto","soprano", "bass", "baritone", "tenor"};void Singer::Set() {Worker::Set();cout << "Enter number for singer's vocal range:\n";int i;for (i = 0; i < Vtypes; i++){cout << i << ": " << pv[i] << " ";if ( i % 4 == 3)cout << endl;}if (i % 4 != 0)cout << endl;while (cin >> voice && (voice < 0 || voice >= Vtypes) )cout << "Please enter a value >= 0 and < " << Vtypes << endl;while (cin.get() != '\n')continue; }void Singer::Show() const {cout << "Category: singer\n";Worker::Show();cout << "Vocal range: " << pv[voice] << endl; }程序清單14.9是一個(gè)簡(jiǎn)短的程序,它使用一個(gè)多態(tài)指針數(shù)組對(duì)這些類進(jìn)行了測(cè)試。
程序清單14.9 worktest.cpp
// worktest.cpp -- test worker class hierarchy #include <iostream> #include "worker0.h" const int LIM = 4; int main() {Waiter bob("Bob Apple", 314L, 5);Singer bev("Beverly Hills", 522L, 3);Waiter w_temp;Singer s_temp;Worker * pw[LIM] = {&bob, &bev, &w_temp, &s_temp};int i;for (i = 2; i < LIM; i++)pw[i]->Set();for (i = 0; i < LIM; i++){pw[i]->Show();std::cout << std::endl;}return 0; }下面是程序清單14.7~程序清單14.9組成的程序的輸出:
Enter waiter's name: Waldo Dropmaster Enter worker's ID: 442 Enter waiter's panache rating: 3 Enter singer's name: Sylvie Sirenne Enter worker's ID: 555 Enter number for singer's vocal range: 0: other 1: alto 2: contralto 3: soprano 4: bass 5: baritone 6: tenor 3 Category: waiter Name: Bob Apple Employee ID: 314 Panache rating: 5Category: singer Name: Beverly Hills Employee ID: 522 Vocal range: sopranoCategory: waiter Name: Waldo Dropmaster Employee ID: 442 Panache rating: 3Category: singer Name: Sylvie Sirenne Employee ID: 555 Vocal range: soprano這種設(shè)計(jì)看起來(lái)是可行的:使用Waiter指針來(lái)調(diào)用Waiter::Show()和Waiter::Set();使用Singer指針來(lái)調(diào)用Singer::Show()和Singer::Set()。然后,如果添加一個(gè)從Singer和Waiter類派生出的SingingWaiter類后,將帶來(lái)一些問(wèn)題。具體地說(shuō),將出現(xiàn)以下問(wèn)題。
- 有多少Worker?
- 哪個(gè)方法?
14.3.1 有多少Worker
假設(shè)首先從Singer和Waiter公有派生出SingingWaiter:
class SingingWaiter: public Singer, public Waiter {...};因?yàn)镾inger和Waiter都繼承了一個(gè)Worker組件,因此SingingWaiter將包含兩個(gè)Worker組件(參見(jiàn)圖14.4)。
正如預(yù)期的,這將引起問(wèn)題。例如,通常可以將派生類對(duì)象的地址賦給基類指針,但現(xiàn)在將出現(xiàn)二義性:
SingingWaiter ed; Worker * pw = &ed; // ambiguous通常,這種賦值將把基類指針設(shè)置為派生對(duì)象中的基類對(duì)象的地址。但ed中包含兩個(gè)Worker對(duì)象,有兩個(gè)地址可供選擇,所以應(yīng)使用類型轉(zhuǎn)換來(lái)指定對(duì)象:
Worker * pw1 = (Waiter *) &ed; // the Worker in Waiter Worker * pw2 = (Singer *) &ed; // the Worker in Singer這將使得使用基類指針來(lái)引用不同的對(duì)象(多態(tài)性)復(fù)雜化。
包含兩個(gè)Worker對(duì)象拷貝還會(huì)導(dǎo)致其他的問(wèn)題。然而,真正的問(wèn)題是:為什么需要Worker對(duì)象的兩個(gè)拷貝?唱歌的侍者和其他Worker對(duì)象一樣,也應(yīng)只包含一個(gè)姓名和一個(gè)ID。C++引入多重繼承的同時(shí),引入了一種新技術(shù)——虛基類(virtual base class),使MI成為可能。
圖14.4 繼承兩個(gè)基類對(duì)象
1.虛基類
虛基類使得從多個(gè)類(它們的基類相同)派生出的對(duì)象只繼承一個(gè)基類對(duì)象。例如,通過(guò)在類聲明中使用關(guān)鍵字virtual,可以使Worker被用作Singer和Waiter的虛基類(virtual和public的次序無(wú)關(guān)緊要):
class Singer : virtual public Worker {...}; class Waiter : public virtual Worker {...};然后,可以將SingingWaiter類定義為:
class SingingWaiter: public Singer, public Waiter {...};現(xiàn)在,SingingWaiter對(duì)象將只包含Worker對(duì)象的一個(gè)副本。從本質(zhì)上說(shuō),繼承的Singer和Waiter對(duì)象共享一個(gè)Worker對(duì)象,而不是各自引入自己的Worker對(duì)象副本(參見(jiàn)圖14.5)。因?yàn)镾ingingWaiter現(xiàn)在只包含了一個(gè)Worker子對(duì)象,所以可以使用多態(tài)。
圖14.5 虛基類繼承
您可能會(huì)有這樣的疑問(wèn):
- 為什么使用術(shù)語(yǔ)“虛”?
- 為什么不拋棄將基類聲明為虛的這種方式,而使虛行為成為多MI的準(zhǔn)則呢?
- 是否存在麻煩呢?
首先,為什么使用術(shù)語(yǔ)虛?畢竟,在虛函數(shù)和虛基類之間并不存在明顯的聯(lián)系。C++用戶強(qiáng)烈反對(duì)引入新的關(guān)鍵字,因?yàn)檫@將給他們帶來(lái)很大的壓力。例如,如果新關(guān)鍵字與重要程序中的重要函數(shù)或變量的名稱相同,這將非常麻煩。因此,C++對(duì)這種新特性也使用關(guān)鍵字virtual——有點(diǎn)像關(guān)鍵字重載。
其次,為什么不拋棄將基類聲明為虛的這種方式,而使虛行為成為MI的準(zhǔn)則呢?第一,在一些情況下,可能需要基類的多個(gè)拷貝;第二,將基類作為虛的要求程序完成額外的計(jì)算,為不需要的工具付出代價(jià)是不應(yīng)當(dāng)?shù)?#xff1b;第三,這樣做有其缺點(diǎn),將在下一段介紹。
最后,是否存在麻煩?是的。為使虛基類能夠工作,需要對(duì)C++規(guī)則進(jìn)行調(diào)整,必須以不同的方式編寫一些代碼。另外,使用虛基類還可能需要修改已有的代碼。例如,將SingingWaiter類添加到Worker集成層次中時(shí),需要在Singer和Waiter類中添加關(guān)鍵字virtual。
2.新的構(gòu)造函數(shù)規(guī)則
使用虛基類時(shí),需要對(duì)類構(gòu)造函數(shù)采用一種新的方法。對(duì)于非虛基類,唯一可以出現(xiàn)在初始化列表中的構(gòu)造函數(shù)即是基類構(gòu)造函數(shù)。但這些構(gòu)造函數(shù)可能需要將信息傳遞給其基類。例如,可能有下面一組構(gòu)造函數(shù):
class A {int a; public:A(int n = 0) : a(n) {}... }; class B: public A {int b; public:B(int m = 0, int n = 0) : A(n), b(m) {}... }; class C : public B {int c; public:C(int q = 0, int m = 0, int n = 0) : B(m, n), c(q) {}... };C類的構(gòu)造函數(shù)只能調(diào)用B類的構(gòu)造函數(shù),而B類的構(gòu)造函數(shù)只能調(diào)用A類的構(gòu)造函數(shù)。這里,C類的構(gòu)造函數(shù)使用值q,并將值m和n傳遞給B類的構(gòu)造函數(shù);而B類的構(gòu)造函數(shù)使用值m,并將值n傳遞給A類的構(gòu)造函數(shù)。
如果Worker是虛基類,則這種信息自動(dòng)傳遞將不起作用。例如,對(duì)于下面的MI構(gòu)造函數(shù):
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other): Waiter(wk,p), Singer(wk,v) {} // flawed存在的問(wèn)題是,自動(dòng)傳遞信息時(shí),將通過(guò)2條不同的途徑(Waiter和Singer)將wk傳遞給Worker對(duì)象。為避免這種沖突,C++在基類是虛的時(shí),禁止信息通過(guò)中間類自動(dòng)傳遞給基類。因此,上述構(gòu)造函數(shù)將初始化成員panache和voice,但wk參數(shù)中的信息將不會(huì)傳遞給子對(duì)象Waiter。然而,編譯器必須在構(gòu)造派生對(duì)象之前構(gòu)造基類對(duì)象組件;在上述情況下,編譯器將使用Worker的默認(rèn)構(gòu)造函數(shù)。
如果不希望默認(rèn)構(gòu)造函數(shù)來(lái)構(gòu)造虛基類對(duì)象,則需要顯式地調(diào)用所需的基類構(gòu)造函數(shù)。因此,構(gòu)造函數(shù)應(yīng)該是這樣:
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other): Worker(wk), Waiter(wk,p), Singer(wk,v) {}上述代碼將顯式地調(diào)用構(gòu)造函數(shù)worker(const Worker &)。請(qǐng)注意,這種用法是合法的,對(duì)于虛基類,必須這樣做;但對(duì)于非虛基類,則是非法的。
警告:
如果類有間接虛基類,則除非只需使用該虛基類的默認(rèn)構(gòu)造函數(shù),否則必須顯式地調(diào)用該虛基類的某個(gè)構(gòu)造函數(shù)。
本文摘自《C++ Primer Plus(第6版)中文版》
本書在介紹C++特性的同時(shí),還討論了基本C語(yǔ)言,使兩者成為有機(jī)的整體。書中介紹了C++的基本概念,并通過(guò)短小精悍的程序來(lái)闡明,這些程序都很容易復(fù)制和試驗(yàn)。書中還介紹了輸入和輸出,如何讓程序執(zhí)行重復(fù)性任務(wù),如何讓程序做出選擇,處理數(shù)據(jù)的多種方式,以及如何使用函數(shù)等內(nèi)容。另外,本書還講述了C++在C語(yǔ)言的基礎(chǔ)上新增的很多特性,包括:
- 類和對(duì)象;
- 繼承;
- 多態(tài)、虛函數(shù)和RTTI(運(yùn)行階段類型識(shí)別);
- 函數(shù)重載;
- 引用變量;
- 泛型(獨(dú)立于類型的)編程,這種技術(shù)是由模板和標(biāo)準(zhǔn)模板庫(kù)(STL)提供的;
處理錯(cuò)誤條件的異常機(jī)制;
- 管理函數(shù)、類和變量名的名稱空間。
?
總結(jié)
以上是生活随笔為你收集整理的一起来学C++:C++中的代码重用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 手柄xinput模式_玩家新宠,谷粒金刚
- 下一篇: s3c2440移植MQTT