日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

一起来学C++:C++中的代码重用

發布時間:2023/12/14 c/c++ 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一起来学C++:C++中的代码重用 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

14.1 包含對象成員的類

14.1.1 valarray類簡介

14.1.2 Student類的設計

14.1.3 Student類示例

1.初始化被包含的對象

2.使用被包含對象的接口

3.使用新的Student類

14.2 私有繼承

14.2.1 Student類示例(新版本)

1.初始化基類組件

2.訪問基類的方法

3.訪問基類對象

4.訪問基類的友元函數

5.使用修改后的Student類

14.2.2 使用包含還是私有繼承

14.2.3 保護繼承

14.2.4 使用using重新定義訪問權限

14.3 多重繼承

14.3.1 有多少Worker

1.虛基類

2.新的構造函數規則


本章內容包括:

  • has-a關系;
  • 包含對象成員的類;
  • 模板類valarray;
  • 私有和保護繼承;
  • 多重繼承;
  • 虛基類;
  • 創建類模板;
  • 使用類模板;
  • 模板的具體化。

C++的一個主要目標是促進代碼重用。公有繼承是實現這種目標的機制之一,但并不是唯一的機制。本章將介紹其他方法,其中之一是使用這樣的類成員:本身是另一個類的對象。這種方法稱為包含(containment)、組合(composition)或層次化(layering)。另一種方法是使用私有或保護繼承。通常,包含、私有繼承和保護繼承用于實現has-a關系,即新的類將包含另一個類的對象。例如,HomeTheater類可能包含一個BluRayPlayer對象。多重繼承使得能夠使用兩個或更多的基類派生出新的類,將基類的功能組合在一起。

第10章介紹了函數模板,本章將介紹類模板——另一種重用代碼的方法。類模板使我們能夠使用通用術語定義類,然后使用模板來創建針對特定類型定義的特殊類。例如,可以定義一個通用的棧模板,然后使用該模板創建一個用于表示int值棧的類和一個用于表示double值棧的類,甚至可以創建一個這樣的類,即用于表示由棧組成的棧。

14.1 包含對象成員的類

首先介紹包含對象成員的類。有一些類(如string類和第16章將介紹的標準C++類模板)為表示類中的組件提供了方便的途徑。下面來看一個具體的例子。

學生是什么?入學者?參加研究的人?殘酷現實社會的避難者?有姓名和一系列考試分數的人?顯然,最后一個定義完全沒有表示出人的特征,但非常適合于簡單的計算機表示。因此,讓我們根據該定義來開發Student類。

將學生簡化成姓名和一組考試分數后,可以使用一個包含兩個成員的類來表示它:一個成員用于表示姓名,另一個成員用于表示分數。對于姓名,可以使用字符數組來表示,但這將限制姓名的長度。當然,也可以使用char指針和動態內存分配,但正如第12章指出的,這將要求提供大量的支持代碼。一種更好的方法是,使用一個由他人開發好的類的對象來表示。例如,可以使用一個String類(參見第12章)或標準C++ string類的對象來表示姓名。較簡單的選擇是使用string類,因為C++庫提供了這個類的所有實現代碼,且其實現更完美。要使用String類,您必須在項目中包含實現文件string1.cpp。

對于考試分數,存在類似的選擇??梢允褂靡粋€定長數組,這限制了數組的長度;可以使用動態內存分配并提供大量的支持代碼;也可以設計一個使用動態內存分配的類來表示該數組;還可以在標準C++庫中查找一個能夠表示這種數據的類。

自己開發這樣的類一點問題也沒有。開發簡單的版本并不那么難,因為double數組與char數組有很多相似之處,因此可以根據String類來設計表示double數組的類。事實上,本書以前的版本就這樣做過。

當然,如果C++庫提供了合適的類,實現起來將更簡單。C++庫確實提供了一個這樣的類,它就是valarray。

14.1.1 valarray類簡介

