模板是泛型編程的基礎,泛型編程及以一種獨立於任何特定類型的方式編寫代碼。
模板是創建泛型類或函數的藍圖或公式。庫容器:比如迭代器,都是泛型編程的例子,他們都使用類模板的概念, 每個容器都有一個單一的定義,比如向量,我們可以定義許多不同類型的向量,比如vector <int>或 vector <string>。
模板是一種對類型進行參數化的工具,通常有兩種形式:函數模板和類模板;
(1) 函數模板針對僅參數類型不同的函數;
(2) 類模板針對僅數據成員和成員函數類型不同的類。
使用模板讓程序員能夠編寫與類型無關的代碼。比如:編寫一個交換兩個int型的swap函數,這個函數就只能實現int型,對double,字符這些類型無法實現,要實現這些類型的交換就要重新寫另一個swap函數。使用模板的目的就是要讓這程序的實現與類型無關,比如一個swap模板函數,即可以實現int型,又可以實現double型的交換。
注意:模板的聲明或定義只能在全局,命名空間或類范圍內進行。即不能在局部范圍,函數內進行,比如不能在main函數中聲明或定義一個模板。
一、函數模板
在函數重載中,為了交換不同類型變量的值,通過函數重載定義了四個名字相同,參數列表不同的函數,如下所示:
//交換 int 變量的值
void Swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
//交換 float 變量的值
void Swap(float *a, float *b){
float temp = *a;
*a = *b;
*b = temp;
}
這些函數雖然在調用時方便了一些,但從本質上說還是定義了功能相同、函數體相同的函數,只是數據的類型不同而已,這看起來有點浪費代碼,能不能把它們壓縮成一個函數呢?
能!可以借助本節講的函數模板。
數據的值可以通過函數參數傳遞,在函數定義時數據的值是未知的,只有等到函數調用時接收了實參才能確定其值。這就是值的參數化。
在C++中,數據的類型也可以通過參數傳遞,在函數定義時可以不指明具體的數據類型,當發生函數調用時,編譯器可以根據傳入的實參自動推斷數據類型。這就是類型的參數化。
值(Value)和類型(Type)是數據的兩個主要特征,它們在C++中都可以被參數化。
函數模板,實際上是建立一個通用函數,它所用到的數據的類型(包括返回值類型、形參類型、局部變量類型)可以不具體指定,而是用一個虛擬的類型來代替(實際上是用一個標識符來占位),等發生函數調用時再根據傳入的實參來逆推出真正的類型。
這個通用函數就稱為函數模板。在函數模板中,數據的值和類型都被參數化了,發生函數調用時編譯器會根據傳入的實參來推演形參的值和類型。換個角度說,函數模板除了支持值的參數化,還支持類型的參數化。
一但定義了函數模板,就可以將類型參數用於函數定義和函數聲明了。說得直白一點,原來使用 int、float、char 等內置類型的地方,都可以用類型參數來代替。
下面我們就來實踐一下,將上面的Swap() 函數壓縮為一個函數模板:
#include <iostream>
using namespace std;
template<typename T>
void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
int main(){
//交換 int 變量的值
int n1 = 100, n2 = 200;
Swap(n1, n2);
cout<<n1<<", "<<n2<<endl;
//交換 float 變量的值
float f1 = 12.5, f2 = 56.93;
Swap(f1, f2);
cout<<f1<<", "<<f2<<endl;
return 0;
}
定義模板函數的語法:
template <typename 類型參數1 , typename 類型參數2 , ...>
返回值類型 函數名(形參列表)
{
//在函數體中可以使用類型參數
}
// 類型參數可以有多個,它們之間以逗號,分隔。類型參數列表以< >包圍,形式參數列表以( )包圍。
注意:模板中typename和class關鍵字的區別?
template<typename T> class Widget; // uses "typename"
template<class T> class Widget; // uses "class"
typename關鍵字也可以使用class關鍵字替代。C++早期對模板的支持並不嚴謹,沒有引入新的關鍵字,而是用class來指明類型參數,但是class關鍵字本來已經用在類的定義中了,這樣做顯得不太友好,所以后來C++又引入了一個新的關鍵字typename,專門用來定義類型參數。不過至今仍然有很多代碼在使用class關鍵字,包括C++標准庫、一些開源程序等。
但是,《Effective C++》中建議我們盡量使用typename而不是class來聲明一個模板。因為class給人的感覺總像是T應該是一個類,或者說是用戶自定義的類型,而typename則更像是表明T可以是任意一種類型。雖然在C++編譯器看來,這兩種聲明方式沒有任何的區別,其意義是完全一樣的。
不過,typename關鍵字在模板中還有另一種用法: 使用嵌套依賴類型
class MyArray
{
public:
typedef int LengthType;
.....
}
template<class T>
void MyMethod(T myarr)
{
typedef typename T::LengthType LengthType;
LengthType length = myarr.GetLength;
}
這個時候typename的作用就是告訴c++編譯器,typename后面的字符串為一個類型名稱,而不是成員函數或者成員變量,這個時候如果前面沒有typename,編譯器沒有任何辦法知道T::LengthType是一個類型還是一個成員名稱(靜態數據成員或者靜態函數),所以編譯不能夠通過。
二、類模板
template<typename 類型參數1 , typename 類型參數2 , …> class 類名
{
//TODO:
};
一但聲明了類模板,就可以將類型參數用於類的成員函數和成員變量了。換句話說,原來使用 int、float、char 等內置類型的地方,都可以用類型參數來代替。
假如我們現在要定義一個類來表示坐標,要求坐標的數據類型可以是整數、小數和字符串,例如:
x = 10、y = 10
x = 12.88、y = 129.65
x = "東經180度"、y = "北緯210度"
這個時候就可以使用類模板,請看下面的代碼:
template<typename T1, typename T2> //這里不能有分號
class Point
{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const; //獲取x坐標
void setX(T1 x); //設置x坐標
T2 getY() const; //獲取y坐標
void setY(T2 y); //設置y坐標
private:
T1 m_x; //x坐標
T2 m_y; //y坐標
};
// x 坐標和 y 坐標的數據類型不確定,借助類模板可以將數據類型參數化,這樣就不必定義多個類了。
注意:模板頭和類頭是一個整體,可以換行,但是中間不能有分號。
上面的代碼僅僅是類的聲明,我們還需要在類外定義成員函數。在類外定義成員函數時仍然需要帶上模板頭,格式為:
template<typename 類型參數1 , typename 類型參數2 , …>
返回值類型 類名<類型參數1 , 類型參數2, ...>::函數名(形參列表)
{
//TODO:
}
// 第一行是模板頭,第二行是函數頭,它們可以合並到一行,不過為了讓代碼格式更加清晰,一般是將它們分成兩行。
下面就對 Point 類的成員函數進行定義:
template<typename T1, typename T2> //模板頭
T1 Point<T1, T2>::getX() const /*函數頭*/ {
return m_x;
}
template<typename T1, typename T2>
void Point<T1, T2>::setX(T1 x){
m_x = x;
}
template<typename T1, typename T2>
T2 Point<T1, T2>::getY() const{
return m_y;
}
template<typename T1, typename T2>
void Point<T1, T2>::setY(T2 y){
m_y = y;
}
使用類模板創建對象
上面的兩段代碼完成了類的定義,接下來就可以使用該類創建對象了。使用類模板創建對象時,需要指明具體的數據類型。請看下面的代碼:
Point<int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, char*> p3(12.4, "東經180度");
// 與函數模板不同的是,類模板在實例化時必須顯式地指明數據類型,編譯器不能根據給定的數據推演出數據類型。
除了對象變量,我們也可以使用對象指針的方式來實例化:
Point<float, float> *p1 = new Point<float, float>(10.6, 109.3);
Point<char*, char*> *p = new Point<char*, char*>("東經180度", "北緯210度");
需要注意的是,賦值號兩邊都要指明具體的數據類型,且要保持一致。下面的寫法是錯誤的:
//賦值號兩邊的數據類型不一致
Point<float, float> *p = new Point<float, int>(10.6, 109);
//賦值號右邊沒有指明數據類型
Point<float, float> *p = new Point(10.6, 109);
綜合示例
【實例1】將上面的類定義和類實例化的代碼整合起來,構成一個完整的示例,如下所示:
#include <iostream>
using namespace std;
template<class T1, class T2> //這里不能有分號
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const; //獲取x坐標
void setX(T1 x); //設置x坐標
T2 getY() const; //獲取y坐標
void setY(T2 y); //設置y坐標
private:
T1 m_x; //x坐標
T2 m_y; //y坐標
};
template<class T1, class T2> //模板頭
T1 Point<T1, T2>::getX() const /*函數頭*/ {
return m_x;
}
template<class T1, class T2>
void Point<T1, T2>::setX(T1 x){
m_x = x;
}
template<class T1, class T2>
T2 Point<T1, T2>::getY() const{
return m_y;
}
template<class T1, class T2>
void Point<T1, T2>::setY(T2 y){
m_y = y;
}
int main(){
Point<int, int> p1(10, 20);
cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
Point<int, char*> p2(10, "東經180度");
cout<<"x="<<p2.getX()<<", y="<<p2.getY()<<endl;
Point<char*, char*> *p3 = new Point<char*, char*>("東經180度", "北緯210度");
cout<<"x="<<p3->getX()<<", y="<<p3->getY()<<endl;
return 0;
}
// x=10, y=20
// x=10, y=東經180度
// x=東經180度, y=北緯210度
【實例2】用類模板實現可變長數組。
#include <iostream>
#include <cstring>
using namespace std;
template <class T>
class CArray
{
int size; //數組元素的個數
T *ptr; //指向動態分配的數組
public:
CArray(int s = 0); //s代表數組元素的個數
CArray(CArray & a);
~CArray();
void push_back(const T & v); //用於在數組尾部添加一個元素v
CArray & operator=(const CArray & a); //用於數組對象間的賦值
T length() { return size; }
T & operator[](int i)
{//用以支持根據下標訪問數組元素,如a[i] = 4;和n = a[i]這樣的語句
return ptr[i];
}
};
template<class T>
CArray<T>::CArray(int s):size(s)
{
if(s == 0)
ptr = NULL;
else
ptr = new T[s];
}
template<class T>
CArray<T>::CArray(CArray & a)
{
if(!a.ptr) {
ptr = NULL;
size = 0;
return;
}
ptr = new T[a.size];
memcpy(ptr, a.ptr, sizeof(T ) * a.size);
size = a.size;
}
template <class T>
CArray<T>::~CArray()
{
if(ptr) delete [] ptr;
}
template <class T>
CArray<T> & CArray<T>::operator=(const CArray & a)
{ //賦值號的作用是使"="左邊對象里存放的數組,大小和內容都和右邊的對象一樣
if(this == & a) //防止a=a這樣的賦值導致出錯
return * this;
if(a.ptr == NULL) { //如果a里面的數組是空的
if( ptr )
delete [] ptr;
ptr = NULL;
size = 0;
return * this;
}
if(size < a.size) { //如果原有空間夠大,就不用分配新的空間
if(ptr)
delete [] ptr;
ptr = new T[a.size];
}
memcpy(ptr,a.ptr,sizeof(T)*a.size);
size = a.size;
return *this;
}
template <class T>
void CArray<T>::push_back(const T & v)
{ //在數組尾部添加一個元素
if(ptr) {
T *tmpPtr = new T[size+1]; //重新分配空間
memcpy(tmpPtr,ptr,sizeof(T)*size); //拷貝原數組內容
delete []ptr;
ptr = tmpPtr;
}
else //數組本來是空的
ptr = new T[1];
ptr[size++] = v; //加入新的數組元素
}
int main()
{
CArray<int> a;
for(int i = 0;i < 5;++i)
a.push_back(i);
for(int i = 0; i < a.length(); ++i)
cout << a[i] << " ";
cout << endl;
return 0;
}
// 0 1 2 3 4