Cpp模板筆記
文章目錄
- Cpp模板筆記
- 1. 為什么要定義模板
- 2. 模板的定義
- 2.1 函數模板
- 2.1.1 函數模板的重載
- 2.1.2 頭文件與實現文件形式(重要)
- 2.1.3 模板的特化
- 2.1.4 模板的參數類型
- 2.1.5 成員函數模板
- 2.1.6 使用模板的規則
- 2.2 類模板
- 2.3 可變參數模板
模板是一種通用的描述機制,使用模板允許使用通用類型來定義函數或類。在使用時,通用類型可被具體的類型,如 int、double 甚至是用戶自定義的類型來代替。模板引入一種全新的編程思維方式,稱為“泛型編程”或“通用編程”。
1. 為什么要定義模板
像C/C++/Java等語言,是編譯型語言,先編譯后運行。它們都有一個強大的類型系統,也被稱為強類型語言,希望在程序執行之前,盡可能地發現錯誤,防止錯誤被延遲到運行時。所以會對語言本身的使用造成一些限制,稱之為靜態語言。
與之對應的,還有動態語言,也就是解釋型語言。如javascript/python/Go,在使用的過程中,一個變量可以表達多種類型,也稱為弱類型語言。因為沒有編譯的過程,所以相對更難以調試。
強類型程序設計中,參與運算的所有對象的類型在編譯時即確定下來,并且編譯程序將進行嚴格的類型檢查。為了解決強類型的嚴格性和靈活性的沖突,也就是在嚴格的語法要求下盡可能提高靈活性,有以下方式:
例如,想要實現能夠處理各種類型參數的加法函數
以前我們需要進行函數重載(函數名相同,函數參數不同)
int add(int x, int y) {return x + y; }double add(double x, double y) {return x + y; }long add(long x, long y) {return x + y; }string add(string x, string y) {return x + y; }
在使用時看起來只需要調用add函數,傳入不同類型的參數就可以進行相應的計算了,很方便。
但是程序員為了這種方便,實際上要定義很多個函數來處理各種情況的參數。
模板(將數據類型作為參數)
上面的問題用函數模板的方式就可以輕松解決:
//希望將類型參數化 //使用class關鍵字或typename關鍵字都可以 template <class T> T add(T x, T y) {return x + y; }int main(void){cout << add(1,2) << endl;cout << add(1.2,3.4) << endl;return 0; }
函數模板的優點:
不需要程序員定義出大量的函數,在調用時實例化出對應的模板函數,更“智能”
2. 模板的定義
模板作為實現代碼重用機制的一種工具,它可以實現類型參數化,也就是把類型定義為參數,從而實現了真正的代碼可重用性。
模板可以分為兩類,一個是函數模版,另外一個是類模板。通過參數實例化定義出具體的函數或類,稱為模板函數或模板類。模板的形式如下:
// 形式 template <typename/class T1, typename T2,...> // T1,T2稱為類型參數或者模板參數
模板參數是一個更大的概念,包含了類型參數和非類型參數,這里的T1/T2屬于類型參數,代表了類型。
模板發生的時機是在編譯時
模板本質上就是一個代碼生成器,它的作用就是讓編譯器根據實際調用來生成代碼。
編譯器去處理時,實際上由函數模板生成了多個模板函數,或者由類模板生成了多個模板類。
#include <iostream> using std::cout; using std::endl;template <class T> T add(T x, T y) {return x + y; }// 模板本質上就是一個代碼生成器 // 編譯器處理時,實際上由函數模板生成了多個模板函數 // 或者多個模板類 #if 0 // 編譯器生成的,而非顯式定義的 int add(int x, int y) {return x + y; }double add(double x, double y) {return x + y; } #endifint main() {cout << add(1, 2) << endl;cout << add(1.2, 1.3) << endl;return 0; }
2.1 函數模板
由函數模板到模板函數的過程稱之為實例化
函數模板 --》 生成相應的模板函數 --》編譯 —》鏈接 --》可執行文件
以下程序實際上可以理解為生成了四個模板函數
template <class T> T add(T t1,T t2) { return t1 + t2; }void test0(){short s1 = 1, s2 = 2;int i1 = 3, i2 = 4;long l1 = 5, l2 = 6;double d1 = 1.1, d2 = 2.2;cout << "add(s1,s2): " << add(s1,s2) << endl;cout << "add(i1,i2): " << add(i1,i2) << endl;cout << "add(l1,l2): " << add(l1,l2) << endl;cout << "add(d1,d2): " << add(d1,d2) << endl; }
上述代碼中在進行模板實例化時,并沒有指明任何類型,函數模板在生成模板函數時通過傳入的參數類型確定出(推導出)模板類型,這種做法稱為隱式實例化。
我們在使用函數模板時還可以在函數名之后直接寫上模板的類型參數列表,指定類型,這種用法稱為顯式實例化。
template <class T> T add(T t1,T t2) { return t1 + t2; }void test0(){int i1 = 3, i2 = 4;cout << "add(i1,i2): " << add<int>(i1,i2) << endl; //顯式實例化 }
2.1.1 函數模板的重載
函數模板的重載分為:
(1)函數模板與函數模板重載**(謹慎使用)**
(2)函數模板與普通函數重載
函數模板與函數模板重載
如果一個函數模板無法實例化出合適的模板函數(去進行顯式實例化也有一些問題)的時候,可以再給出另一個函數模板
//函數模板與函數模板重載 //模板參數個數不同,ok template <class T> //模板一 T add(T t1,T t2) { return t1 + t2; }template <class T1, class T2> //模板二 T1 add(T1 t1, T2 t2) { return t1 + t2; }double x = 9.1; int y = 10; cout << add(x,y) << endl; //會調用模板二生成的模板函數,不會損失精度//猜測一下,這種調用方式返回的結果是什么呢? cout << add(y,x) << endl;
上面輸出的結果會是19,調用的仍然是模板二,但返回類型和函數的第一個參數類型相同,采用隱式實例化時根據參數推導類型,推導出返回類型應該是int型。
#include <iostream> using std::cout; using std::endl;template <class T> T add(T t1, T t2) {cout << "模板一" << endl;return t1 + t2; }template <class T1, class T2> T1 add(T1 t1, T2 t2) {cout << "模板二" << endl;return t1 + t2; }int main() { int x = 8;double y = 9.8;int z = 10;cout << add(x, y) << endl; // 17 調用模板二cout << add(y, x) << endl; // 17.8 調用模板二cout << add<int>(y, x) << endl; // 17 調用模板一cout << add(x, z) << endl; // 18 調用模板一return 0; }
事實上,在一個源文件中定義多個通用模板的寫法應該謹慎使用(盡量避免),如果實在需要也盡量使用隱式實例化的方式進行調用,編譯器會選擇參數類型最匹配的模板(通常是參數類型需要更少轉換的模板)。
函數模板與函數模板重載:
(1)首先,名稱必須相同(顯然)
(2)模板參數列表中的模板參數在函數中所處位置不同 —— 但不建議進行這樣的重載。
(3)模板參數的個數不一樣時,可以構成重載(相對常見)
函數模板與普通函數重載
普通函數優先于函數模板執行——因為普通函數更快
編譯器掃描到函數模板的實現時并沒有生成函數,只有掃描到下面調用add函數的語句時,給add傳參,知道了參數的類型,這才生成一個相應類型的模板函數——模板參數推導。所以使用函數模板一定會增加編譯的時間。
此處,就直接調用了普通函數,而不去采用函數模板,因為更直接,效率更高。
//函數模板與普通函數重載 template <class T1, class T2> T1 add(T1 t1, T2 t2) { return t1 + t2; }short add(short s1, short s2){ cout << "add(short,short)" << endl; return s1 + s2; }void test1(){ short s1 = 1, s2 = 2; cout << add(s1,s2) << endl; //調用普通函數 }
如果沒有普通函數,就會調用上面的函數模板,實例化出相應的模板函數。盡管s1/s2的類型相同,也是可以使用該模板的。
—— T1/T2并不一定非得是不同類型,能推導出即可。
當然,如果采用顯示實例化的方式調用,肯定是調用函數模板。
2.1.2 頭文件與實現文件形式(重要)
為什么C++標準頭文件沒有所謂的.h后綴?
在一個源文件中,函數模板的聲明與定義分離是可以的,即使把函數模板的實現放在調用之下也是ok的,與普通函數一致。
//函數模板的聲明 template <class T> T add(T t1, T t2);void test1(){ int i1 = 1, i2 = 2;cout << add(i1,i2) << endl; }//函數模板的實現 template <class T> T add(T t1, T t2) {return t1 + t2; }
如果在不同文件中進行分離
如果像普通函數一樣去寫出了頭文件、實現文件、測試文件,編譯時會出現未定義報錯
//add.h template <class T> T add(T t1, T t2);//add.cc #include "add.h" template <class T> T add(T t1, T t2) { return t1 + t2; }//testAdd.cc #include "add.h" void test0(){ int i1 = 1, i2 = 2; cout << add(i1,i2) << endl; }
- 單獨編譯“實現文件”
add.cc
,使之生成目標文件,查看目標文件,會發現沒有生成與add名稱相關的函數。- 單獨編譯測試文件
testAdd.cc
,發現有與add名稱相關的函數,但是沒有地址,這就表示只有聲明。看起來和普通函數的情況有些不一樣。
從原理上進行分析,函數模板定義好之后并不會直接產生一個具體的模板函數,只有在調用時才會實例化出具體的模板函數。
解決方法 —— 在”實現文件“中要進行調用,因為有了調用才有推導,才能由函數模板生成需要的函數
//add.cc template <class T> T add(T t1, T t2) { return t1 + t2; }//在這個文件中如果只是寫出了函數模板的實現 //并沒有調用的話,就不會實例化出模板函數 void test1(){ cout << add(1,2) << endl; }
但是在“實現文件”中對函數模板進行了調用,這種做法不優雅 。
設想:如果在測試文件調用時,在推導的過程中,看到的是完整的模板的代碼,那么應該可以解決問題
//add.h template <class T> T add(T t1, T t2);#include "add.cc"
可以在頭文件中加上#include “add.cc”,即使實現文件中沒有調用函數模板,單獨編譯testAdd.cc,也可以發現問題已經解決。
因為本質上相當于把函數模板的定義寫到了頭文件中。
總結:
對模板的使用,必須要拿到模板的全部實現,如果只有一部分,那么推導也只能推導出一部分,無法滿足需求。
換句話說,就是模板的使用過程中,其實沒有了頭文件和實現文件的區別,在頭文件中也需要獲取模板的完整代碼,不能只有一部分。
C++的標準庫都是由模板開發的,所以經過標準委員會的商討,將這些頭文件取消了后綴名,與C的頭文件形成了區分;這些實現文件的后綴名設為了tcc
2.1.3 模板的特化
在函數模板的使用中,有時候會有一些通用模板處理不了的情況,我們可以定義普通函數或特化模板來解決。雖然普通函數的優先級更高,但有些場景下是必須使用特化模板的。它的形式如下:
- template后直接跟 <> ,里面不寫類型
- 在函數名后跟 <> ,其中寫出要特化的類型
比如,add函數模板在處理C風格字符串相加時遇到問題,如果只是簡單地讓兩個C風格字符串進行+操作,會報錯。
可以利用特化模板解決:
#include <iostream> #include <string.h> using std::cout; using std::endl;template <class T> T add(T t1, T t2) {cout << "通用模板" << endl;return t1 + t2; }// 模板的特化 通用模板處理不了的情況 template <> const char* add<const char*>(const char* p1, const char* p2) {char* pret = new char[strlen(p1) + strlen(p2) + 1]();strcpy(pret, p1);strcat(pret, p2);cout << "string特化模板" << endl;return pret; }int main() { short x = 10;short y = 20;// 普通函數優于函數模板執行 因為普通函數更快cout << add(x, y) << endl;cout << add(1.1, 2.2) << endl;const char* p = add("hello", "world");cout << p << endl;delete [] p;return 0; }
輸出結果:
通用模板 30 通用模板 3.3 string特化模板 helloworld
使用模板特化時,必須要先有基礎的函數模板
如果沒有模板的通用形式,無法定義模板的特化形式。因為特化模板就是為了解決通用模板無法處理的特殊類型的操作。
特化版本的函數名、參數列表要和原基礎的函數模板相同,避免不必要的錯誤。
2.1.4 模板的參數類型
類型參數
之前的T/T1/T2等等稱為模板參數,也稱為類型參數,類型參數T可以寫成任何類型
非類型參數
需要是整型數據, char/short/int/long/size_t等
不能是浮點型,float/double不可以
定義模板時,在模板參數列表中除了類型參數還可以加入非類型參數。
此時,調用模板時需要傳入非類型參數的值
template <class T,int kBase> T multiply(T x, T y){return x * y * kBase; }void test0(){ int i1 = 3,i2 = 4; //此時想要進行隱式實例化就不允許了,因為kBase無法推導 cout << multiply(i1,i2) << endl; //error cout << multiply<int,10>(i1,i2) << endl; //ok }
可以給非類型參數賦默認值,有了默認值后調用模板時就可以不用傳入這個非類型參數的值
template <class T,int kBase = 10> T multiply(T x, T y){return x * y * kBase; }void test0(){int i1 = 3,i2 = 4;cout << multiply<int,10>(i1,i2) << endl;cout << multiply<int>(i1,i2) << endl;cout << multiply(i1,i2) << endl; }
函數模板的模板參數賦默認值與普通函數相似,從右到左,右邊的非類型參數賦了默認值,左邊的類型參數也可以賦默認值
template <class T = int,int kBase = 10> T multiply(T x, T y){ return x * y * kBase; }void test0(){double d1 = 1.2, d2 = 1.2;cout << multiply<int>(d1,d2) << endl; //okcout << multiply(d1,d2) << endl; //ok }
第一次的調用T代表了int,這個很好理解,因為使用模板時指定了類型參數。那么第二次也會代表int嗎?
—— 結果發現返回的結果是double型的。
我們可以得出結論
優先級:指定的類型 > 推導出的類型 > 類型的默認參數
總結:在沒有指定類型時,模板參數的默認值(不管是類型參數還是非類型參數)只有在沒有足夠的信息用于推導時起作用。當存在足夠的信息時,編譯器會按照實際參數的類型去調用,不會受到默認值的影響。
2.1.5 成員函數模板
上面我們認識了普通的函數模板,實際上,在一個普通類中也可以定義成員函數模板,如下:
#include <iostream> using std::cout; using std::endl;class Point{ public:Point(double x, double y): _ix(x), _iy(y){}// 成員函數模板不能加上virtual修飾template <class T>T convert() {return (T)_ix + (T)_iy;} private:double _ix;double _iy; };int main() {Point pp(1.4, 2.3);cout << pp.convert<int>() << endl;return 0; }
在convert函數模板中可以訪問Point的數據成員,說明成員函數模板的使用原理同普通函數模板一樣,在調用時會實例化出一個模板成員函數。普通的成員函數會有隱含的this指針作為參數,這里生成的模板成員函數中也會有。如果定義一個static的成員函數模板,那么在其中就不能訪問非靜態數據成員。
但是要注意:成員函數模板不能加上virtual修飾,否則編譯器報錯。
因為函數模板是在編譯時生成函數,而虛函數機制起作用的時機是在運行時。
—— 如果要將成員函數模板在類之外進行實現,需要注意帶上模板的聲明
class Point { public:Point(double x,double y): _x(x), _y(y){}template <class T>T add(T t1);private:double _x;double _y; };template <class T> T Point::add(T t1) {return _x + _y + t1; }
2.1.6 使用模板的規則
- 在一個模塊中定義多個通用模板的寫法應該謹慎使用;
- 調用函數模板時盡量使用隱式調用,讓編譯器推導出類型;
- 無法使用隱式調用的場景只指定必須要指定的類型;
2.2 類模板
一個類模板允許用戶為類定義個一種模式,使得類中的某些數據成員、默認成員函數的參數,某些成員函數的返回值,能夠取任意類型(包括內置類型和自定義類型)。
如果一個類中的數據成員的數據類型不能確定,或者是某個成員函數的參數或返回值的類型不能確定,就需要將此類聲明為模板,它的存在不是代表一個具體的、實際的類,而是代表一類類。
類模板的定義形式如下:
template <class/typename T, ...> class 類名{ //類定義...... };
實際上,我們之前已經多次見到了類模板,打開c++參考文檔,發現vector、set、map等等都是使用類模板定義的。
類模板定義
示例,用類模板的方式實現一個Stack類,可以存放任意類型的數據
——使用函數模板實例化模板函數使用類模板實例化模板類
template <class T, int kCapacity = 10> class Stack { public:Stack(): _top(-1), _data(new T[kCapacity]()){cout << "Stack()" << endl;}~Stack(){if(_data){delete [] _data;_data = nullptr;}cout << "~Stack()" << endl;}bool empty() const;bool full() const;void push(const T &);void pop();T top(); private:int _top;T * _data; };
類模板的成員函數如果放在類模板定義之外進行實現,需要注意
(1)需要帶上template模板形參列表(如果有默認參數,此處不要寫,寫在聲明時就夠了)
(2)在添加作用域限定時需要寫上完整的類名和模板實參列表
template <class T, int kCapacity> bool Stack<T,kCapacity>::empty() const{return _top == -1; }
定義了這樣一個類模板后,就可以去創建存放各種類型元素的棧
void test() {Stack<string, 20> ss;Stack<int> st;Stack<> st2; }
2.3 可變參數模板
可變參數模板(variadic templates)是 C++11 新增的最強大的特性之一,它對參數進行了高度泛化,它能表示0到任意個數、任意類型的參數。由于可變參數模板比較抽象,使用起來需要一定的技巧,所以它也是 C++11 中最難理解和掌握的特性之一。
回想一下C語言中的printf函數,其實是比較特殊的。printf函數的參數個數可能有很多個,用…表示,參數的個數、類型、順序可以隨意,可以寫0到任意多個參數。
可變參數模板和普通模板的語義是一樣的,只是寫法上稍有區別,聲明可變參數模板時需要在typename 或 class 后面帶上省略號 “…”
template <class ...Args> void func(Args ...args);//普通函數模板做對比 template <class T1,class T2> void func(T1 t1, T2 t2);
Args叫做模板參數包,相當于將 T1/T2/T3/…等類型參數打了包
args叫做函數參數包,相當于將 t1/t2/t3/…等函數參數打了包
省略號寫在參數包的左邊,代表打包
例如,我們在定義一個函數時,可能有很多個不同類型的參數,不適合一一寫出,就可以使用可變參數模板的方法。
——試驗:希望打印出傳入的參數的內容
就需要對參數包進行解包。每次解出第一個參數,然后遞歸調用函數模板,直到遞歸出口
#include <iostream> using std::cout; using std::endl; // 遞歸的出口 void print() {cout << endl; }// 可變參數模板 template <class T, class ...Args> void print(T x, Args ...args) {cout << x << " ";print(args...); // 省略號寫在參數包的右邊,代表解包 }int main() {print(1, "hello", 1.34, "world");return 0; }
省略號寫在參數包的右邊,代表解包
如果沒有準備遞歸的出口,那么在可變參數模板中解包解到print()時,不知道該調用什么,因為這個模板至少需要一個參數。
遞歸的出口可以使用普通函數或者普通的函數模板,但是規范操作是使用普通函數。
(1)盡量避免函數模板之間的重載;
(2)普通函數的優先級一定高于函數模板,更不容易出錯。