valarray類是由頭文件valarray支持的。顧名思義,這個類用于處理數值(或具有類似特性的類),它支持諸如將數組中所有元素的值相加以及在數組中找出最大和最小的值等操作。valarray被定義為一個模板類,以便能夠處理不同的數據類型。本章后面將介紹如何定義模板類,但就現在而言,您只需知道如何使用模板類即可。

模板特性意味著聲明對象時,必須指定具體的數據類型。因此,使用valarray類來聲明一個對象時,需要在標識符valarray后面加上一對尖括號,并在其中包含所需的數據類型:

valarray<int> q_values; // an array of int valarray<double> weights; // an array of double

第4章介紹vector和array類時,您見過這種語法,它非常簡單。這些類也可用于存儲數字,但它們提供的算術支持沒有valarray多。

這是您需要學習的唯一新語法,它非常簡單。

類特性意味著要使用valarray對象,需要了解這個類的構造函數和其他類方法。下面是幾個使用其構造函數的例子:

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

從中可知,可以創建長度為零的空數組、指定長度的空數組、所有元素度被初始化為指定值的數組、用常規數組中的值進行初始化的數組。在C++11中,也可使用初始化列表:

valarray<int> v5 = {20, 32, 17, 9}; // C++11

下面是這個類的一些方法。

  • operator?:讓您能夠訪問各個元素。
  • size():返回包含的元素數。
  • sum():返回所有元素的總和。
  • max():返回最大的元素。
  • min():返回最小的元素。

還有很多其他的方法,其中的一些將在第16章介紹;但就這個例子而言,上述方法足夠了。

14.1.2 Student類的設計

至此,已經確定了Student類的設計計劃:使用一個string對象來表示姓名,使用一個valarray<double>來表示考試分數。那么如何設計呢?您可能想以公有的方式從這兩個類派生出Student類,這將是多重公有繼承,C++允許這樣做,但在這里并不合適,因為學生與這些類之間的關系不是is-a模型。學生不是姓名,也不是一組考試成績。這里的關系是has-a,學生有姓名,也有一組考試分數。通常,用于建立has-a關系的C++技術是組合(包含),即創建一個包含其他類對象的類。例如,可以將Student類聲明為如下所示:

class Student { private:string name; // use a string object for namevalarray<double> scores; // use a valarray<double> object for scores... };

同樣,上述類將數據成員聲明為私有的。這意味著Student類的成員函數可以使用string和valarray<double>類的公有接口來訪問和修改name和scores對象,但在類的外面不能這樣做,而只能通過Student類的公有接口訪問name和score(請參見圖14.1)。對于這種情況,通常被描述為Student類獲得了其成員對象的實現,但沒有繼承接口。例如,Student對象使用string的實現,而不是char * name或char name [26]實現來保存姓名。但Student對象并不是天生就有使用函數string operator+=()的能力。

圖14.1 對象中的對象:包含

?


接口和實現

使用公有繼承時,類可以繼承接口,可能還有實現(基類的純虛函數提供接口,但不提供實現)。獲得接口是is-a關系的組成部分。而使用組合,類可以獲得實現,但不能獲得接口。不繼承接口是has-a關系的組成部分。


對于has-a關系來說,類對象不能自動獲得被包含對象的接口是一件好事。例如,string類將+運算符重載為將兩個字符串連接起來;但從概念上說,將兩個Student對象串接起來是沒有意義的。這也是這里不使用公有繼承的原因之一。另一方面,被包含的類的接口部分對新類來說可能是有意義的。例如,可能希望使用string接口中的operator<()方法將Student對象按姓名進行排序,為此可以定義Student::operator<()成員函數,它在內部使用函數string::operator<()。下面介紹一些細節。

14.1.3 Student類示例

現在需要提供Student類的定義,當然它應包含構造函數以及一些用作Student類接口的方法。程序清單14.1是Student類的定義,其中所有構造函數都被定義為內聯的;它還提供了一些用于輸入和輸出的友元函數。

程序清單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

為簡化表示,Student類的定義中包含下述typedef:

typedef std::valarray<double> ArrayDb;

這樣,在以后的代碼中便可以使用表示ArrayDb,而不是std::valarray<double>,因此類方法和友元函數可以使用ArrayDb類型。將該typedef放在類定義的私有部分意味著可以在Student類的實現中使用它,但在Student類外面不能使用。

請注意關鍵字explicit的用法:

explicit Student(const std::string & s): name(s), scores() {} explicit Student(int n) : name("Nully"), scores(n) {}

本書前面說過,可以用一個參數調用的構造函數將用作從參數類型到類類型的隱式轉換函數;但這通常不是好主意。在上述第二個構造函數中,第一個參數表示數組的元素個數,而不是數組中的值,因此將一個構造函數用作int到Student的轉換函數是沒有意義的,所以使用explicit關閉隱式轉換。如果省略該關鍵字,則可以編寫如下所示的代碼:

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]。如果構造函數省略了explicit,則將使用構造函數調用Student(5)將5轉換為一個臨時Student對象,并使用“Nully”來設置成員name的值。因此賦值操作將使用臨時對象替換原來的doh值。使用了explicit后,編譯器將認為上述賦值運算符是錯誤的。


