當編譯器遇到一個模板定義時,它並不生成代碼。只有當我們實例化出模板的一個特定版本時,編譯器才會生成代碼。當我們使用模板時,比編譯器才生成代碼。
這一特性影響了我們如何組織代碼以及錯誤何時被檢測到。
通常,當我們調用一個函數時,編譯器只需要掌握函數的聲明。
類似的,當我們使用一個類類型的對象時,類定義必須是可用的,但成員函數的定義不必已經出現。
因此我們將類定義和函數聲明放在頭文件中,而普通函數和類的成員函數的定義放在源文件中。
模板則不同:
為了生成一個實例化版本,編譯器需要掌握函數模板或類模板成員函數的定義。因此,與非模板代碼不同,模板的頭文件通常即包括聲明也包括定義。
總結一下:
模板的具體實現被稱為實例化或具體化。
因為模板不是函數,他們不能單獨編譯,模板必須與特定的模板實例化請求一起使用。
因此,最簡單的方法是將所有模板信息放在一個頭文件中,並在要使用這些模板的文件中包含該頭文件。
大多數編譯錯誤在實例化期間報告
模板知道實例化時才生成代碼,這一特性影響了我們何時才會獲知模板內代碼的編譯錯誤。
通常,編譯器會在三個階段報告錯誤:
1.第一個階段是編譯模板本身時。
在這個階段,編譯器通常不會發現很多錯誤。編譯器可以檢查語法錯誤,例如忘記分好或者變量名寫錯等等。
2.第二個階段是編譯器遇到模板使用時:
在此階段,編譯器仍然沒有很多檢查的。對於函數模板調用,編譯器通常會檢查實參數目是否正確。他還能檢查參數類型是否匹配,對於類模板,編譯器可以檢查用戶是否提供了正確數目的模板實參,但也僅限於此了。
3.第三個階段是模板實例化時,只有這個階段才能發現類型相關的錯誤。
依賴於編譯器如何管理實例化,這類錯誤可能在鏈接時才報告。
模板類也是模板,必須以關鍵字template開頭,後接模板形參表。
//模板類格式 templateclass 類名 { ... };
關鍵字template告訴編譯器,將要定義一個模板,尖括號中的內容相當於函數的參數列表。
下面用模板類實現動態順序表
以模板方式實現動態順序表
templateclass SeqList { public : SeqList(); ~ SeqList(); private : int _size ; int _capacity ; T* _data ; }; template SeqList :: SeqList() : _size(0) , _capacity(10) , _data(new T[ _capacity]) {} template SeqList ::~ SeqList() { delete [] _data ; } void test1 () { SeqList sl1; SeqList sl2; }
類模板是用來生成類的藍圖的。
與函數模板的不同之處是:編譯器不能為類模板推斷模板參數類型。為了使用類模板,我們必須在模板名後的尖括號中提供額外信息——用來代替模板參數的模板實參列表。
只要有一種不同的類型,編譯器就會實例化出一個對應的類。
SeqListsl1; SeqList sl2;
當定義上述兩種類型的順序表時,編譯器會使用int和double分別代替模板形參,重新編寫SeqList類,最後創建名為SeqList和SeqList的類。
當編譯器從我們的Seqlist模板實例化出一個類時,他會重寫Seqlist模板,將模板參數T的每個實例替換為給定的模板實參。
上面的代碼中,編譯器生成了兩個不同的類。
一個類模板的每個實例都形成一個獨立的類。
我們既可以在類模板內部,也可以在類模板外部為其定義成員函數,且定義在類模板內的成員函數被隱式聲明為內聯函數。
類模板的成員函數本身是一個普通函數,但是類模板的每個實例都有其自己版本的成員函數,因此類模板的成員函數具有和模板相同的模板參數。
因而,定義在類模板之外的成員函數就必須以關鍵字template開始,後接類模板參數列表。
默認情況下,一個類模板的成員函數只有當程序用到它時才進行實例化。
如果一個成員函數沒有被使用,則他不會被實例化。
成員函數只有在被用到時才進行實例化,這一特性使得即使某種類型不能完全符合模板操作的要求,我們仍然能用該類型實例化類。
默認情況下,對於一個實例化了的類模板,其成員只有在使用時才被實例化。
當我們使用一個類模板類型時必須提供模板實參,但這一規則有一個例外。
就是在類模板自己的作用域中,我們可以直接使用模板名而不提供實參。
templateclass SeqList { public : SeqList(); ~ SeqList(); SeqList& operator++() {} private : int _size ; int _capacity ; T* _data ; };
為了舉例子,我沒有實現++運算符重載函數,但是要注意的其實是,返回值SeqList&,而不是SeqList&。
當我們處於一個類模板的作用域中時,編譯器處理模板自身引用時就好像是我們已經提供了與模板參數匹配的實參一樣。
當我們在類模板外定義其成員時,我們並不在類的作用域中,知道遇到類名才表示進入類的作用域。
所以在類外定義函數時:
SeqList& SeqList ::operator++() {}
類似於函數模板,類模板也可以有非類型模板參數。
我們現在來使用非類型模板參數構造一個順序表:
templateclass SeqList { private: T a[n]; }
上面的代碼就實現了一個數組,剩下的函數我都省略了。
已經知道,模板可以包含類型參數和非類型參數,現在還要加一個,類模板可以包含本身,也就是模板的參數。
這種參數是模板新增的特性,用於實現STL。
看下面的代碼:
templateclass SeqList { private : int _size ; int _capacity ; T* _data ; }; // template class Container> //不帶缺省參數 template class Container = SeqList> // 缺省參數 class Stack { public : void Push(const T& x ); void Pop(); const T& Top(); bool Empty(); private : Container _con; }; void Test() { Stack s1; Stack s2; }
在上面的代碼中,我們使用了模板參數:
templateclass Container
使用template來標示這個參數是一個模板參數。
在上面的例子中,我們給了它一個缺省參數為SeqList,如果不給缺省參數,直接傳參數也是可以的。
上面的例子中,我們使用順序表構造了一個棧的類型。
這就是STL中的容器適配器。
下面我們簡單介紹一下STL六大組件中的配接器。
STL是標准模板庫的英文縮寫,STL有六大組件:
1.容器
2.算法
3.迭代器
4.空間配置器
5.配接器
6.仿函數
這篇中我們只介紹配接器與仿函數,剩下的不做提及。
配接器在STL組件的靈活組合運用功能上,扮演者轉換器的角色。
配接器分為:
1.應用於容器的container adapters
2.應用於迭代器的iterator adapters
3.應用於仿函數的:functor adapters
首先在上面的代碼:
templateclass SeqList { private : int _size ; int _capacity ; T* _data ; }; // template class Container> //不帶缺省參數 template class Container = SeqList> // 缺省參數 class Stack { public : void Push(const T& x ); void Pop(); const T& Top(); bool Empty(); private : Container _con; }; void Test() { Stack s1; Stack s2; }
我們就實現了容器配接器。
在STL中,STL提供的兩個容器queue和stack,其實都是一種配接器。他們修飾deque的接口而成就出另一種容器風貌。
functor adapters是所有配接器中數量最龐大的一個族群,它的價值在於,通過他們之間的綁定,組合,修飾能力,幾乎可以無限制的創造出各種可能的表達式,搭配STL算法一起。
我以冒泡排序為例:
void BubbleSort(int *a,int size) { assert(a); int max = a[0]; for(int i = 0; i < size;i++) { int ret = -1; for(int j = i ;j < i;j++) { if(a[j] > a[j+1]) { ret = 1; swap(a[j],a[j+1]); } } if(ret == -1) break; } }
上面的代碼實現了遞增排序的冒泡排序,那麼如果我們想要遞減呢?
我們還得再去定義一個冒泡排序,這樣十分不方便,但是我們可以用實現仿函數解決這個問題。
class Greater { bool operator()(int a,int b) { return a>b?true:false; } } class Less { bool operator()(int a,int b) { return a void BubbleSort(int *a,int size) { Com com; assert(a); int max = a[0]; for(int i = 0; i < size;i++) { int ret = -1; for(int j = i ;j < i;j++) { if(com(a[j],a[j+1]) { ret = 1; swap(a[j],a[j+1]); } } if(ret == -1) break; } }
看上面的代碼,我們可以分析一下
我們通過參數Com生成了一個對象,這個對象默認為Greater,我們重載了Greater和Less類的()運算符,給他傳入兩個參數以判斷大小。
通過上面的代碼就實現了仿函數。
templateclass SeqList { public : SeqList(); ~ SeqList(); private : int _size ; int _capacity ; T* _data ; }; template SeqList :: SeqList() : _size(0) , _capacity(10) , _data(new T[ _capacity]) { cout<<"SeqList " < SeqList ::~ SeqList() { delete[] _data ; }
我們定義了SeqList類,下面對它進行全特化:
template <> class SeqList{ public : SeqList(int capacity); ~ SeqList(); private : int _size ; int _capacity ; int* _data ; }; // 特化後定義成員函數不再需要模板形參 SeqList :: SeqList(int capacity) : _size(0) , _capacity(capacity ) , _data(new int[ _capacity]) { cout<<"SeqList " < ::~ SeqList() { delete[] _data ; } void test1 () { SeqList sl2; SeqList sl1(2); }
顧名思義,全特化就是對模板參數列表中的所有參數都進行特化,不論有幾個參數,都要進行特化,這個函數模板的特化相同。
templateclass Data { public : Data(); private : T1 _d1 ; T2 _d2 ; }; template Data ::Data() { cout<<"Data " < class Data { public : Data(); private : T1 _d1 ; int _d2 ; }; template Data ::Data() { cout<<"Data " < 下面的例子可以看出,偏特化並不僅僅是指特殊部分參數,而是針對模板參數更進一步的條件限制所設計出來的一個特化版本。
局部特化兩個參數為指針類型
// 局部特化兩個參數為指針類型 template <typename t2="" typename=""> class Data <t1*,> { public : Data(); private : T1 _d1 ; T2 _d2 ; T1* _d3 ; T2* _d4 ; }; template <typename t2="" typename=""> Data<t1>:: Data() { cout<<"Data<t1*,>" <<endl; pre="">
// 局部特化兩個參數為引用 templateclass Data { public : Data(const T1& d1, const T2& d2); private : const T1 & _d1; const T2 & _d2; T1* _d3 ; T2* _d4 ; }; template Data :: Data(const T1& d1, const T2& d2) : _d1(d1 ) , _d2(d2 ) { cout<<"Data " < d1; Data d2; Data d3; Data d4(1, 2); }
模板的全特化和偏特化都是在已定義的模板基礎之上,不能單獨存在。
解決辦法:
1.在模板頭文件 xxx.h 裡面顯示實例化->模板類的定義後面添加 template class SeqList; 一般不推薦這種方法,一方面老編譯器可能不支持,另一方面實例化依賴調用者。(不推薦) 2.將聲明和定義放到一個文件 “xxx.hpp” 裡面,推薦使用這種方法。
優點
模板復用了代碼,節省資源,更快的迭代開發,C++的標准模板庫(STL)因此而產生。增強了代碼的靈活性。
缺點
模板讓代碼變得凌亂復雜,不易維護,編譯代碼時間變長。 出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤。