顧得泉:個人主頁
個人專欄:《Linux操作系統》?《C++從入門到精通》??《LeedCode刷題》
鍵盤敲爛,年薪百萬!
一、泛型編程
如何實現一個通用的交換函數呢?
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}
void Swap(double& left, double& right)
{double temp = left;left = right;right = temp;
}
void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}
使用函數重載雖然可以實現,但是有一下幾個不好的地方:
? ? ? ?1.重載的函數僅僅是類型不同,代碼復用率比較低,只要有新類型出現時,就需要用戶自己增加對應的函數
? ? ? ?2.代碼的可維護性比較低,一個出錯可能所有的重載均出錯
那能否告訴編譯器一個模子,讓編譯器根據不同的類型利用該模子來生成代碼呢?
???????如果在C++中,也能夠存在這樣一個模具,通過給這個模具中填充不同材料(類型),來獲得不同材料的鑄件(即生成具體類型的代碼),那將會節省許多頭發。巧的是前人早已將樹栽好,我們只需在此乘涼。
???????泛型編程:編寫與類型無關的通用代碼,是代碼復用的一種手段。模板是泛型編程的基礎
?二、函數模板
1.函數模板概念
???????函數模板代表了一個函數家族,該函數模板與類型無關,在使用時被參數化,根據實參類型產生函數的特定類型版本。
2.函數模板格式
???????template<typename T1, typename T2,…,typename Tn>
???????返回值類型 函數名(參數列表){}
template<typename T>
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}
注意:typename是用來定義模板參數關鍵字,也可以使用class(切記:不能使用struct代替class)
3.函數模板的原理
???????那么如何解決上面的問題呢?大家都知道,瓦特改良蒸汽機,人類開始了工業革命,解放了生產力。機器生產淘汰掉了很多手工產品。本質是什么,重復的工作交給了機器去完成。
???????有人給出了論調:懶人創造世界。
???????函數模板是一個藍圖,它本身并不是函數,是編譯器用使用方式產生特定具體類型函數的模具。所以其實模板就是將本來應該我們做的重復的事情交給了編譯器。
???????在編譯器編譯階段,對于模板函數的使用,編譯器需要根據傳入的實參類型來推演生成對應類型的函數以供調用。比如:當用double類型使用函數模板時,編譯器通過對實參類型的推演,將T確定為double類型,然后產生一份專門處理double類型的代碼,對于字符類型也是如此。
???????對于上述代碼,我們可以驗證他們調用的不是同一個函數,由此可以更好的理解模板
4.函數模板的實例化
???????用不同類型的參數使用函數模板時,稱為函數模板的實例化。模板參數實例化分為:隱式實例化和顯式實例化。
1.隱式實例化
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;Add(a1, a2);Add(d1, d2); /*該語句不能通過編譯,因為在編譯期間,當編譯器看到該實例化時,需要推演其實參類型通過實參a1將T推演為int,通過實參d1將T推演為double類型,但模板參數列表中只有一個T,編譯器無法確定此處到底該將T確定為int 或者 double類型而報錯注意:在模板中,編譯器一般不會進行類型轉換操作,因為一旦轉化出問題,編譯器就需要背黑鍋Add(a1, d1);*/ // 此時有兩種處理方式:1. 用戶自己來強制轉化 2. 使用顯式實例化Add(a, (int)d);return 0;
}
2.顯式實例化
int main(void)
{int a = 10;double b = 20.0;// 顯式實例化Add<int>(a, b);return 0;
}
???????如果類型不匹配,編譯器會嘗試進行隱式類型轉換,如果無法轉換成功編譯器將會報錯。
5.模板參數的匹配原則
1.一個非模板函數可以和一個同名的函數模板同時存在,而且該函數模板還可以被實例化為這個非模板函數
// 專門處理int的加法函數
int Add(int left, int right)
{return left + right;
}
// 通用加法函數
template<class T>
T Add(T left, T right)
{return left + right;
}
void Test()
{Add(1, 2); // 與非模板函數匹配,編譯器不需要特化Add<int>(1, 2); // 調用編譯器特化的Add版本
}
2.對于非模板函數和同名函數模板,如果其他條件都相同,在調動時會優先調用非模板函數而不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數, 那么將選擇模板。
// 專門處理int的加法函數
int Add(int left, int right)
{return left + right;
}
// 通用加法函數
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{return left + right;
}
void Test()
{Add(1, 2); // 與非函數模板類型完全匹配,不需要函數模板實例化Add(1, 2.0); // 模板函數可以生成更加匹配的版本,編譯器根據實參生成更加匹配的Add函數
}
3.模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換
三、類模板
1.類模板的定義格式
template<class T1, class T2, ..., class Tn>
class 類模板名
{// 類內成員定義
};?
// 動態順序表
// 注意:Vector不是具體的類,是編譯器根據被實例化的類型生成具體類的模具
template<class T>
class Vector
{?
public :Vector(size_t capacity = 10): _pData(new T[capacity]), _size(0), _capacity(capacity){}// 使用析構函數演示:在類中聲明,在類外定義。~Vector();void PushBack(const T& data);void PopBack();// ...size_t Size() {return _size;}T& operator[](size_t pos){assert(pos < _size);return _pData[pos];}private:T* _pData;size_t _size;size_t _capacity;
};
// 注意:類模板中函數放在類外進行定義時,需要加模板參數列表
template <class T>
Vector<T>::~Vector()
{if(_pData)delete[] _pData;_size = _capacity = 0;
}
2.類模板的實例化
???????類模板實例化與函數模板實例化不同,類模板實例化需要在類模板名字后跟<>,然后將實例化的類型放在<>中即可,類模板名字不是真正的類,而實例化的結果才是真正的類。
// Vector類名,Vector<int>才是類型
Vector<int> s1;
Vector<double> s2;
四、非類型模板參數
模板參數分為:類型形參與非類型形參
???????類型形參:出現在模板參數列表中,跟在class或者typename之類的參數類型名稱。
???????非類型形參,就是用一個常量作為類(函數)模板的一個參數,在類(函數)模板中可將該參數當成常量來使用。
namespace goodQ
{// 定義一個模板類型的靜態數組template<class T, size_t N = 10>class array{public:T& operator[](size_t index) { return _array[index]; }const T& operator[](size_t index)const { return _array[index]; }size_t size()const { return _size; }bool empty()const { return 0 == _size; }private:T _array[N];size_t _size;};
}
注意:
???????浮點數、類對象以及字符串是不允許作為非類型模板參數的。
???????非類型的模板參數必須在編譯期就能確認結果。
五、模板的特化
1.概念
???????通常情況下,使用模板可以實現一些與類型無關的代碼,但對于一些特殊類型的可能會得到一些錯誤的結果,需要特殊處理,比如:實現了一個專門用來進行小于比較的函數模板
// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
int main()
{cout << Less(1, 2) << endl; // 可以比較,結果正確Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; // 可以比較,結果正確Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比較,結果錯誤return 0;
}
???????可以看到,Less絕對多數情況下都可以正常比較,但是在特殊場景下就得到錯誤的結果。上述示例中,p1指向的d1顯然小于p2指向的d2對象,但是Less內部并沒有比較p1和p2指向的對象內容,而比較的是p1和p2指針的地址,這就無法達到預期而錯誤。
???????此時,就需要對模板進行特化。
???????即:在原模板類的基礎上,針對特殊類型所進行特殊化的實現方式。模板特化中分為函數模板特化與類模板特化。
2.函數模板特化
函數模板的特化步驟:
? ? ? ? 1.必須要先有一個基礎的函數模板
????????2.關鍵字template后面接一對空的尖括號<>
????????3.函數名后跟一對尖括號,尖括號中指定需要特化的類型
????????4.函數形參表: 必須要和模板函數的基礎參數類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤。
// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
// 對Less函數模板進行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}
int main()
{cout << Less(1, 2) << endl;Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl;Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 調用特化之后的版本,而不走模板生成了return 0;}
注意:一般情況下如果函數模板遇到不能處理或者處理有誤的類型,為了實現簡單通常都是將該函數直接給出。
bool Less(Date* left, Date* right)
{return *left < *right;
}
???????該種實現簡單明了,代碼的可讀性高,容易書寫,因為對于一些參數類型復雜的函數模板,特化時特別給出,因此函數模板不建議特化。
3.類模板特化
1.全特化
???????全特化即是將模板參數列表中所有的參數都確定化。
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>" << endl; }
private:int _d1;char _d2;?
};
void TestVector()
{Data<int, int> d1;Data<int, char> d2;
}
2.偏特化
???????偏特化:任何針對模版參數進一步進行條件限制設計的特化版本。
比如對于以下模板類:
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}
private:T1 _d1;T2 _d2;}
偏特化有以下兩種表現方式:
? ?①部分特化
???????將模板參數類表中的一部分參數特化。
// 將第二個參數特化為int
template <class T1>
class Data<T1, int>
{
public:Data() {cout<<"Data<T1, int>" <<endl;}
private:T1 _d1;int _d2;
};
? ②參數更進一步的限制
???????偏特化并不僅僅是指特化部分參數,而是針對模板參數更進一步的條件限制所設計出來的一個特化版本。
//兩個參數偏特化為指針類型
template <typename T1, typename T2>
class Data <T1*, T2*>
{?
public:Data() {cout<<"Data<T1*, T2*>" <<endl;}private:T1 _d1;T2 _d2;
};
//兩個參數偏特化為引用類型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2){cout<<"Data<T1&, T2&>" <<endl;}private:const T1 & _d1;const T2 & _d2;?};
void test2 ()?
{Data<double , int> d1; // 調用特化的int版本Data<int , double> d2; // 調用基礎的模板?Data<int *, int*> d3; // 調用特化的指針版本Data<int&, int&> d4(1, 2); // 調用特化的指針版本
}
?六、模板分離編譯
1.什么是分離編譯
???????一個程序(項目)由若干個源文件共同實現,而每個源文件單獨編譯生成目標文件,最后將所有目標文件鏈接起來形成單一的可執行文件的過程稱為分離編譯模式。
2.模板的分離編譯
???????假如有以下場景,模板的聲明與定義分離開,在頭文件中進行聲明,源文件中完成定義:
// a.h
template<class T>
T Add(const T& left, const T& right);// a.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}// main.cpp
#include"a.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}
分析:
3.解決方法
??????1.將聲明和定義放到一個文件 “xxx.hpp” 里面或者xxx.h其實也是可以的。推薦使用這種。
??????2.模板定義的位置顯式實例化。這種方法不實用,不推薦使用。
【分離編譯擴展閱讀】http://blog.csdn.net/pongba/article/details/19130
4.模板總結
【優點】
1.模板復用了代碼,節省資源,更快的迭代開發,C++的標準模板庫(STL)因此而產生
2.增強了代碼的靈活性
【缺陷】
1.模板會導致代碼膨脹問題,也會導致編譯時間變長
2.出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤
結語:C++關于模板的分享到這里就結束了,希望本篇文章的分享會對大家的學習帶來些許幫助,如果大家有什么問題,歡迎大家在評論區留言,最后祝大家新的一年里學業有成,天天開心~~~?