C++和約束

C++包含讓程序員能夠限制程序結構的特性——使用explicit防止單參數構造函數的隱式轉換,使用const限制方法修改數據,等等。這樣做的根本原因是:在編譯階段出現錯誤優于在運行階段出現錯誤。


1.初始化被包含的對象

構造函數全都使用您熟悉的成員初始化列表語法來初始化name和score成員對象。在前面的一些例子中,構造函數用這種語法來初始化內置類型的成員:

Queue::Queue(int qs) : qsize(qs) {...} // initialize qsize to qs

上述代碼在成員初始化列表中使用的是數據成員的名稱(qsize)。另外,前面介紹的示例中的構造函數還使用成員初始化列表初始化派生對象的基類部分:

hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs) {...}

對于繼承的對象,構造函數在成員初始化列表中使用類名來調用特定的基類構造函數。對于成員對象,構造函數則使用成員名。例如,請看程序清單14.1的最后一個構造函數:

Student(const char * str, const double * pd, int n): name(str), scores(pd, n) {}

因為該構造函數初始化的是成員對象,而不是繼承的對象,所以在初始化列表中使用的是成員名,而不是類名。初始化列表中的每一項都調用與之匹配的構造函數,即name(str)調用構造函數string(const char *),scores(pd, n)調用構造函數ArrayDb(const double *, int)。

如果不使用初始化列表語法,情況將如何呢?C++要求在構建對象的其他部分之前,先構建繼承對象的所有成員對象。因此,如果省略初始化列表,C++將使用成員對象所屬類的默認構造函數。


初始化順序

當初始化列表包含多個項目時,這些項目被初始化的順序為它們被聲明的順序,而不是它們在初始化列表中的順序。例如,假設Student構造函數如下:

Student(const char * str, const double * pd, int n): scores(pd, n), name(str) {}

則name成員仍將首先被初始化,因為在類定義中它首先被聲明。對于這個例子來說,初始化順序并不重要,但如果代碼使用一個成員的值作為另一個成員的初始化表達式的一部分時,初始化順序就非常重要了。


2.使用被包含對象的接口

被包含對象的接口不是公有的,但可以在類方法中使用它。例如,下面的代碼說明了如何定義一個返回學生平均分數的函數:

double Student::Average() const {if (scores.size() > 0)return scores.sum()/scores.size();elsereturn 0; }

上述代碼定義了可由Student對象調用的方法,該方法內部使用了valarray的方法size()和sum()。這是因為scores是一個valarray對象,所以它可以調用valarray類的成員函數??傊?#xff0c;Student對象調用Student的方法,而后者使用被包含的valarray對象來調用valarray類的方法。

同樣,可以定義一個使用string版本的<<運算符的友元函數:

// use string version of operator<<() ostream & operator<<(ostream & os, const Student & stu) {os << "Scores for " << stu.name << ":\n";... }

