在咱們編寫代碼時,咱們會碰見這種狀況:
好比交換函數,當咱們要交換的類型是int(傳的參數爲int型)時,咱們要編寫的swap函數的形參就應該是int,但當咱們要交換的是double型時,咱們還要再寫一個swap函數來知足要求。每換一種類型就要再重載一個swap函數來知足條件。
雖然經過這方法重載實現全部類型的交換函數,可是這種方法有幾個很差的地方,一是重載函數僅僅類型不一樣,致使代碼的複用率很低,只要有新類型出現,就要增長對應的函數;再者代碼的可維護性比較低,一個出錯可能全部的重載都出錯,要一個一個改。編程
經過上面的例子,咱們想能不能告訴編譯器一個模子,編譯器能夠經過不一樣的類型利用這樣的模子自動生成適合各類類型的函數。答案是固然能夠
即泛型編程:編寫與類型無關的通用代碼,而模板是泛型編程的基礎。ide
下面咱們來鄭重的引入模板函數
如何使用?
template <typename T1,typename T2...>
返回值 函數名(參數列表){ }
typename是用來定義模板參數關鍵字的,也能夠用class
例如:spa
template<typename T> void Swap(T &x, T &y) //以前不一樣的類型,對應不一樣的形參列表,因此要寫多個重載函數,但如今只寫一個模板函數,編譯器就能夠根據這個模板結合傳入的參數就可完成全部類型的生成對應類型的函數以供調用 T tmp = x; x = y; y = tmp; } //在這個函數中,T只能被替換爲同樣的類型,若傳入參數不一樣,則編譯器其則生成不了匹配的函數,而編譯器又不會進行類型轉換,於是會報錯 //若想要不一樣類型,可定義兩個T template<class T1, class T2> //typename可用class代替 void Swap(T1 &x, T2 &y) { T1 tmp = x; x = y; y = tmp; } int main() { int x1=0, y1=1; double x2 = 1.0, y2 = 2.0; Swap(x1, y1); Swap(x2, y2); Swap(x1, y2); }
函數模板實例化
編譯器根據不一樣的參數用模板推演不一樣的函數稱爲函數模板的實例化,模板參數實例化分爲:隱式實例化和顯示實例化。
經過下面例子介紹隱式實例化和顯示實例化.net
template<typename T> T Add(const T &x, const T &y) { return x + y; } int main() { int x1=0, y1=1; double x2 = 1.0, y2 = 2.0; //隱式實例化,讓編譯器根據實參推演模板參數的實際類型 Add(x1, y1); //隱式實例化 //當沒法肯定T是什麼,由該推演成什麼的時候,會報錯,於是要否則進行強轉使傳入參數類型相同,要否則進行顯式實例化 Add(x1, (int)y2); //強轉 //顯式實例化,直接告訴編譯器,由模板該推演成什麼,在函數名後的<>中指定模板參數的實際類型 Add<int>(x1, (int)y2); }
1.一個非模板函數能夠和一個同名的函數模板同時存在,並且該函數模板還能夠被實例化爲這個非模板函數。
2.模板函數不容許自動類型轉換,但普通函數能夠進行自動類型轉換。
3.對於非模板函數和同名函數模板,若是其餘條件都相同,在調動時會優先調用非模板函數而不會從該模板產生出一個實例。若是模板能夠產生一個具備更好匹配的函數,那麼將選擇模板。指針
int Add(int x, int y) { return x + y; } template<typename T> T Add(const T &x, const T &y) { return x + y; } template<typename T1, typename T2> T1 Add(const T1 &x, const T2 &y) { return x + y; } int main() { //1 Add(1, 2); //調用非模板函數,無需模板實例化 Add<int>(1, 2);//調用編譯器特化的模板函數版本 --若是指定類型就必須用模板來生成相應類型 //2. Add(1, 2.0); //此處會調用非模板函數,發生隱式類型轉換 (說明:此處還未加Add(const T1 &x, const T2 &y)函數模板) //3. Add(1, 2.0); //選擇函數模板若選擇非模板函數,則會發生隱式類型轉化, //不如調用Add(const T1 &x, const T2 &y)這個實例化的函數形參列表更匹配 }
使用格式:
template <class T1,class T2....>
class A( /A爲類模板名,A不是具體的類,是編譯器根據被實例化的類型生成具體類的模具)
{
...
};code
類模板實例化須要在類模板名字後跟<>,而後將實例化的類型放在<>中便可,類模板名字不是真正的類,而實例化的結果纔是真正的類。
例如容器vector的實現:對象
template <class T> class vector{ typedef T* iterator; public: //構造析構 vector() : _start(nullptr) , _finish(nullptr) , _endOfStorage(nullptr) { } vector(int n, const T &value = 0) :_start(nullptr) , _finish(nullptr) , _endOfStorage(nullptr) { _start = new T[n+1]; int i = n; while (i--) { _start[i] = value; } _start[n] = '\0'; _finish = _start + n; _endOfStorage = _finish; } template <class InputIterator> vector(InputIterator first, InputIterator last) :_start(nullptr) , _finish(nullptr) , _endOfStorage(nullptr) { int n=0; auto tmp = first; while (tmp != last) { n++; tmp++; } _start = new T[n + 1]; _finish = _start + n; _endOfStorage = _finish; last--; while (n--) { _start[n] = *last; last--; } } vector(const vector<T>& v) :_start(nullptr) , _finish (nullptr) ,_endOfStorage(nullptr) { reserve(v.capacity()); //memcpy(_start, v._start,v.size()); for (int i = 0; i < v.size(); i++) { _start[i] = v._start[i]; } _finish = _start + v.size(); _endOfStorage = _start + v.capacity(); } ~vector() { delete[] _start; _start = nullptr; _finish = nullptr; _endOfStorage = nullptr; } iterator begin() { return _start; } iterator end() { return _finish; } int size()const { return _finish - _start; } int capacity()const { return _endOfStorage - _start; } friend iterator find(iterator begin, iterator end, const T &value); private: T *_start; T *_finish; T *_endOfStorage; }; // 注意:類模板中函數放在類外進行定義時,須要加模板參數列表 template <class T> T* find(T* begin, T* end, const T &value) { auto it = begin; while (it != end) { if (*it == value) { return it; } it++; } return nullptr; } int main(){ vector<int> v1; //實例化 }
模板的其餘知識說明:blog
模板參數分爲類型形參與非類型形參
類型形參:出如今模板參數列表中,跟在class或者typename之類的參數類型名稱。
非類型形參,就是用一個常量做爲類(函數)模板的一個參數,在類(函數)模板中可將該參數當成常量來使用。ci
例如: namespace Eg { template <class T,size_t N=10> //N在下面類中,做爲常數使用 class array{ public: size_t size()const { //N = 20; //會報錯:錯誤 1 error C2106: 「=」: 左操做數必須爲左值,可證實N是常數 return _size; } private: T _array[N]; size_t _size; }; } void Test1(){ Eg::array<int> x; x.size(); }
對於一些特殊的類型(好比指針類型),使用已寫的模板可能達不到咱們想要的結果,得出錯誤的結果。
例如:
template <class T> T& MAX_T(T& left, T& right) { return left>right?left:right; } void Test(){ int x = 2,y=3; cout << MAX_T(x, y) << endl; char *p1 = "wello"; char *p2 = "hello"; cout << MAX_T(p1, p2) << endl; //應該輸出wello 但由於比較的是地址卻輸出hello不符合邏輯,所以咱們要爲char*來特化一個模板提供給這種類型 }
經過上述列子咱們能夠經過對模板進行特化,在原來模板函數(或類)的基礎上,針對特殊類型進行特殊化的實現。好比上述例子中爲char*特化一個函數,按照咱們的想法針對這種類型進行特殊化處理,獲得正確的結果。
模板特化又分爲函數模板特化與類模板特化
函數模板的特化方式:
1.要先有一個基礎函數模板
2.關鍵字template後面接一堆空的尖括號<>
3.函數名後跟一對尖括號裏面放須要特化的類型
4.函數形參表必需要和模板函數的基礎參數類型徹底相同,不一樣的話編譯器可能會報一些錯誤
//好比針對上述char*類型特化: template <class T> T& MAX_T(T& left, T& right) { return left>right?left:right; } template <> char*& MAX_T<char*>(char*& left, char*& right) { if (strcmp(left, right) == 1) { return left; } else{ return right; } } void Test(){ char *p1 = "wello"; char *p2 = "hello"; cout << MAX_T(p1, p2) << endl; //會調用char*& MAX_T<char*>(char*& left, char*& right)特化模板函數 }
可是!!
對於特例化的函數模板,通常都是將該函數直接給出,一是實現簡單,二是由於函數模板可能會遇到不能處理或者處理有誤的類型
類模板特化又分爲全特化和偏特化
template<class T1, class T2> class Data1 { public: Data1() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; template<> class Data1<int, int> { public: Data1() { cout << "Data1<int, int>" << endl; } private: int _d1; int _d2; }; void Test(){ Data1<int, int> d1; //用特化模板參數類 Data1<int, double> d2; }
2.偏特化:有兩種表現:部分特化和參數更進一步的限制
template<class T1, class T2> class Data2 { public: Data2() { cout << "Data2<T1, T2>" << endl; } private: T1 _d1; T2 _d2; };
template<class T1> class Data2<T1, int> //特化一半 { public: Data2() { cout << "Data2<T1, int>" << endl; } private: T1 _d1; int _d2; }; void Test(){ Data2<int, int> d1; //用部分特化模板參數類 Data2<double, int> d2; //用部分特化模板參數類 由於後面的都是int,都符合這個部分特化模板 Data2<int, double> d3; Data2<double, double> d4; }
template<class T1, class T2> class Data2<T1*, T2*> { public: Data2() { cout << "Data2<T1*, T2*>" << endl; } private: T1* _d1; T2* _d2; }; void Test(){ Data2<int*, int> d5; //使用class Data2<T1, int> Data2<int, int*> d6; //使用template<class T1, class T2> class Data2{} Data2<int*, int*> d7; //使用class Data2<T1*, T2*> Data2<int*, double*> d8; //使用class Data2<T1*, T2*> }
經過如下題目來感覺類型萃取
// 寫一個通用的拷貝函數,要求:效率儘量高 /*Way1: //String:用該函數會報錯拷貝的是地址,析構會對同一塊空間釋放兩次,會報錯 template<class T> void Copy(T* dst, T* src, size_t size) { memcpy(dst, src, sizeof(T)*size); } */ //於是要區分自定義和內置類型,來調用使用不一樣的方法進行拷貝 /* //Way2:增長函數斷定 區分自定義和內置類型 bool IsPodType(const char* strType){ const char* arrType[] = { "char", "short", "int", "long", "long long", "float","double", "long double" }; for (size_t i = 0; i < sizeof(arrType) / sizeof(arrType[0]); ++i) //每次都要遍歷,效率過低! { if (0 == strcmp(strType, arrType[i])) return true; } return false; } template<class T> void Copy(T* dst, T* src, size_t size) { if (IsPodType(typeid(T).name())) { memcpy(dst, src, sizeof(T)*size); } else{ for (int i = 0; i < size; i++) { dst[i] = src[i]; } } } */ //Way3:萃取類型 //表明內置類型 struct TrueType{ static bool Get(){ //只有靜態才能用 :: 訪問 return true; } }; //表明自定義類型 struct FlaseType{ static bool Get(){ return false; } }; template<class T> struct TypeTraits{ typedef FlaseType IsPodeType; }; //對上述模板進行實例化,將內置類型都特化 template<> struct TypeTraits<int>{ typedef TrueType IsPodeType; }; template<> struct TypeTraits<double>{ typedef TrueType IsPodeType; }; /* T爲int:TypeTraits<int>已經特化過,程序運行時就會使用已經特化過的TypeTraits<int>, 該類中的IsPODType恰好爲類TrueType,而TrueType中Get函數返回true,內置類型使用memcpy方式拷貝 T爲string:TypeTraits<string>沒有特化過,程序運行時使用TypeTraits類模板, 該類模板中的IsPODType恰好爲類FalseType,而FalseType中Get函數返回true,自定義類型使用賦值方式拷貝 */ template<class T> void Copy(T* dst, T* src, size_t size) { if (TypeTraits<T>::IsPodeType::Get()) { memcpy(dst, src, sizeof(T)*size); } else{ for (int i = 0; i < size; i++) { dst[i] = src[i]; } } } void TestCopy() { int array1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int array2[10]; Copy(array2, array1, 10); String s1[3] = { "1111", "2222", "3333" }; String s2[3]; Copy(s2, s1, 3); }
要說模板分離編譯咱們就要先來談談什麼是分離編譯
一個工程中有不少文件,但他們分爲兩大類:頭文件和源文件
程序的運行有如下五個步驟:
預處理-->編譯-->彙編-->連接
頭文件會在預處理階段展開,在預處理期間程序會將頭文件中的內容複製一份到源文件。參與編譯的只有源文件,並且每一個源文件都單獨進行編譯生成目標文件,最後將全部目標文件連接造成單一的可執行文件。以下圖:
目標文件連接的時候,是經過找函數地址(入口)進行調用的。
注意:強調:每一個源文件都單獨進行編譯
對於模板:
實例化以前編譯器只會作一些簡單的語法測驗,不會生成處理具體類型的代碼
實例化期間編譯器用過推演形參類型來確保模板參數,經過列表中T的實際類型在生成處理具體類型的代碼
//頭文件 CompilingTest.h template <class T> T Add(T left,T right); //源文件 CompilingTest.c #include "CompilingTest.h" template <class T> T Add(T left, T right) { return left + right; } //源文件 main.c #include "CompilingTest.h" int main() { Add(1.0, 1.0); //會發生報錯,由於沒有實例化Add(int,int),找不到匹配的函數入口地址,於是在連接時會報錯 }
//頭文件 CompilingTest.h template <class T> T Add(T left,T right); //源文件 CompilingTest.c #include "CompilingTest.h" template <class T> T Add(T left, T right) { return left + right; } void tmp(){ Add(1, 2); //在此編譯器推演出了T->int的函數,在連接時找到了適合Add(1, 1);該函數的入口地址,於是纔不會報錯 } //源文件 main.c #include "CompilingTest.h" int main() { Add(1, 1); //這樣就不會發生錯誤了 //Add(1.0, 1.0); //無匹配的Add(double,,double) }
經過上述例子可得模板不支持分離編譯
解決方法:
分離編譯詳細講解: http://blog.csdn.net/pongba/article/details/19130
優勢: