--- title: 'C++ 繼承變形 & 模板' disqus: kyleAlien --- C++ 繼承變形 & 模板 === ## OverView of Content 如有引用參考請詳註出處,感謝 :smile: 以下會說明私有繼承、保護繼承、樣板類別 [TOC] ## 私有繼承 Has-a 關係 在繼承類別中有提到 [**Has-a 關係**](https://hackmd.io/oM3IAtEVSCSn5Qh_ve6nOw#依賴-has-a) 有兩種方是可達成,各有各地訪問訪法,要特別注意使用特色規則 :::success OOP UML 來說就是 **聚合** 的概念 ::: ### 私有成員 (顯式) * 將需要的類別加在 private 區塊,**作為私有成員變數,在內部就可以直接使用** :::info * **將關係類變為私有成員變數** 1. **初始化列表**使用 **++成員名稱++** > 符號 `:`,後續還有要初始化就使用 `,` 串接 2. Has-a **++有++ 物件名稱** 3. 使用**成員問算子訪問成員方法** ::: 1. 宣告 Class:並在 `private` 區塊創建 (聚合) 私有成員 `valarray<double>` ```cpp= /** * 宣告 */ #include <iostream> #include <string> #include <valarray> using namespace std; class newString { private: typedef std::valarray<double> valArray; // 聚合 valarray 物件 string name; valArray score; ostream& showScore(ostream& o) const; public: newString() : name("Null Name"), score() {} newString(const newString &nStr); // 複製建構函數 newString(const string &str, int count) : name(str), score(count) {} newString(const string &str, valArray &va) : name(str), score(va) {} newString(const char* c, const double *val, int count) : name(c), score(val, count) {} // 規定顯示建構函數 (避免指定建構函數,也就是單一參數匹配) explicit newString(const string& str) : name(str), score() {} explicit newString(int count) : name("Null Name"), score(count) {} virtual ~newString(); double average() const; // 平均值 const string& getName() const { // inline return this->name; } double &operator[] (int index); double operator[] (int index) const; friend ostream& operator << (ostream& o, const newString& nstr); }; ``` * 使用 `explicit` 關鍵字 [**關閉類別自動轉換**](https://hackmd.io/Roh5BF4MRN2YPgmZZ7c3Sw?both#explicit-顯式型態),**限制部分操作,比起編譯期間出錯要好** * 使用內嵌函數作為建構子,**並使用初始化列表**,初始化內部成員,初始化列表在**公開繼承也可以使用,==並且初始化列表 ++只作用在建構函數++==** > 一般函數不可用初始化列表 :::warning - 元素建構順序 ? 初始化列表建構順序,**順序是按照 ++宣告變數的順序++** 而建構的,不會依照初始化列表的順序 ```java= string name; // 第一建構 valArray score; // 第二建構 ``` ::: 2. 定義類函數實現 ```cpp= #include "newString.h" newString::newString(const newString &nStr) { this->name = nStr.name; this->score = nStr.score; } double newString::average() const { int size = score.size(); if(size <= 0) { return 0; } return score.sum() / size; } double& newString::operator[] (int index) { // 返回可操作值 return score[index]; } double newString::operator[] (int index) const { // read-only return score[index]; } ostream& newString::showScore(ostream& o) const { int size = score.size(); if(size <= 0) { o << "Empty score list."; } else { for(int i = 0; i < size; i++) { o << "score[" << i << "]= " << score[i] <<endl; } } return o; } ostream& operator << (ostream& o, const newString& nStr) { nStr.showScore(o) << "Name : " << nStr.name << endl; return o; } newString::~newString() { cout << "new String deconstrucror: " << this->name << endl; } ``` * 透過 **覆寫 `operator[]` 並++返回 array 原型的 reference++**,就可以直接修改 array 內部資料,以這個例子來說就是呼叫 valArray 的成員函數 valArray<double\>::operator[](),在返回 reference double * 返回的若為 double 類型,代表它並沒有辦法改變 array,just can read-only 3. 測試使用 ```cpp= int main() { const static int count = 4; const static int val[4] = {90, 61, 72, 33}; newString s[count] = { // 第二個參數是指定內部 array 大小 newString(string("Alien"), 4), newString(string("Pan"), 2), newString(string("Kyle"), 3), newString(string("Hello"), 4), }; for(int j = 0; j < count; j++) { newString &cache = s[j]; for(int i = 0; i < count; i++) { cache[i] = val[i]; } } for(int i = 0; i < count; i++) { cout << s[i]; cout << "Average: " << s[i].average() << "\n" << endl; } } ``` > ![](https://i.imgur.com/wYLgahe.png) ### 私有繼承類 (隱式) * 私有繼承會將基類的所有成員都繼承 (包括私有),但是當**使用此類時,並無法使用到基類的成員**,當沒有指定為 `public` 繼承時,**++==預設為 private 繼承==++** :::info * **使用 private 繼承的特色在 **==只獲取實作,不獲取界面==**;class 後使用 `:` 繼承類** :::warning C++ 支援多繼承,符號 `:`,後續還有要初始化就使用 `,` 串接 ::: 1. **初始化列表**使用 **++類別名稱++** > 符號 `:`,後續還有要初始化就使用 `,` 串接 2. Has-a **++沒有++ 物件名稱** 3. 使用 **++範疇運算子++訪問匿名成員方法** 4. 新類別並不需要私有區段 ::: 1. 宣告抽象類,並繼承 (可多繼承) 需要的類別,這些繼承的類別稱為 **隱式成員** ```cpp= /** * 宣告 */ #include <iostream> #include <string> #include <valarray> // 坑... 沒有 include 無法使用 using namespace std; class PrivateScore : private string, private valarray<double> { private: typedef std::valarray<double> valArray; public: PrivateScore() : string("NULL"), valarray() {}; PrivateScore(const PrivateScore & u); PrivateScore(const string &str, int i) : string(str), valArray(i) {}; PrivateScore(const string &str, valArray &a) : string(str), valarray(a) {}; PrivateScore(const char *str, const double *val, int count) : string(str), valArray(val, count) {}; explicit PrivateScore(const string& str) : string(str), valarray() {}; explicit PrivateScore(int i) : string("NULL"), valArray(i) {}; virtual ~PrivateScore(); double average() const; const string& getName() const { return (const string&) *this; }; ostream& showScore(ostream& o) const; // operator double& operator[](int index); double operator[](int index) const; // read-only // friend friend std::ostream& operator<<(std::ostream& o, const PrivateScore& s); }; ``` * 初始化串列 **==使用 class 類名來初始化==** 2. 實現類方法 ```cpp= #include "PrivateScore.h" double PrivateScore::average() const { int size = this->valarray::size(); if(size <= 0) { return 0; } return this->valarray::sum() / size; } double& PrivateScore::operator[](int index) { // 必須使用範疇運算子 return this->valarray::operator [](index); } double PrivateScore::operator[](int index) const { // 必須使用範疇運算子 return this->valarray::operator[](index); } ostream& PrivateScore::showScore(ostream& o) const { int size = this->valarray::size(); if(size <= 0) { o << "Empty score list."; } else { for(int i = 0; i < size; i++) { o << "score[" << i << "]= " << this->valarray::operator[](i) << endl; } } return o; } ostream& operator << (ostream& o, const PrivateScore& nStr) { // 將累型態轉換再使用 nStr.showScore(o) << "Name : " << (const string&) nStr << endl; return o; } PrivateScore::~PrivateScore() { // 將累型態轉換再使用 cout << "PrivateScore deconstructor." << (string &) *this << endl; } ``` * 由於是私有繼承,class 成員會被隱藏,**匿名成員物件要使用時要透過 ++型態轉換++,顯示指定要轉型的類** > 成員函數可以使用範疇運算子,但是 **friend 函式並非成員函數,所以必須透過 ++型態轉換++** * 對於 **私有繼承成員要使用其函數必須透過 ++範疇運算子++ 呼叫** 3. 測試 ```cpp= int main() { typedef std::string string; cout << "Hello World" << endl; const static int count_1 = 4; const static int val_2[4] = {90, 61, 72, 33}; PrivateScore s[count_1] = { PrivateScore(string("Alien"), 4), PrivateScore(string("Pan"), 2), PrivateScore(string("Kyle"), 3), PrivateScore(string("Hello"), 4), }; for(int j = 0; j < count_1; j++) { PrivateScore &cache = s[j]; for(int i = 0; i < count_1; i++) { cache[i] = val_2[i]; } } for(int i = 0; i < count_1; i++) { cout << s[i]; cout << "Average: " << s[i].average() << "\n" << endl; } return 0; } ``` > ![](https://i.imgur.com/MSApIv9.png) ### 私有成員 & 私有繼承 * 上數個兩個方法,大部分會選用 `私有成員`,主要原因有 :::warning 1. 因為私有繼承太過於抽象,不易理解,總是要轉型 2. **繼承限制++單一物件++** (無法繼承兩個 valarray),無法同時宣告多個同樣物件來使用,而私有成員就可以 ::: * **`私有繼承`仍有 `私有成員` 所沒有的特質** :::info 1. **可以存取 protected 成員**,protected 允許衍生類在內 2. **私有函數可已重新定義 [++虛擬函數++](https://hackmd.io/oM3IAtEVSCSn5Qh_ve6nOw#同名異式的公用繼承)**,而私有成員不行重新定義虛擬函數 ::: ## 保護繼承 **保護類在第三代的繼承仍可使用 ++第一代++ 的 private 成員**,**但在 ==protected 可以在之後繼承都可使用 ++第一代++ 的 private 成員==** ```cpp= class ProtectedScore : protected std::string, protected std::valarray<double> { ... } ``` ### Using 重新定義存取方式 * 使用保護繼承,可以使用 `private`、`protected` 保護成員,當需要使用到基類的函數時 **通常是使用定義一個 public function 讓使用者使用**,但還有另外一個方法,**using 宣告** * 用 [**using 宣告**](https://hackmd.io/uf-tHfltRVCV45D1h6ZiGA#using-宣告-amp-using-指令) 可讓衍生類的使用者直接使用到基類函數,**必須將 using 宣告定義在 public 區塊** :::danger * **==using 宣告的方法 ++只能用於繼承++==,++不能用於私有成員++** ::: :::info * **using 宣告指++使用成員名稱++,沒有括號、函數簽名** ::: 1. **抽象宣告**:同時在這邊對外 **使用 using 宣告要公開的函數** ```cpp= #include <iostream> #include <string> #include <valarray> using namespace std; class ProtectedScore : protected string, protected valarray<double> { private: typedef std::valarray<double> valarray; protected: using valarray::max; public: using valarray::min; ProtectedScore(const string &str, int count) : string(str), valarray(count) {}; ProtectedScore(); virtual ~ProtectedScore(); double average() const; ostream& showScore(ostream& o) const; // operator double& operator[](int index); double operator[](int index) const; // read-only // friend friend std::ostream& operator<<(std::ostream& o, const ProtectedScore& s); }; ``` * using 宣告 **放置在 class public 區塊**,宣告時只包含函式的名稱 (沒有括弧、引數...等等) * **私有、保護繼承透過 using 宣告也可以使用基類的函式** 2. Class 定義:這部分跟之前差不多,但是在內部呼叫了 protect 的 max 方法 ```cpp= #include "ProtectedScore.h" double ProtectedScore::average() const { int size = this->valarray::size(); if(size <= 0) { return 0; } return this->valarray::sum() / size; } double& ProtectedScore::operator[](int index) { return this->valarray::operator [](index); } double ProtectedScore::operator[](int index) const { return this->valarray::operator[](index); } ostream& ProtectedScore::showScore(ostream& o) const { int size = this->valarray::size(); if(size <= 0) { o << "Empty score list."; } else { for(int i = 0; i < size; i++) { o << "score[" << i << "]= " << this->valarray::operator[](i) << endl; } } return o; } ostream& operator << (ostream& o, const ProtectedScore& nStr) { nStr.showScore(o) << "Name : " << (const string&) nStr << endl; // 呼叫 protected 的 max 方法 cout << "Max:" << nStr.max() << endl; return o; } ProtectedScore::~ProtectedScore() { cout << "ProtectedScore deconstructor." << (string &) *this << endl; } ``` 3. **測試**:外部可以使用 using 宣告的函數 (min) ```cpp= int main() { typedef std::string string; cout << "Hello World" << endl; const static int count_1 = 4; const static int val_2[4] = {90, 61, 72, 33}; ProtectedScore s[count_1] = { ProtectedScore(string("Alien"), 4), ProtectedScore(string("Pan"), 2), ProtectedScore(string("Kyle"), 3), ProtectedScore(string("Hello"), 4), }; for(int j = 0; j < count_1; j++) { ProtectedScore &cache = s[j]; for(int i = 0; i < count_1; i++) { cache[i] = val_2[i]; } } for(int i = 0; i < count_1; i++) { cout << s[i]; cout << "Average: " << s[i].average() << endl; cout << "Min:" << s[i].min() << endl << "\n"; } return 0; } ``` > ![](https://i.imgur.com/c8AYjWD.png) ### 繼承的整理 | 成員 | 公用 public 繼承 | 保護 protected 繼承 | 私有 private 繼承 | | -------- | -------- | -------- | -------- | | 公用成員 | 衍生類可用 | 衍生類別的保護成員 | 衍生類的私有 | | 保護成員 | 衍生類可用 | 衍生類別的保護成員 | 衍生類的私有 | | 私有成員 | 只能透過 Function 訪問 | 只能透過 Function 訪問 | 只能透過 Function 訪問 | | 隱式向上轉型 | OK | OK (限於衍生類中) | NO | ## 多重繼承 (multiple inheritance, MI) **==多重繼承表示 is-a 的關係==,is-a 關係是 public 公開繼承關係,has-a 是 private、protected 關係 (因為外部不可以使用繼承的基類)** > ![](https://i.imgur.com/AJ3AKhZ.png) :::warning * 問題 上圖會產生的問題是,**繼承兩個衍生類別會繼承到兩個相同名稱的函數** > 可能 `Singer` 有 name,而 `Waiter` 也有 name 函數,那 `SingWaiter` 呼叫 name 時就會衝突 ::: ### 基礎繼承 * 先創建一個基本繼承,再來看看多重繼承有相同函數的問題、解決方法 1. **基礎基類** ```cpp= /** * 宣告 */ #include <string> using std::string; #include <iostream> using std::cout; using std::endl; class Worker { private: string name; long id; public: Worker(long n = 0, string s = "NULL") : name(s), id(n) {}; Worker(const string & s, long n) : name(s), id(n) {}; // 純虛寒數 virtual ~Worker() = 0; // 虛擬函數 virtual void Set(); virtual void Show() const; }; // ----------------------------------------------------------------------- using std::cin; // Worker class Worker::~Worker() {} // must implements, even if pure ! void Worker::Set() { cout << "Enter Worker's name: "; getline(cin, name); cout << "Enter Worker's id: "; cin >> id; while(cin.get() != '\n') continue; // 換行才結束 } void Worker::Show() const { cout << "name: " << name << endl; cout << "id: " << id << endl; } ``` 2. **創建兩個衍生類** (Waiter、Singer) ```cpp= class Waiter : public Worker { // public 公用繼承 private: int age; public: Waiter() : Worker(), age(18) {} Waiter(const Worker & w, int g = 18) : Worker(w), age(g) {} Waiter(const string& s, long n, int g = 18) : Worker(s, n), age(g) {} // 實現虛擬函數 void Set(); void Show() const; }; class Singer : public Worker { // public 公用繼承 protected: enum { other, alto, contralto, soprano, bass, baritone, tenor, total }; private: static char* pv[total]; int voice; public: Singer() : Worker(), voice(other){} Singer(const Worker & w, int v = other) : Worker(w), voice(other){ } Singer(const string& s, long n, int v = other) : Worker(s, n), voice(other){ } // 實現虛擬函數 void Set(); void Show() const; }; // -------------------------------------------------------------------------- // Waiter class void Waiter::Set() { // 使用範疇運算子指定 Worker::Set(); cout << "Enter Waiter age: "; cin >> age; while(cin.get() != '\n') continue; } void Waiter::Show() const { // 使用範疇運算子指定 Worker::Show(); cout << "age: " << age << endl; } // Singer class char * Singer::pv[total] = {"other", "alto", "contralto", "soprano", "bass", "baritone", "tenor"}; void Singer::Set() { // 使用範疇運算子指定 Worker::Set(); cout << "Enter number for singer's vocal range:" << endl; for(int i = 0; i < total; i++) { cout << i << ": " << pv[i] << " "; // 顯示所有選項 } cout << endl; cin >> voice; while(cin.get() != '\n') continue; } void Singer::Show() const { // 使用範疇運算子指定 Worker::Show(); cout << "voice: " << pv[voice] << endl; } ``` * 上面清楚定義類型 (使用 `::` 範疇運算子),所以再呼叫 Function 時不會有模糊性,但是**如果定義一個多重繼承的 WaiterSinger,就會導致 ++WaiterSinger 內部有多個定義++**,帶這個問題往下看多重繼承 3. 測試: ```cpp= /** * 使用 */ #include "Worker.h" int main() { static const int max = 4; Waiter bob("Bob", 314L, 23); // name, id, age; Singer alien("Alien", 523L, 6); Waiter w_temp; // 坑...不能有括弧 w_temp() Singer s_temp; Worker * ws[max] = { &bob, &alien, &w_temp, &s_temp }; for(int i = 2; i < max; i++) { ws[i]->Set(); // 設定後面兩個 } for(int i = 0; i < max; i++) { ws[i]->Show(); cout << endl; } return 0; } ``` **--實作--** > ![](https://i.imgur.com/cViKsga.png) ### 多重繼承 - 問題 * 在建立 `WaiterSinger` 時,其 **真正的問題是為有 ++兩份 Worker 物件++**,因為 `WaiterSinger` constructor 會呼叫兩個基類 `Waiter` & `Singer`,這時這 **兩個基類又會建立個別的 Worker** 這時再呼叫 `WaiterSinger` function (Show、Set) 就會造成模糊問題 > ![](https://i.imgur.com/XURl4gc.png) ### 虛擬基類 virtual - 解決方案 * **虛擬基類可以使此物件 ++只繼承++ ==共同基礎類別的++一個物件++== (以上面來說就是 `WaiterSinger` 只會有一個 `Worker` 物件)** * 在 `Singer` & `Waiter` **繼承時使用關鍵字 virtual**,放置不分前後,都是相同效果 ```cpp= // 虛擬碼 // 以下兩種寫法都可以 class Singer : virtual public Worker {} class Waiter : public virtual Worker {} // 多重繼承類 class SingerWaiter : public Singer, public Waiter {} ``` > ![](https://i.imgur.com/WZC1c9d.png) * **在使用抽象衍生類時 (`Singer`、`Waiter`) 要特別注意==建構函數==,中間類並不會呼叫基類建構函數;就像在 `Waiter` constructor 並不會調用 `Worker` constructor,==必須明確指定基類的建構函數==** ```cpp= SingerWaiter::SingerWaiter(const Worker& w, int a = 18, int v = Singer::other) : Waiter(w,a), Singer(w,v) { } // 並不會傳達至基類 Worker SingerWaiter::SingerWaiter (const Worker& w, int a = 18, int v = Singer::other) : Work(w), // 明確指定,會傳至基類 Waiter(w,a), Singer(w,v) { } ``` * 接下來要考慮 **要使用哪一個函數**,因為如果**使用 *繼承呼叫* 時就會呼叫到兩次基類 `Worker`,使用 ==模組化== 就可以避免這個問題 (++細分資料內容++),並將該資料放置在 `protected` 保護,只讓內部類訪問** * 將上點的範例做修改,主要修改的地方有 1. **基類模組化,把需要函數放置 `protected` 區塊** ```cpp= class Worker { private: string name; long id; protected: // 模組定義在 protected 區塊,限制只給內部使用 virtual void Data() const; virtual void Get(); public: Worker(long n = 0, string s = "NULL") : name(s), id(n) {}; Worker(const string & s, long n) : name(s), id(n) {}; virtual ~Worker() = 0; // 轉變為純虛擬函數 (子類必須實現) virtual void Set() = 0; virtual void Show() const = 0; }; ``` 2. **虛擬化繼承共通的類 virtual public**:它的特色是如果有使用到多繼承,其內部就會只有一個共同基類 (以目前來說就是 `Worker`) ```cpp= class Waiter : virtual public Worker { // 虛擬類 - 繼承 private: int age; protected: void Data() const; void Get(); public: Waiter() : Worker(), age(18) {} Waiter(const Worker & w, int g = 18) : Worker(w), age(g) {} Waiter(const string& s, long n, int g = 18) : Worker(s, n), age(g) {} void Set(); void Show() const; }; // ---------------------------------------------------------------------- class Singer : public virtual Worker { // 虛擬類 - 繼承 protected: enum { other, alto, contralto, soprano, bass, baritone, tenor, total }; void Data() const; void Get(); private: static char* pv[total]; int voice; public: Singer() : Worker(), voice(other){} Singer(const Worker & w, int v = other) : Worker(w), voice(other){} Singer(const string& s, long n, int v = other) : Worker(s, n), voice(other){} void Set(); void Show() const; }; ``` 3. 在多重繼承類的 constructor **顯式指定基類 constructor** ```cpp= // 多重繼承虛擬類 (virtual class) class SingerWaiter : public Singer, public Waiter { protected: void Data() const; void Get(); public: SingerWaiter() : Worker(), // 顯式指定 Waiter(), Singer() {} SingerWaiter(const string& s, long n, int a = 0, int v = Singer::other) : Worker(s, n), // 顯式指定 Waiter(s, a), Singer(s, n, v) {} SingerWaiter(const Worker& w, int a = 0, int v = Singer::other) : Worker(w), // 顯式指定 Waiter(w, a), Singer(w, v) {} SingerWaiter(const Waiter& wt, int v = Singer::other) : Worker(wt), Waiter(wt), Singer(wt, v) {} SingerWaiter(const Singer& s, int a = 18) : Worker(s), Waiter(s, a), Singer(s) {} void Set(); void Show() const; }; ``` 4. 模組化基礎、衍生類別的資料,才能分開調用 ```cpp= #include "Worker.h" // Worker class 移除 Show & Set 的定義,改為定義模組 Worker::~Worker() {} // must implements, even if pure ! void Worker::Data() const { cout << "name: " << name << endl; cout << "id: " << id << endl; } void Worker::Get() { cout << "Enter Worker's name: "; getline(cin, name); cout << "Enter Worker's id: "; cin >> id; while(cin.get() != '\n') continue; // 換行才結束 } // ------------------------------------------------------------------------- // Waiter class void Waiter::Data() const { cout << "age: " << age << endl; } void Waiter::Get() { cout << "Enter Waiter age: "; cin >> age; while(cin.get() != '\n') continue; } void Waiter::Set() { // 先呼叫基類 Get Worker::Get(); // 再呼叫自身的 Get Get(); } void Waiter::Show() const { // 先呼叫基類 Show Worker::Data(); // 再呼叫自身的 Show Data(); } // ------------------------------------------------------------------------- // Singer class char * Singer::pv[total] = {"other", "alto", "contralto", "soprano", "bass", "baritone", "tenor"}; void Singer::Data() const { cout << "voice: " << pv[voice] << endl; } void Singer::Get() { cout << "Enter number for singer's vocal range:" << endl; for(int i = 0; i < total; i++) { cout << i << ": " << pv[i] << " "; // 顯示所有選項 } cout << endl; cin >> voice; while(cin.get() != '\n') continue; } void Singer::Set() { Worker::Get(); Get(); } void Singer::Show() const { Worker::Data(); Data(); } // SingerWaiter class void SingerWaiter::Data() const { // 模組化 Singer::Data(); Waiter::Data(); } void SingerWaiter::Show() const { Worker::Data(); // 只呼叫了一次基類 Data(); } void SingerWaiter::Get() { // 模組化 Singer::Get(); Waiter::Get(); } void SingerWaiter::Set() { Worker::Get(); // 只呼叫了一次基類 Get(); } ``` 5. 使用 ```cpp= /** * 使用 */ #include "Worker.h" #include <cstring> int main() { static const int max = 5; Worker *ws[max]; int i = 0; for(i = 0; i < max; i++) { char choice; cout << "Enter the employee category: \n" << "w: waiter, s: singer, t: singer_waiter, q: quit" << endl; cin >> choice; while(strchr("wstq", choice) == NULL) { // 比對字串 cout << "Enter: " << endl; cin >> choice; } if(choice == 'q') { cout << "Quit\n"; break; } switch(choice) { // 動態,新增物件 case 'w': cout << "Add one waiter\n"; ws[i] = new Waiter; break; case 's': cout << "Add one singer\n"; ws[i] = new Singer; break; case 't': cout << "Add one singer_waiter\n"; ws[i] = new SingerWaiter; break; } cin.get(); ws[i]->Set(); // 設定物件 } cout << "Here is your employ info:\n"; for(int j = 0; j < i; j++) { cout << endl; ws[j]->Show(); } for(int j = 0; j < i; j++) { delete ws[j]; // 刪除動態區 heap 記憶體 } return 0; } ``` * 上面使用動態創建,由於不知道使用者要選擇的類,所以使用動態創建類 (存放在 heap 區塊),所以必須記得刪除 **--實作結果--** > ![](https://i.imgur.com/BHnA8Mw.png) ## C++ 模板 **模板撿來說就是,==參數化型態==**,其關鍵字就是 **`template<class Type>`,Type 的位子可以任意的通用型態名稱** :::info template 中的 class 是較舊的用法,可能會導致他人誤以為 class T 為一個類別,但其實並不是 **新的用法是使用 `typename`** > template<++typename++ Type\> ::: ### 定義 class 樣板 * 將 template 定義在 class 之上,**模板 class 並不算一個類,它只告訴編譯器要++如何生成類別++、++成員函數定義++,實現樣板稱為實體化 (instantiation)、持定化 (specialization)** * **==樣板必須與 [特定樣板](https://hackmd.io/7Csy-1LVTouQFl2e2sBvtA#%E7%89%B9%E5%AE%9A%E5%8C%96%E6%A8%A1%E6%9D%BF---explicit-%E9%A1%AF%E7%A4%BA) 實體化的程式碼 ++放在一起++== 否則不能運作** :::warning **除非使用特定的 export (但 C++11 已停止使用) 否則 ++模板必須與實作放置++ 在一起,最好的方式就是一同放置標頭檔 (`.h` 檔)** ::: 1. 模板 (泛型) 化 Class 類 ```cpp= template<typename T> class TemplateStack { private: enum {MAX = 10}; int index; T List[MAX]; // 模板類 T public: TemplateStack() : index(0) { } bool isEmpty() const; bool isFull() const; // 模板類 T bool push(T t); bool pop(T& t); virtual ~TemplateStack() { } }; ``` 2. 實作 Class 的模板函數 ```cpp= #include <iostream> using std::cout; using std::endl; template<typename T> bool TemplateStack<T>::isEmpty() const { return index == 0; } template<typename T> bool TemplateStack<T>::isFull() const { return index == MAX; } template<typename T> bool TemplateStack<T>::push(T t) { if(isFull()) { cout << "Stack full, cannot push" << endl; return false; } else { List[index++] = t; return true; } } // template<class & typename> 相同都可以使用 template<class T> bool TemplateStack<T>::pop(T &t) { if(isEmpty()) { cout << "Stack Empty, cannot pop" << endl; return false; } else { t = List[--index]; return true; } } ``` 3. 使用模板類 ```cpp= using std::string; using std::cin; int main() { TemplateStack<int> stack1; int i = 10; stack1.push(i); int res = -1; stack1.pop(res); cout << "res: " << res << endl; string str; char ch; TemplateStack<string> stack2; while(cin >> ch && ch != 'Q') { switch(ch) { case 'A': cout << "type input : "; cin >> str; if(stack2.isFull()) { goto FIN; } else { stack2.push(str); } break; case 'P': if(stack2.isEmpty()) { goto FIN; } else { stack2.pop(str); cout << "pop: " << str << endl; } break; } } FIN: while(!stack2.isEmpty()) { string str; stack2.pop(str); cout << "str: " << str << endl; } cout << "Bye" << endl; return 0; } ``` * 實體化的方式是宣告樣板型態的物件,且**將通用名稱替換成需要的型態** > 函數 : 編譯程式會用傳入函數的引數型態,決定要產生何種型態的函數 > > ![](https://i.imgur.com/IES47u3.png) ### 樣板 - 指標堆疊、動態創建 new * 如果說上一個範例不使用 `string` 類,而是使用 `char *` 指標呢 ? 也是 **可以產生指標堆疊,但是要 ++注意指標位置是否是相同++** :::warning 1. 將 `string` 換成 `char*` 這方法會立即失敗 ! > 因為使用 cin 輸入必須要一個實例化的物件 2. 將 `string` 換成 `char *str[40];` 在 pop function 時會錯誤 > `pop` 接收的參數為 reference 必須參考某些型態的左值,而非陣列名稱,並且在 reference 賦值時不能賦予陣列單個數值 3. 將 `string` 為 `char *str = new char str[40];` 這就符合 pop 的標準,它是一個指標,可間接改值,但還是不行 > 因為每次 pop 時都會丟入同一個地址,地址並沒有更新,會導致 pop 最後一個存入的字串 ::: * 以下使用 **new 關鍵字** 去創建 Array,這種動態創建要注意三個點 :::success * 如果你的類會與記憶體中的「**堆**」有關係,則須注意... * 解構函數,在解構函數要釋放 **堆** 的記憶體 * **[複製函數](https://hackmd.io/rYFShCgjTHCcKsaNneFfmA?both#淺拷貝-amp-深拷貝),要去拷貝並創建一個 堆 空間** * **[指定操作符](https://hackmd.io/rYFShCgjTHCcKsaNneFfmA?both#指定運算子---顯式深層拷貝) (`=`),==刪除原先的 堆 空間、創建新的 堆 空建==、並拷貝其資料** ::: 1. 抽象像宣告 ```cpp= #include <iostream> using std::cout; using std::endl; template<typename T> class DynamicStack { private: enum {DEF_SIZE = 5}; T *t; int size, index; void copyArray(const DynamicStack &d); public: DynamicStack(int s = DEF_SIZE) : size(s), index(0) { t = new T[size]; } // 隱式拷貝函數 DynamicStack(const DynamicStack<T> & d); // 顯示拷貝符號 DynamicStack<T> & operator=(const DynamicStack & d); // DynamicStack 等於 DynamicStack<T> bool isEmpty() const { return index == 0; } bool isFull() const { return index == size; } bool push(T t); bool pop(T &t); virtual ~DynamicStack() { delete[] t; } }; ``` * **可以在樣板宣告 & 樣板函數中(實體化) 可以使用 ++沒有 DynamicStack<T\>++ 的 DynamicStack 類** 2. 類實現 ```cpp= #include "DynamicStack.h" template<typename T> // 隱式深層拷貝 copy construct DynamicStack<T>::DynamicStack(const DynamicStack<T> & d) : index(0) { this->size = d.size; this->t = new T[d.size]; copyArray(d); } //"2. " template<typename T> // 顯示深層拷貝 = 指定函數 DynamicStack<T> & DynamicStack<T>::operator=(const DynamicStack<T> & d) { if(this == &d) { return *this; } delete[] this->t; this->size = d.size; this->t = new T[this->size]; copyArray(d); return *this; } template<typename T> bool DynamicStack<T>::push(T iT) { if(isFull()) { return false; } else { t[index++] = iT; return true; } } template<typename T> bool DynamicStack<T>::pop(T &oT) { if(isEmpty()) { return false; } else { oT = t[--index]; return true; } } template<typename T> // copy ptr in array void DynamicStack<T>::copyArray(const DynamicStack &d) { for(int i = 0; i < size; i++) { this->t[i] = d.t[i]; } } ``` * **在類別之外,包括 ==標示回傳型態==、==使用範疇運算子== 時都需要 ++完整的加上模板 DynamicStack<T\>++** 3. 測試 ```cpp= int main() { static const int NUM = 10; const char* in[NUM] = { // 要存入元素 "Apple", "Box", "Car", "Dex_File", "Elements", "Fork", "Game", "Hello", "Illegal", "Joke" }; DynamicStack<const char*> stack(NUM); // <const char*> != <char*> const char* out; // 輸出元素 while(!stack.isFull()) { static int i = 0; stack.push(in[i++]); } while(!stack.isEmpty()) { static int i = 0; stack.pop(out); cout << i++ <<" out: " << out << endl; } return 0; } ``` > ![](https://i.imgur.com/KmcHN1Y.png) ### 樣板 - 非型態引數 * 通常模板的功能是參數化類別型態,這是因為**型態參數的概念**,而**非型態引數是使用++特定的型態++**,看看以下例子 * 非型態又稱為**運算式引數**,**它有一定的++限制++、++特點++(下面程式第 3 點)** > ![](https://i.imgur.com/tAnEYET.png) ### 遞迴樣板 * **==樣板中可以再有樣板==**,這樣就如同一個二維陣列,如同 `TpArray<TpArray<double, 5>, 3> twodee;` **與二維陣列不同的是它的 ++順序相反++** ```cpp= // 普通二維 int normal2Array[5][3]; // 5 個內部各有 3 個元素的陣列 // 模板二維 TpArray<TpArray<double, 5>, 3> twodee; // 3 個內部各有 5 個元素的陣列 ``` * 創建一個 5 次分數、記錄 3 次,並**使用 ++遞迴 template++ 創建** ```cpp= /** * 使用 */ #include "templateArray.h" int main() { TpArray<double, 5> sum; // 5次 總和 TpArray<double, 5> aves; // 5次 平均 //"1. " TpArray<TpArray<double, 5>, 3> twodee; // 存放 5 次的成績,總共存 3 次 int i = 0, j = 0; for(; i < 3; i++) { sum[i] = 0; for(j = 0; j < 5; j++) { twodee[i][j] = (i + 1) * (j + 1); //"2. " sum[i] += twodee[i][j]; } aves[i] = sum[i] / 5; } i = j = 0; for(; i < 3; i++) { for(j = 0; j < 5; j++) { cout << twodee[i][j] << " "; } cout << " -> Sum: " << sum[i]; cout.width(3); // 小數點三位 cout << " -> Average: " << aves[i] << endl; } return 0; } void basic() { TpArray<double, 3> a1; TpArray<double, 5> a2; for(int i = 0; i < 3; i++) { a1[i] = 3.69 + i; cout << a1[i] << " "; } cout << endl; for(int i = 0; i < 3; i++) { a2[i] = 1.23 + i; cout << a2[i] << " "; } } ``` 1. **創建遞迴樣板相當於一個夠多維度的陣列**,以這個範例來說就是**使用遞迴模板創建了 2 維陣列** 2. **==可直接使用陣列的方式賦予值==** (創建時順序式相反的,如果使用陣列來接收應該是 int twodee[3][5]) **--實做--** > ![](https://i.imgur.com/j9Qr5C0.png) ### 多個類型參數 * **如同 [Java 泛型](https://hackmd.io/Dlm0Ov0URya2t6mbTgNQkQ#泛型類、接口、方法),可以有多個參數化類型**,`tmplate<class T1, class T2, Class T3\>`,STL (stander Template Library) 內部也有一個 pair 類似於 Java 的 Map ```cpp= #include <iostream> using std::cout; using std::endl; //"1. " template<typename T1, class T2> class HandlerPair{ private: T1 key; T2 value; public: HandlerPair(const T1 & t1, const T2 & t2) : key(t1), value(t2) {} T1 & getKey() { return key; } T2 & getValue(); }; //"2. " template<class T1, typename T2> T2 & HandlerPair<T1, T2>::getValue() { return value; } int main() { using std::string; HandlerPair<string, double> infos[] = { HandlerPair<string, double>("Alien", 84.3), HandlerPair<string, double>("Kyle", 90.3), HandlerPair<string, double>("Pan", 88.8), }; //"3. " int size = sizeof(infos) / sizeof(HandlerPair<string, double>); for(int i = 0; i < size; i ++) { cout << "info -> Key: " << infos[i].getKey() << ", Value: " << infos[i].getValue() << endl; } } ``` 1. **typename & class 是可以互相切換互補的** 2. 在類 (class) 以外不可省略 `template<T1, T2>`,必須要詳細寫出 3. sizeof(Array) 可以計算出該陣列所佔用的位元數 (Byte 單位),**sizeof 計算單個模板的時候也需要寫出模板 `<>`** **--實作--** > ![](https://i.imgur.com/tbkiCCp.png) ### 預設型態樣板 * 如同建構函數中的預設參數,`template<class T1, class T2 = int> `使用等於符號指定其預設值 ```cpp= #include <iostream> using std::cout; using std::endl; //"1. " template<typename T1, class T2 = double> class HandlerPair{ private: T1 key; T2 value; public: HandlerPair(const T1 & t1, const T2 & t2) : key(t1), value(t2) {} T1 & getKey() { return key; } T2 & getValue(); }; template<class T1, typename T2> T2 & HandlerPair<T1, T2>::getValue() { return value; } int main() { using std::string; HandlerPair<string, double> infos[] = { //"2. " HandlerPair<string>("Alien", 84.3), HandlerPair<string>("Kyle", 90.3), HandlerPair<string>("Pan", 88.8), }; int size = sizeof(infos) / sizeof(HandlerPair<string, double>); for(int i = 0; i < size; i ++) { cout << "info -> Key: " << infos[i].getKey() << ", Value: " << infos[i].getValue() << endl; } } ``` 1. 模板預設參數型態 2. 在使用時就同預設建構參數,**如果有預設型態,可以不用指定有預設的型態** **--實作--** > ![](https://i.imgur.com/4vwmDv5.png) ## 模板特定化 | 處理方式 | 解釋 | 範例 | | -------- | -------- | -------- | | 隱式實體化 (`implicit instantiation`) | 宣告物件表示目的型態,**編譯程式會使用通用樣板提供** | TpArray<double, 5> sum; | | 顯式實體化 (`explicit instantiation`) | 使用關鍵子 template 宣告類別,並指定目的型態 | **template class** TpArray<double, 5>; | | **顯式特定化 (`implicit specialization`)** | 樣板以**特定型態**描述類別 | | ### 隱式實體化 * **隱式實體化**:宣告指標,再使用 new 關鍵字定義;之前的範例皆是使用隱式實體化 ```cpp= // 使用通用模板創造 TpArray<double, 5> sum; // 指標宣告,無實體化 TpArray<double, 5> *ptrSum; ptrSum = new TpArray<double, 5>; // 這時才產生實體化,並存放在 heap 記憶體區塊 ``` ### 顯式實體化 * 使用關鍵子 template 宣告類別,並指定目的型態時,編譯程式會產生此類別宣告的實體化,**該宣告應與此類別在同樣的名稱空間中** ```cpp= template<typename T1, class T2 = double> class HandlerPair{ private: T1 key; T2 value; public: HandlerPair(const T1 & t1, const T2 & t2) : key(t1), value(t2) {} T1 & getKey() { return key; } T2 & getValue() { return value; }; void f(){} }; // 顯示實體化 template class HandlerPair<string, double>; // 產生 HandlerPair 的類 // 以下不可 !!! 同時與 類實體化 一起使用 template void HandlerPair<string, double>::f(); template string& HandlerPair<string, double>::getKey(); template float& HandlerPair<string, double>::getValue(); ``` * 宣告在全域,**可以指定實例化類別**,可實例化方法、類,**==如果已經顯式實體化類就不能實體化方法==,因為會 ++導致方法重複定義++** > ![](https://i.imgur.com/XjRhFCI.png) ### 顯示特定化 * 簡稱為 **特定化,一般來說定義只有一個**,但特定化可以針對不同型態 * **顯示特定化是針對特定型態的定義,此定義會用來 ++取代通用樣板++**,就像是在比較大小的樣板,用在數字上只要複寫 `T::operator>()` 就可以,但是如果使用在 `char` 比較就要使用 `strcmp()` 取代 `>` ```cpp= // 特定化 - 舊格式 class ClassName<特定格式> { }; // 特定化 - 新格式 template <> class ClassName<特定格式> { }; ``` * 特定化類別使用 ```cpp= #include <iostream> using std::cout; using std::endl; using std::string; template<typename T1, class T2> class HandlerPair { private: T1 key; T2 value; public: HandlerPair(const T1 & t1, const T2 & t2) : key(t1), value(t2) {} T1 & getKey() { return key; } T2 & getValue() { return value; }; void f(); }; template<typename T1, class T2> void HandlerPair<T1, T2>::f() { cout << "General Function" << endl; } // ------------------------------------------------------------------ // 特定化 template <> class HandlerPair<string, const char*> { public: void f(); }; // 特定化實現 void HandlerPair<string, const char*>::f() { cout << "Specialization Function" << endl; } // ------------------------------------------------------------------ int main() { HandlerPair<string, double> genernal("Alien", 12); genernal.f(); HandlerPair<string, const char*> special; special.f(); } ``` **--實作--** > ![](https://i.imgur.com/Ow1DSIX.png) ### 部分特定化 * C++ 也接受 **部分特定化 (partial specialization),這部分的限制了樣板的通用性** ```cpp= // general template <class T1, class T2> class Pair { ... }; // special -1 template <class T1> class Pair<T1, int> { // 指定 pair 第二個型態為 int ... }; // special -2 template <class T1> class Pair<float, int> { // 指定 pair 第一個型態為 float、第二個型態為 int ... }; int main() { Pair<double, float> Pair; // 通用模板 Pair<double, int> Pair_s1; // special -1 Pair<float, int> Pair_s2; // special -2 return 0; } ``` ## Appendix & FAQ :::info 1. 在實作虛擬類繼承時,多重繼承的類呼叫基類建構函數有警告...好像是 **++順序錯誤++** ??? > ![](https://i.imgur.com/QsGdjs3.png) ::: ###### tags: `C++`