目錄
?
1.泛型編程
2.函數模板
2.1函數模板概念
2.2實現函數模板
2.3模板的原理
2.4函數模板的實例化
2.4.1隱式實例化
2.4.2顯式初始化
2.5模板參數的匹配原則
3.類模板
3.1類模板定義格式
3.2類模板的實例化
4.總結
1.泛型編程
對廣泛的類型法寫代碼,我們之前寫的是一個專用的類型,如果想要用其他類型就要再寫一個專用的函數寫這個功能和實現方式與其他的差不多的代碼,非常麻煩,如:
//交換兩個整型
void Swap(int& a, int& b)
{int c = a;a = b;b = c;
}
//交換兩個浮點型
void Swap(double& a, double& b)
{double c = a;a = b;b = c;
}
我們可以發現,如果我們把第一個函數的int 改為double就可以直接變為第二個函數了,這樣寫起來很費時間,而且如果我們這個函數只用幾次的話,寫了這個是完全沒意義的,所以這有兩個不好的地方:
(1)重載的函數僅僅是類型不同,代碼復用率比較低,只要有新類型出現時,就需要用戶自己增加對應的函數;
(2)代碼的可維護性比較低,一個出錯可能所有的重載均出錯。
我們能否告訴編譯器一個模子,讓編譯器根據不同的類型利用該模子來生成代碼呢?
這就是所謂的泛型編程,編寫與類型無關的通用代碼,是代碼復用的一種手段,模板是泛型編程的基礎。
而這就需要我們來通過模板這個東西來實現,模板分為函數模板和類模板。
2.函數模板
2.1函數模板概念
函數模板代表了一個函數家族,該函數模板與類型無關,根據實參類型產生函數的特定類型版本。
2.2實現函數模板
模板的格式:template<typename T1,typename T2,……,typedname Tn>
其中T1、T2、……T3都是模板的參數,而這個template是關鍵字,typename是用來定義模板參數關鍵字,也可以使用class(切記不可用struct代替)。(到模板進階(C++初階最后的部分)會講class與typename的區別)
如,我們之前實現的交換函數可以這樣寫:
#include<iostream>
using namespace std;
//需要幾個類型就寫幾個參數
template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}
int main()
{//交換兩個整型int a, b;cin >> a >> b;Swap(a, b);cout << a << " " << b << endl;//交換兩個浮點型double c, d;cin >> c >> d;Swap(c, d);cout << c << " " << d << endl;//交換兩個字符型char e, f;cin >> e >> f;Swap(e, f);cout << e << " " << f << endl;return 0;
}
運行結果如下:
如果我們實參不是一個類型,則會報錯:
我們相較于之前寫交換函數只是在函數定義前加了一行模板的定義而已,并且把類型換為模板了而已。
2.3模板的原理
函數模板是一個藍圖,它本身并不是函數,是編譯器用使用方式產生特定具體類型函數的模具。
所以其實模板就是將本來應該我們做的重復的事情交給了編譯器。只是看實參類型用模板去匹配而已,實際上生成的兩個函數都不是一樣的,編譯器根據實參類型生成一個新函數,如果有不同的類型就再生成一個新函數,地址不同。如,我們之前實現的交換函數:
在編譯器編譯階段,對于模板函數的使用,編譯器需要根據傳入的實參類型來推演生成對應
類型的函數以供調用。比如:當用double類型使用函數模板時,編譯器通過對實參類型的推演,
將T確定為double類型,然后產生一份專門處理double類型的代碼,對于字符類型也是如此。
2.4函數模板的實例化
用不同類型的參數使用函數模板時,稱為函數模板的實例化。模板參數實例化分為:隱式實例化
和顯式實例化。
2.4.1隱式實例化
讓編譯器根據實參推演模板參數的實際類型。
和我之前實現的那個交換函數調用時一樣的形式,int a,b;Swap(a,b);則通過自動識別出a和b都是int類型,這就是隱式實例化。
但是如果我們給的是不同的類型,通過識別出兩個實參類型不同就會報錯,這個時候我們要么就改實參為同一類型,要么就用顯式實例化,要么就要一個類型強制類型轉換為另一類型:
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int main()
{int a = 3, b = 2;double c = 3.024, d = 4.501;cout << Add(a, b) << endl;cout << Add(c, d) << endl;cout << Add(a, (int)c) << endl;cout << Add((double)a, c) << endl;return 0;
}
運行結果如下:
建議不要去運行之前的交換排序代碼,因為它我測試過不僅顯式實例化沒有用處,強制類型轉換也沒有用處,至于原因感興趣的可以去探討一下。
當然這里涉及到精度丟失的問題,所以建議Add這類的函數還是加一個參數寫成如下形式:
但是返回值你也要改一下,要么返回double類型要么返回int類型!
template<class T1,class T2>
T1 Add(const T1& left, const T2& right)
{return left + right;
}
這是一個演示的,如果之后學的更多了,也可能會有其他類型的返回值等等。
2.4.2顯式初始化
我們之前的代碼也可以改為如下形式,這樣的形式也不用強制類型轉換了,直接用<>里面的類型進行生成函數的操作。
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int main()
{int a = 2, b = 3;double c = 4.53, d = 2.54;//指定參數為int類型(形參)cout << Add<int>(a, c) << endl;//指定參數為double類型(形參)cout << Add<double>(b, d) << endl;return 0;
}
運行結果如下:
當然,運行時輸出的會有一個提示:
也就是說double轉換為int會有小數點后的數據丟失,所以建議把int轉換為double類型!因為這樣算出來的結果才更加準確。
當然不是所有的都可以用兩種實例化的方式:
template<class T>
T* func(size_t n)
{return new T[n];
}
因為形參沒有任何的能代表T的類型,也就是說不能通過類型推理來知道T的類型,所以就只能進行顯式實例化了。
2.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版本
}
若有int類型的,編譯器會先用現成的(非模板),否則用模板的。但若我們不想用現成的我們就可以直接顯式實例化。
(2)對于非模板函數和同名函數模板,如果其他條件都相同,在調動時會優先調用非模板函數而
不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數, 那么將選擇模板。
如:
#include<iostream>
using namespace std;
// 專門處理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);// 模板函數可以生成更加匹配的版本,編譯器根據實參生成更加匹配的
}
只要非模板函數的形參類型與實參全部匹配就用非模板函數,但是如果是兩個參數類型不一樣的情況,發現模板函數用的不會使精度丟失,所以直接用了模板函數了。
(3)?模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換。
3.類模板
3.1類模板定義格式
template<class T1, class T2, ..., class Tn>
class 類模板名
{
// 類內成員定義
}; ???
和之前的方式一樣,當然,不是因為它是類就只能用class來定義參數,也可以用typename定義參數,只是現在沒學到后面,不知道二者的區別而已。
我們可以把之前實現棧來改為這個方式:
#include<iostream>
using namespace std;
// 類模版
template<typename T>
class Stack
{
public:Stack(size_t capacity = 4){_array = new T[capacity];_capacity = capacity;_size = 0;}void Push(const T& data);
private:T* _array;size_t _capacity;size_t _size;
};
// 模版不建議聲明和定義分離到兩個文件.h 和.cpp會出現鏈接錯誤,具體原因后面會講
template<class T>
void Stack<T>::Push(const T& data)
{// 擴容_array[_size] = data;++_size;
}
我們定義這個棧的時候如果成員函數聲明和定義分離時不能忘記加這一行模板了,否則會出現:
3.2類模板的實例化
Stack<int> st1; ???// int
Stack<double> st2; // double
由于類模板不能通過隱式實例化來確定形參類型,所以必須用顯式實例化,且注意:Stack是類名,Stack<int>是類型。
其次再補充一下之前的:
template<class T>
void Stack<T>::Push(const T& data)
{// 擴容_array[_size] = data;++_size;
}
模板參數不能是憑空出來的,故指定類域得加模板參數,否則T是用不了的,因為模板是給Stack類用的模板參數,因為模板參數是屬于這個Stack類型,因為T可以為任意類型,若去掉,則本質上Push要實例化出多個,這樣就不行了。
4.總結
這是模板初階的東西,所以很多東西是沒有講解完全的,需要到模板進階來講清楚,但是模板進階要很晚了,下面這張圖片是C++初階的全部內容,所以我們現在已經C++初階過了一半了,之后難度會比較高(第8節開始),所以需要深入學習也需要持之以恒哦!
加油吧,相信自己能學會才是最好的安慰!喜歡的可以一鍵三連哦,下一節很簡單,所以應該很快就發出來的!
?