因為stu.name是一個string對象,所以它將調用函數operatot<<(ostream &, const string &),該函數位于string類中。注意,operator<<(ostream & os, const Student & stu)必須是Student類的友元函數,這樣才能訪問name成員。另一種方法是,在該函數中使用公有方法Name(),而不是私有數據成員name。

同樣,該函數也可以使用valarray的<<實現來進行輸出,不幸的是沒有這樣的實現;因此,Student類定義了一個私有輔助方法來處理這種任務:

// 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; }

通過使用這樣的輔助方法,可以將零亂的細節放在一個地方,使得友元函數的編碼更為整潔:

// 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是Student類的類方法文件,其中包含了讓您能夠使用[ ]運算符來訪問Student對象中各項成績的方法。

程序清單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并沒有新增多少代碼。使用包含讓您能夠充分利用已有的代碼。

3.使用新的Student類

下面編寫一個小程序來測試這個新的Student類。出于簡化的目的,該程序將使用一個只包含3個Student對象的數組,其中每個對象保存5個考試成績。另外還將使用一個不復雜的輸入循環,該循環不驗證輸入,也不讓用戶中途退出。程序清單14.3列出了該測試程序,請務必將該程序與Student.cpp一起進行編譯。

程序清單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組成的程序的運行情況:

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++還有另一種實現has-a關系的途徑——私有繼承。使用私有繼承,基類的公有成員和保護成員都將成為派生類的私有成員。這意味著基類方法將不會成為派生對象公有接口的一部分,但可以在派生類的成員函數中使用它們。

下面更深入地探討接口問題。使用公有繼承,基類的公有方法將成為派生類的公有方法??傊?#xff0c;派生類將繼承基類的接口;這是is-a關系的一部分。使用私有繼承,基類的公有方法將成為派生類的私有方法。總之,派生類不繼承基類的接口。正如從被包含對象中看到的,這種不完全繼承是has-a關系的一部分。

使用私有繼承,類將繼承實現。例如,如果從String類派生出Student類,后者將有一個String類組件,可用于保存字符串。另外,Student方法可以使用String方法來訪問String組件。

包含將對象作為一個命名的成員對象添加到類中,而私有繼承將對象作為一個未被命名的繼承對象添加到類中。我們將使用術語子對象(subobject)來表示通過繼承或包含添加的對象。

因此私有繼承提供的特性與包含相同:獲得實現,但不獲得接口。所以,私有繼承也可以用來實現has-a關系。接下來介紹如何使用私有繼承來重新設計Student類。

14.2.1 Student類示例(新版本)

要進行私有繼承,請使用關鍵字private而不是public來定義類(實際上,private是默認值,因此省略訪問限定符也將導致私有繼承)。Student類應從兩個類派生而來,因此聲明將列出這兩個類:

class Student : private std::string, private std::valarray<double> { public:... };

使用多個基類的繼承被稱為多重繼承(multiple inheritance,MI)。通常,MI尤其是公有MI將導致一些問題,必須使用額外的語法規則來解決它們,這將在本章后面介紹。但在這個示例中,MI不會導致問題。

新的Student類不需要私有數據,因為兩個基類已經提供了所需的所有數據成員。包含版本提供了兩個被顯式命名的對象成員,而私有繼承提供了兩個無名稱的子對象成員。這是這兩種方法的第一個主要區別。

1.初始化基類組件

隱式地繼承組件而不是成員對象將影響代碼的編寫,因為再也不能使用name和scores來描述對象了,而必須使用用于公有繼承的技術。例如,對于構造函數,包含將使這樣的構造函數:

Student(const char * str, const double * pd, int n): name(str), scores(pd, n) {} // use object names for containment

對于繼承類,新版本的構造函數將使用成員初始化列表語法,它使用類名而不是成員名來標識構造函數:

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)。這是包含和私有繼承之間的第二個主要區別。

程序清單14.4列出了新的類定義。唯一不同的地方是,省略了顯式對象名稱,并在內聯構造函數中使用了類名,而不是成員名。

