測試環境:Target: x86_64-linux-gnu
gcc version 5.3.1 20160413 (Ubuntu 5.3.1-14ubuntu2.1)
什麼是泛型程式設計?為什麼C++會有模板?這一切的一切都要從如何編寫一個通用的加法函式說起。
很久很久以前
有一個人要編寫一個通用的加法函式,他想到了這麼幾種方法:
- 使用函式過載,針對每個所需相同行為的不同型別重新實現它
1 2 3 4 5 6 7 8 |
int Add(const int &_iLeft, const int &_iRight) { return (_iLeft + _iRight); } float Add(const float &_fLeft, const float &_fRight) { return (_fLeft + _fRight); } |
當然不可避免的有自己的缺點:
- 只要有新型別出現,就要重新新增對應函式,太麻煩
- 程式碼的複用率低
- 如果函式只是返回值型別不同,函式過載不能解決(函式過載的條件:同一作用域,函式名相同,引數列表不同)
- 一個方法有問題,所有的方法都有問題,不好維護
- 使用公共基類,將通用的程式碼放在公共的基礎類裡面
【缺點】
1、藉助公共基類來編寫通用程式碼,將失去型別檢查的優點
2、對於以後實現的許多類,都必須繼承自某個特定的基類,程式碼維護更加困難
- 巨集
1 |
#define ADD(a, b) ((a) + (b)) |
- 不進行引數型別檢測,安全性不高
- 編譯預處理階段完成替換,除錯不便
所以在C++中又引入了泛型程式設計的概念。泛型程式設計是編寫與型別無關的程式碼。這是程式碼複用的一種手段。模板則是泛型程式設計的基礎。
模板分為了函式模板和類别範本:
函式模板
函式模板:代表了一個函式家族,該函式與型別無關,在使用時被引數化,根據實參型別產生函式的特定型別版本。
什麼意思呢?往下看就知道了。
模板函式的格式
template<typename Param1, typename Param2,…,class Paramn>
返回值型別 函式名(引數列表)
{
…
}
一個簡單的Add函式模板:
1 2 3 4 5 6 7 8 9 10 11 12 |
template <typename T> //T可是自己起的名字,滿足命名規範即可 T Add(T left, T right) { return left + right; } int main() { Add(1, 2); //right 呼叫此函式是將 Add(1, 2.0); //error 只有一個型別T,傳遞兩個不同型別引數則無法確定模板引數T的型別,編譯報錯 return 0; } |
對第一個函式呼叫,編譯器生成了 int Add(int, int) 這樣一個函式。
typename是用來定義模板引數關鍵字,也可以使用class。不過建議還是儘量使用typename,因為這個關鍵字是為模板而生的!
注意:不能使用struct代替typename。(這裡的class不是之前那個class的意思了,所以你懂的)
當然你也可以把函式模板宣告為內聯的:
template <typename T>
inline T Add(T left, T right) {//…}
例項化
編譯器用模板產生指定的類或者函式的特定型別版本,產生模板特定型別的過程稱為函式模板例項化。(用類型別來建立一個物件也叫做例項化哦!)
模板的編譯
模板被編譯了兩次:
- 例項化之前,檢查模板程式碼本身,檢視是否出現語法錯誤,如:遺漏分號(遺憾的是不一定能給檢查的出來)
- 在例項化期間,檢查模板程式碼,檢視是否所有的呼叫都有效,如:例項化型別不支援某些函式呼叫
實參推演
從函式實參確定模板形參型別和值的過程稱為模板實參推演。多個型別形參的實參必須完全匹配。
如對這樣的函式呼叫:
1 2 3 4 5 6 7 8 9 10 |
template <typename T1, typename T2, typename T3> void fun(T1 t1, T2 t2, T3 t3) { //do something } int main() { fun(1, 'a', 3.14); return 0; } |
編譯器生成了如下這樣的函式:
其中,函式引數的型別是和呼叫函式傳遞的型別完全匹配的。
型別形參轉換
一般不會轉換實參以匹配已有的例項化,相反會產生新的例項。
舉個例子:對如下的函式呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template <typename T> T Add(T left, T right) { return left + right; } int Add(int left, int right) { return left + right; } int main() { Add(1.2, 3.4); return 0; } |
即程式中已經實現過了Add函式的int版本,那麼呼叫Add(1.2, 3.4);時,編譯器不會將1.2和3.4隱式轉換為int型,進而呼叫已有的Add版本,而是重新合成一個double的版本:
當然前提是能夠生成這麼一個模板函式。如果這個模板函式無法生成的話,那麼只能呼叫已有的版本了。
編譯器只會執行兩種轉換:
1、const轉換:接收const引用或者const指標的函式可以分別用非const物件的引用或者指標來呼叫
2、陣列或函式到指標的轉換:如果模板形參不是引用型別,則對陣列或函式型別的實參應用常規指標轉換。陣列實參將當做指向其第一個元素的指標,函式實參當做指向函式型別的指標。
第一種:
1 2 3 4 5 6 7 8 9 10 11 |
template <typename T> T Add(const T &left,const T &right) { return left + right; } int main() { Add(1.2, 3.4); return 0; } |
面對這樣的傳參,編譯器是可以完成1.2到const double &型別的轉換,因為這樣做是安全的。
第二種:
1 2 3 4 5 6 7 8 9 10 11 |
template <typename T> T Add(const T &left,const T &right) { return left + right; } int main() { Add(1.2, 3.4); return 0; } |
完成了陣列到指標的轉換,因為陣列在作為函式引數傳遞時,本身就會發生降級,形參接收到的是個指標,且指向該陣列的第一個元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template <typename T> int sum(T *t) { //do something } void fun() { //do something } int main() { sum(fun); return 0; } |
上例完成函式到函式指標的轉換。
模板引數
函式模板有兩種型別引數:模板引數和呼叫引數。模板引數又分為模板型別形參和非模板型別形參。
模板形參名字只能在模板形參之後到模板宣告或定義的末尾之間使用,遵循名字遮蔽規則。
模板形參的名字在同一模板形參列表中只能使用一次。
所有模板形參前面必須加上class或者typename關鍵字修飾。
注意:在函式模板的內部不能指定預設的模板實參。
模板型別形參是模板內部定義的常量,在需要常量表示式的時候,可以使用非模板型別引數。例如:
1 2 3 4 5 6 7 8 9 10 11 |
template <typename T, int size> //size即為非模板型別引數 void sum(T (&arr)[size]) { } int main() { int arr[10]; sum(arr); //這裡呼叫時,自動將10傳遞給size,作為陣列元素個數 return 0; } |
模板形參說明:
- 模板形參表使用括起來
- 和函式參數列一樣,跟多個引數時必須用逗號隔開,型別可以相同也可以不相同
- 模板形參表不能為空
- 模板形參可以是型別形參,也可以是非型別新參,型別形參跟在class和typename後
- 模板型別形參可作為型別說明符用在模板中的任何地方,與內建型別或自定義型別使用方法完全相同,可用於指定函式形參型別、返回值、區域性變數和強制型別轉換
- 模板形參表中,class和typename具有相同的含義,可以互換,使用typename更加直觀。但關鍵字typename是作為C++標準加入到C++中的,舊的編譯器可能不支援。
模板函式過載
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int Max(const int& left, const int & right) { return left>right? left:right; } template<typename T> T Max(const T& left, const T& right) { return left>right? left:right; } template<typename T> T Max(const T& a, const T& b, const T& c) { return Max(Max(a, b), c); }; int main() { Max(10, 20, 30); Max<>(10, 20); //3.用模板生成,而不是呼叫顯示定義的同型別版本 Max(10, 20); Max(10, 20.12); Max<int>(10.0, 20.0); //顯示告訴編譯器T的型別 Max(10.0, 20.0); return 0; } |
說明
- 一個非模板函式可以和一個同名的函式模板同時存在,而且該函式模板還可以被例項化為這個非模板函式
- 對於非模板函式和同名函式模板,如果其他條件都相同,在調動時會優先調動非模板函式而不會從該模板產生出一個例項。如果模板可以產生一個具有更好匹配的函式,那麼將選擇模板
- 顯式指定一個空的模板實參列表,該語法告訴編譯器只有模板才能來匹配這個呼叫,而且所有的模板引數都應該根據實參演繹出來
- 模板函式不允許自動型別轉換,但普通函式可以進行自動型別轉換
上面的函式模板不能用來比較兩個字串,如果傳遞引數為字串,返回的則是兩個引數地址的比較,不是我們想要的結果。所以,又有了模板函式特化:
模板函式特化形式如下:
1、關鍵字template後面接一對空的尖括號
2、再接模板名和一對尖括號,尖括號中指定這個特化定義的模板形參
3、函式形參表
4、函式體
在模板特化版本的呼叫中,實參型別必須與特化版本函式的形參型別完全匹配,如果不匹配,編譯器將為實參模板定義中例項化一個例項。
舉一個栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
template <typename T> int cmp(const T &left, const T &right) { return left - right; } template <> int cmp<const char * &>(const char * &p1, const char * &p2) { return strcmp(p1, p2); } int main() { const char *s1 = "abc"; const char *s2 = "abd"; cmp(s1, s2); return 0; } |
再次強調,實參型別必須與特化版本函式的形參型別完全匹配,哪怕實參前邊修飾的沒有const,而模板形參中有,也不會構成特化。特化版本是對應於此模板而寫的,特化版本的引數型別必須完全與模板形參相同,如上述例子中,都為const &。
類别範本
格式
template<class 形參名1, class 形參名2, …class 形參名n>
class 類名
{ … };
舉個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 以模板方式實現動態順序表 template<typename T> class SeqList { public : SeqList(); ~ SeqList(); private : int _size ; int _capacity ; T* _data ; }; template <typename T> SeqList <T>:: SeqList() : _size(0) , _capacity(10) , _data(new T[ _capacity]) {} template <typename T> SeqList <T>::~ SeqList() { delete [] _data ; } void test1 () { SeqList<int > sl1; SeqList<double > sl2; } |
與呼叫函式模板形成對比,使用類别範本時,必須為模板形參顯式指定實參!
模板類的例項化
只要有一種不同的型別,編譯器就會例項化出一個對應的類。
SeqList sl1;
SeqList sl2;
當定義上述兩種型別的順序表時,編譯器會使用int和double分別代替模板形參,重新編寫SeqList類,最後建立名為SeqList和SeqList的類。