測試環境:
Target: x86_64-linux-gnu
gcc version 5.3.1 20160413 (Ubuntu 5.3.1-14ubuntu2.1)
什麼是泛型編程?為什麼C++會有模板?這一切的一切都要從如何編寫一個通用的加法函數說起。
有一個人要編寫一個通用的加法函數,他想到了這麼幾種方法:
使用函數重載,針對每個所需相同行為的不同類型重新實現它
int Add(const int &_iLeft, const int &_iRight) { return (_iLeft + _iRight); } float Add(const float &_fLeft, const float &_fRight) { return (_fLeft + _fRight); }
當然不可避免的有自己的缺點:
只要有新類型出現,就要重新添加對應函數,太麻煩代碼的復用率低如果函數只是返回值類型不同,函數重載不能解決(函數重載的條件:同一作用域,函數名相同,參數列表不同)一個方法有問題,所有的方法都有問題,不好維護 使用公共基類,將通用的代碼放在公共的基礎類裡面
【缺點】
1、借助公共基類來編寫通用代碼,將失去類型檢查的優點
2、對於以後實現的許多類,都必須繼承自某個特定的基類,代碼維護更加困難
宏
#define ADD(a, b) ((a) + (b))
不進行參數類型檢測,安全性不高編譯預處理階段完成替換,調試不便
所以在C++中又引入了泛型編程的概念。泛型編程是編寫與類型無關的代碼。這是代碼復用的一種手段。模板則是泛型編程的基礎。
模板分為了函數模板和類模板:
函數模板:代表了一個函數家族,該函數與類型無關,在使用時被參數化,根據實參類型產生函數的特定類型版本。
什麼意思呢?往下看就知道了。
template
返回值類型 函數名(參數列表)
{
...
}
一個簡單的Add函數模板:
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
typename是用來定義模板參數關鍵字,也可以使用class。不過建議還是盡量使用typename,因為這個關鍵字是為模板而生的!
注意:不能使用struct代替typename。(這裡的class不是之前那個class的意思了,所以你懂的)
當然你也可以把函數模板聲明為內聯的:
template
inline T Add(T left, T right) {//...}
編譯器用模板產生指定的類或者函數的特定類型版本,產生模板特定類型的過程稱為函數模板實例化。(用類類型來創建一個對象也叫做實例化哦!)
模板被編譯了兩次:
實例化之前,檢查模板代碼本身,查看是否出現語法錯誤,如:遺漏分號(遺憾的是不一定能給檢查的出來)在實例化期間,檢查模板代碼,查看是否所有的調用都有效,如:實例化類型不支持某些函數調用
實參推演
從函數實參確定模板形參類型和值的過程稱為模板實參推演。多個類型形參的實參必須完全匹配。
如對這樣的函數調用:
templatevoid fun(T1 t1, T2 t2, T3 t3) { //do something } int main() { fun(1, 'a', 3.14); return 0; }
編譯器生成了如下這樣的函數:
其中,函數參數的類型是和調用函數傳遞的類型完全匹配的。
一般不會轉換實參以匹配已有的實例化,相反會產生新的實例。
舉個栗子:對如下的函數調用:
templateT 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、數組或函數到指針的轉換:如果模板形參不是引用類型,則對數組或函數類型的實參應用常規指針轉換。數組實參將當做指向其第一個元素的指針,函數實參當做指向函數類型的指針。
第一種:
templateT Add(const T &left,const T &right) { return left + right; } int main() { Add(1.2, 3.4); return 0; }
面對這樣的傳參,編譯器是可以完成1.2到const double &類型的轉換,因為這樣做是安全的。
第二種:
templateint sum(T *t) { //do something } int main() { int arr[10]; sum(arr); return 0; }
完成了數組到指針的轉換,因為數組在作為函數參數傳遞時,本身就會發生降級,形參接收到的是個指針,且指向該數組的第一個元素。
templateint sum(T *t) { //do something } void fun() { //do something } int main() { sum(fun); return 0; }
上例完成函數到函數指針的轉換。
函數模板有兩種類型參數:模板參數和調用參數。模板參數又分為模板類型形參和非模板類型形參。
模板形參名字只能在模板形參之後到模板聲明或定義的末尾之間使用,遵循名字屏蔽規則。
模板形參的名字在同一模板形參列表中只能使用一次。
所有模板形參前面必須加上class或者typename關鍵字修飾。
注意:在函數模板的內部不能指定缺省的模板實參。
模板類型形參是模板內部定義的常量,在需要常量表達式的時候,可以使用非模板類型參數。例如:
template//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++中的,舊的編譯器可能不支持。
int Max(const int& left, const int & right) { return left>right? left:right; } templateT Max(const T& left, const T& right) { return left>right? left:right; } template 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 (10.0, 20.0); //顯示告訴編譯器T的類型 Max(10.0, 20.0); return 0; }
說明
一個非模板函數可以和一個同名的函數模板同時存在,而且該函數模板還可以被實例化為這個非模板函數對於非模板函數和同名函數模板,如果其他條件都相同,在調動時會優先調動非模板函數而不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數,那麼將選擇模板顯式指定一個空的模板實參列表,該語法告訴編譯器只有模板才能來匹配這個調用,而且所有的模板參數都應該根據實參演繹出來模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換
上面的函數模板不能用來比較兩個字符串,如果傳遞參數為字符串,返回的則是兩個參數地址的比較,不是我們想要的結果。所以,又有了模板函數特化:
模板函數特化形式如下:
1、關鍵字template後面接一對空的尖括號<>
2、再接模板名和一對尖括號,尖括號中指定這個特化定義的模板形參
3、函數形參表
4、函數體
在模板特化版本的調用中,實參類型必須與特化版本函數的形參類型完全匹配,如果不匹配,編譯器將為實參模板定義中實例化一個實例。
舉一個栗子:
templateint cmp(const T &left, const T &right) { return left - right; } template <> int cmp (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 類名
{ ... };
舉個栗子:
// 以模板方式實現動態順序表 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
SeqList
當定義上述兩種類型的順序表時,編譯器會使用int和double分別代替模板形參,重新編寫SeqList類,最後創建名為SeqList