程序清單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); };#endif

2.訪問基類的方法

使用私有繼承時,只能在派生類的方法中使用基類的方法。但有時候可能希望基類工具是公有的。例如,在類聲明中提出可以使用average()函數。和包含一樣,要實現這樣的目的,可以在公有Student::average()函數中使用私有Student::Average()函數(參見圖14.2)。包含使用對象來調用方法:

圖14.2 對象中的對象:私有繼承

double Student::Average() const {if (scores.size() > 0)return scores.sum()/scores.size();elsereturn 0; }

然而,私有繼承使得能夠使用類名和作用域解析運算符來調用基類的方法:

double Student::Average() const {if (ArrayDb::size() > 0)return ArrayDb::sum()/ArrayDb::size();elsereturn 0; }

總之,使用包含時將使用對象名來調用方法,而使用私有繼承時將使用類名和作用域解析運算符來調用方法。

3.訪問基類對象

使用作用域解析運算符可以訪問基類的方法,但如果要使用基類對象本身,該如何做呢?例如,Student類的包含版本實現了Name()方法,它返回string對象成員name;但使用私有繼承時,該string對象沒有名稱。那么,Student類的代碼如何訪問內部的string對象呢?

答案是使用強制類型轉換。由于Student類是從string類派生而來的,因此可以通過強制類型轉換,將Student對象轉換為string對象;結果為繼承而來的string對象。本書前面介紹過,指針this指向用來調用方法的對象,因此*this為用來調用方法的對象,在這個例子中,為類型為Student的對象。為避免調用構造函數創建新的對象,可使用強制類型轉換來創建一個引用:

const string & Student::Name() const {return (const string &) *this; }

上述方法返回一個引用,該引用指向用于調用該方法的Student對象中的繼承而來的string對象。

4.訪問基類的友元函數

用類名顯式地限定函數名不適合于友元函數,這是因為友元不屬于類。然而,可以通過顯式地轉換為基類來調用正確的函數。例如,對于下面的友元函數定義:

ostream & operator<<(ostream & os, const Student & stu) {os << "Scores for " << (const string &) stu << ":\n"; ... }

如果plato是一個Student對象,則下面的語句將調用上述函數,stu將是指向plato的引用,而os將是指向cout的引用:

cout << plato;

下面的代碼:

os << "Scores for " << (const string &) stu << ":\n";

顯式地將stu轉換為string對象引用,進而調用函數operator<<(ostream &, const string &)。

引用stu不會自動轉換為string引用。根本原因在于,在私有繼承中,未進行顯式類型轉換的派生類引用或指針,無法賦值給基類的引用或指針。

然而,即使這個例子使用的是公有繼承,也必須使用顯式類型轉換。原因之一是,如果不使用類型轉換,下述代碼將與友元函數原型匹配,從而導致遞歸調用:

os << stu;

另一個原因是,由于這個類使用的是多重繼承,編譯器將無法確定應轉換成哪個基類,如果兩個基類都提供了函數operator<<()。程序清單14.5列出了除內聯函數之外的所有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; }

同樣,由于這個示例也重用了string和valarray類的代碼,因此除私有輔助方法外,它包含的新代碼很少。

5.使用修改后的Student類

接下來也需要測試這個新類。注意到兩個版本的Student類的公有接口完全相同,因此可以使用同一個程序測試它們。唯一不同的是,應包含studenti.h而不是studentc.h,應使用studenti.cpp而不是studentc.cpp來鏈接程序。程序清單14.6列出列該程序,請將其與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; }

下面是該程序的運行情況:

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.

輸入與前一個測試程序相同,輸出也相同。

14.2.2 使用包含還是私有繼承

由于既可以使用包含,也可以使用私有繼承來建立has-a關系,那么應使用種方式呢?大多數C++程序員傾向于使用包含。首先,它易于理解。類聲明中包含表示被包含類的顯式命名對象,代碼可以通過名稱引用這些對象,而使用繼承將使關系更抽象。其次,繼承會引起很多問題,尤其從多個基類繼承時,可能必須處理很多問題,如包含同名方法的獨立的基類或共享祖先的獨立基類。總之,使用包含不太可能遇到這樣的麻煩。另外,包含能夠包括多個同類的子對象。如果某個類需要3個string對象,可以使用包含聲明3個獨立的string成員。而繼承則只能使用一個這樣的對象(當對象都沒有名稱時,將難以區分)。

然而,私有繼承所提供的特性確實比包含多。例如,假設類包含保護成員(可以是數據成員,也可以是成員函數),則這樣的成員在派生類中是可用的,但在繼承層次結構外是不可用的。如果使用組合將這樣的類包含在另一個類中,則后者將不是派生類,而是位于繼承層次結構之外,因此不能訪問保護成員。但通過繼承得到的將是派生類,因此它能夠訪問保護成員。

另一種需要使用私有繼承的情況是需要重新定義虛函數。派生類可以重新定義虛函數,但包含類不能。使用私有繼承,重新定義的函數將只能在類中使用,而不是公有的。

提示: 

通常,應使用包含來建立has-a關系;如果新類需要訪問原有類的保護成員,或需要重新定義虛函數,則應使用私有繼承。

14.2.3 保護繼承

保護繼承是私有繼承的變體。保護繼承在列出基類時使用關鍵字protected:

class Student : protected std::string,protected std::valarray<double> {...};

使用保護繼承時,基類的公有成員和保護成員都將成為派生類的保護成員。和私有繼承一樣,基類的接口在派生類中也是可用的,但在繼承層次結構之外是不可用的。當從派生類派生出另一個類時,私有繼承和保護繼承之間的主要區別便呈現出來了。使用私有繼承時,第三代類將不能使用基類的接口,這是因為基類的公有方法在派生類中將變成私有方法;使用保護繼承時,基類的公有方法在第二代中將變成受保護的,因此第三代派生類可以使用它們。

表14.1總結了公有、私有和保護繼承。隱式向上轉換(implicit upcasting)意味著無需進行顯式類型轉換,就可以將基類指針或引用指向派生類對象。

表14.1 各種繼承方式

?

14.2.4 使用using重新定義訪問權限

使用保護派生或私有派生時,基類的公有成員將成為保護成員或私有成員。假設要讓基類的方法在派生類外面可用,方法之一是定義一個使用該基類方法的派生類方法。例如,假設希望Student類能夠使用valarray類的sum()方法,可以在Student類的聲明中聲明一個sum()方法,然后像下面這樣定義該方法:

double Student::sum() const // public Student method {return std::valarray<double>::sum(); // use privately-inherited method }

這樣Student對象便能夠調用Student::sum(),后者進而將valarray<double>::sum()方法應用于被包含的valarray對象(如果ArrayDb typedef在作用域中,也可以使用ArrayDb而不是std::valarray<double>)。

另一種方法是,將函數調用包裝在另一個函數調用中,即使用一個using聲明(就像名稱空間那樣)來指出派生類可以使用特定的基類成員,即使采用的是私有派生。例如,假設希望通過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聲明只使用成員名——沒有圓括號、函數特征標和返回類型。例如,為使Student類可以使用valarray的operator?方法,只需在Student類聲明的公有部分包含下面的using聲明:

using std::valarray<double>::operator[];

這將使兩個版本(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... };

這看起來像不包含關鍵字using的using聲明。這種方法已被摒棄,即將停止使用。因此,如果編譯器支持using聲明,應使用它來使派生類可以使用私有基類中的方法。

14.3 多重繼承

MI描述的是有多個直接基類的類。與單繼承一樣,公有MI表示的也是is-a關系。例如,可以從Waiter類和Singer類派生出SingingWaiter類:

class SingingWaiter : public Waiter, public Singer {...};

請注意,必須使用關鍵字public來限定每一個基類。這是因為,除非特別指出,否則編譯器將認為是私有派生:

class SingingWaiter : public Waiter, Singer {...}; // Singer is a private base

正如本章前面討論的,私有MI和保護MI可以表示has-a關系。Student類的studenti.h實現就是一個這樣的示例。下面將重點介紹公有MI。

MI可能會給程序員帶來很多新問題。其中兩個主要的問題是:從兩個不同的基類繼承同名方法;從兩個或更多相關基類那里繼承同一個類的多個實例。為解決這些問題,需要使用一些新規則和不同的語法。因此,與使用單繼承相比,使用MI更困難,也更容易出現問題。由于這個原因,很多C++用戶強烈反對使用MI,一些人甚至希望刪除MI;而喜歡MI的人則認為,對一些特殊的工程來說,MI很有用,甚至是必不可少的;也有一些人建議謹慎、適度地使用MI。

下面來看一個例子,并介紹有哪些問題以及如何解決它們。要使用MI,需要幾個類。我們將定義一個抽象基類Worker,并使用它派生出Waiter類和Singer類。然后,便可以使用MI從Waiter類和Singer類派生出SingingWaiter類(參見圖 14.3)。這里使用兩個獨立的派生來使基類(Worker)被繼承,這將導致MI的大多數麻煩。首先聲明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的類聲明中包含一些表示聲音類型的內部常量。一個枚舉用符號常量alto、contralto等表示聲音類型,靜態數組pv存儲了指向相應C-風格字符串的指針,程序清單14.8初始化了該數組,并提供了方法的定義。

程序清單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是一個簡短的程序,它使用一個多態指針數組對這些類進行了測試。

程序清單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

這種設計看起來是可行的:使用Waiter指針來調用Waiter::Show()和Waiter::Set();使用Singer指針來調用Singer::Show()和Singer::Set()。然后,如果添加一個從Singer和Waiter類派生出的SingingWaiter類后,將帶來一些問題。具體地說,將出現以下問題。

  • 有多少Worker?
  • 哪個方法?

14.3.1 有多少Worker

假設首先從Singer和Waiter公有派生出SingingWaiter:

class SingingWaiter: public Singer, public Waiter {...};

因為Singer和Waiter都繼承了一個Worker組件,因此SingingWaiter將包含兩個Worker組件(參見圖14.4)。

正如預期的,這將引起問題。例如,通??梢詫⑴缮悓ο蟮牡刂焚x給基類指針,但現在將出現二義性:

SingingWaiter ed; Worker * pw = &ed; // ambiguous

通常,這種賦值將把基類指針設置為派生對象中的基類對象的地址。但ed中包含兩個Worker對象,有兩個地址可供選擇,所以應使用類型轉換來指定對象:

Worker * pw1 = (Waiter *) &ed; // the Worker in Waiter Worker * pw2 = (Singer *) &ed; // the Worker in Singer

這將使得使用基類指針來引用不同的對象(多態性)復雜化。

包含兩個Worker對象拷貝還會導致其他的問題。然而,真正的問題是:為什么需要Worker對象的兩個拷貝?唱歌的侍者和其他Worker對象一樣,也應只包含一個姓名和一個ID。C++引入多重繼承的同時,引入了一種新技術——虛基類(virtual base class),使MI成為可能。

圖14.4 繼承兩個基類對象

1.虛基類

虛基類使得從多個類(它們的基類相同)派生出的對象只繼承一個基類對象。例如,通過在類聲明中使用關鍵字virtual,可以使Worker被用作Singer和Waiter的虛基類(virtual和public的次序無關緊要):

class Singer : virtual public Worker {...}; class Waiter : public virtual Worker {...};

然后,可以將SingingWaiter類定義為:

class SingingWaiter: public Singer, public Waiter {...};

現在,SingingWaiter對象將只包含Worker對象的一個副本。從本質上說,繼承的Singer和Waiter對象共享一個Worker對象,而不是各自引入自己的Worker對象副本(參見圖14.5)。因為SingingWaiter現在只包含了一個Worker子對象,所以可以使用多態。

圖14.5 虛基類繼承

您可能會有這樣的疑問:

  • 為什么使用術語“虛”?
  • 為什么不拋棄將基類聲明為虛的這種方式,而使虛行為成為多MI的準則呢?
  • 是否存在麻煩呢?

首先,為什么使用術語虛?畢竟,在虛函數和虛基類之間并不存在明顯的聯系。C++用戶強烈反對引入新的關鍵字,因為這將給他們帶來很大的壓力。例如,如果新關鍵字與重要程序中的重要函數或變量的名稱相同,這將非常麻煩。因此,C++對這種新特性也使用關鍵字virtual——有點像關鍵字重載。

其次,為什么不拋棄將基類聲明為虛的這種方式,而使虛行為成為MI的準則呢?第一,在一些情況下,可能需要基類的多個拷貝;第二,將基類作為虛的要求程序完成額外的計算,為不需要的工具付出代價是不應當的;第三,這樣做有其缺點,將在下一段介紹。

最后,是否存在麻煩?是的。為使虛基類能夠工作,需要對C++規則進行調整,必須以不同的方式編寫一些代碼。另外,使用虛基類還可能需要修改已有的代碼。例如,將SingingWaiter類添加到Worker集成層次中時,需要在Singer和Waiter類中添加關鍵字virtual。

2.新的構造函數規則

使用虛基類時,需要對類構造函數采用一種新的方法。對于非虛基類,唯一可以出現在初始化列表中的構造函數即是基類構造函數。但這些構造函數可能需要將信息傳遞給其基類。例如,可能有下面一組構造函數:

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類的構造函數只能調用B類的構造函數,而B類的構造函數只能調用A類的構造函數。這里,C類的構造函數使用值q,并將值m和n傳遞給B類的構造函數;而B類的構造函數使用值m,并將值n傳遞給A類的構造函數。

如果Worker是虛基類,則這種信息自動傳遞將不起作用。例如,對于下面的MI構造函數:

SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other): Waiter(wk,p), Singer(wk,v) {} // flawed

存在的問題是,自動傳遞信息時,將通過2條不同的途徑(Waiter和Singer)將wk傳遞給Worker對象。為避免這種沖突,C++在基類是虛的時,禁止信息通過中間類自動傳遞給基類。因此,上述構造函數將初始化成員panache和voice,但wk參數中的信息將不會傳遞給子對象Waiter。然而,編譯器必須在構造派生對象之前構造基類對象組件;在上述情況下,編譯器將使用Worker的默認構造函數。

如果不希望默認構造函數來構造虛基類對象,則需要顯式地調用所需的基類構造函數。因此,構造函數應該是這樣:

SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other): Worker(wk), Waiter(wk,p), Singer(wk,v) {}

上述代碼將顯式地調用構造函數worker(const Worker &)。請注意,這種用法是合法的,對于虛基類,必須這樣做;但對于非虛基類,則是非法的。

警告: 

如果類有間接虛基類,則除非只需使用該虛基類的默認構造函數,否則必須顯式地調用該虛基類的某個構造函數。

本文摘自《C++ Primer Plus(第6版)中文版》

本書在介紹C++特性的同時,還討論了基本C語言,使兩者成為有機的整體。書中介紹了C++的基本概念,并通過短小精悍的程序來闡明,這些程序都很容易復制和試驗。書中還介紹了輸入和輸出,如何讓程序執行重復性任務,如何讓程序做出選擇,處理數據的多種方式,以及如何使用函數等內容。另外,本書還講述了C++在C語言的基礎上新增的很多特性,包括:

  • 類和對象;
  • 繼承;
  • 多態、虛函數和RTTI(運行階段類型識別);
  • 函數重載;
  • 引用變量;
  • 泛型(獨立于類型的)編程,這種技術是由模板和標準模板庫(STL)提供的;

 處理錯誤條件的異常機制;

  • 管理函數、類和變量名的名稱空間。

?

總結

以上是生活随笔為你收集整理的一起来学C++:C++中的代码重用的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。