淺談C++模板機制
一、 什麼是模板?
1. 模板(Template)可以看做成對於某一類問題一種通用的解決方案,而實現的具體細節則需要根據實際問題對模板做出調整和優化。
2. 如我們在使用Word進行文檔處理時,模板決定了文檔的基本結構和文檔的設置,如果你想要某種風格的文檔結構,你可以對模板進行修改。模板提供了更加通用、靈活的解決方案。
3. 在C++中,模板是泛型編程的基礎,是創建類和函數的藍圖或公式。
二、 從函數模板談起
1. 從一個實例出發
假設我們想設計一個函數根據輸入參數的類型來返回這個參數的絕對值,如果按照C語言的做法,我們會設計如下幾個函數:
int fabsInt(int arg);
double fabsDouble(double arg);
float fabsFloat(float arg);
這樣設計出的三個函數雖然函數定義不同,但是完成的功能卻是相同的,這樣的設計比較麻煩。C++提供了函數模板使得這一問題的解決方案更加通用、靈活。
源代碼:
template<typename T>
T fabs(T arg)
{
return arg>0?arg:(-arg);
}
測試:
inta;
doubleb;
floatc;
cout<<"a=";
cin>>a;
cout<<"b=";
cin>>b;
cout<<"c=";
cin>>c;
cout<<"|"<<a<<"|"<<"="<<fabs(a)<<endl;
//執行int fabs(int arg)
cout<<"|"<<b<<"|"<<"="<<fabs(b)<<endl;
//執行double fabs(double arg)
cout<<"|"<<c<<"|"<<"="<<fabs(c)<<endl;
//執行float fabs(float arg)
這樣編譯器在編譯的時候就可以根據傳送的參數類型實例化某個函數執行。
1. 定義函數模板的注意事項:
(1)函數模板是一個獨立於類型的函數,可以看做一種通用的功能;
(2)模板定義必須以template開始,之後接模板的形參列表且此列表不能為空;
(3)可用typename或class定義模板的類型,兩者沒有區別。
2. 模板形參的注意事項:
(1)模板形參遵循名字屏蔽規則,如下例:
typedef int T; //T是全局作用域
template<class T> // 此時的T是局部作用域,它屏蔽了全局的T
void fun(constT& val)
{
T temp=val;
//此時temp的類型是具有局部作用域的T,並非int類型
//Todo something
}
(2) 模板的類型不能重複定義,如下例:
template<classT, class T> / /T重複定義
//編譯器報錯:error C2991: redefinition of template parameter "T"
void fun(constT& val)
{
T temp=val;
//Todo something
}
3.實例化
(1)模板實參的推斷
在使用函數模板時,編譯器通常會自動推斷出模板實參,如以上求絕對值的例子。
(2)實例化時形參、實參類型必須匹配
template<typenameT>
T compare(constT& a,const T& b) //此模板函數返回a, b的最大值
{
return a>b?a:b;
}
void main()
{
int a=1;
double b=3;
cout<<compare(a,b); //類型不匹配
//編譯器報錯:
//error C2782: "T__cdecl compare(const T &,const T &)" : template parameter "T" isambiguous
}
原因分析:此模板只有一個類型T,在編譯時,編譯器最先遇到的實參是a,類型是整型,編譯器就將該模板形參解釋為整型,此後出現的形參b不能被解釋成整型,所以編譯器報錯。
解決方案一:強制類型轉換
cout<<compare(a,int(b));
//將變數b強制轉換為整型,但轉換後精度損失
或:
cout<<compare(double(a),b);
//此時整型變數a會提升為double型,精度不會損失
解決方案二:修改函數模板
但這樣做,可能會違背函數模板設計的初衷,所以我們在使用函數模板時,最好做到形參、實參類型完全匹配。
(3)實例化與函數指針的確定
在C/C++語言中,函數名代表函數的入口地址,那麼我們可以將這個地址放在一個指針變數中,這個指針變數就是指向函數的指針,如下代碼:
int add(int a, int b); //返回a, b之和
int (*ptrfun)(int a, int b); //ptrfun是指向函數的指針
ptrfun=add; //賦值
ptrfun(1,3); //調用
或者以另一種形式賦值:
int (*ptrfun)(int a, int b)=add;
這裡我們寫了一個關於加法的函數模板:
template<typename T>
T add(const T &a, const T &b)
{
return a+b;
}
int (*pfun)(const int &a ,const int &b)=add;
//此時編譯器在會以整型來實例化函數模板
如果編譯器不能從函數指針類型(如上例中的函數指針所指函數有整型參數)推斷出函數模板的類型,那麼編譯器就會報錯。如下例:
template<typename T>
T add(const T &a, const T &b)
{
return a+b;
}
//定義重載函數func,注意此這兩個func函數的參數分別是指向不同函數的指針
void func(int (*)(const int&, const int&));
//指向兩個整型數相加函數
void func(string (*)(const string&, conststring&));
//指向兩個字元串連接函數
func(add); //編譯器報錯
//error C2668: "func" : ambiguous call to overloadedfunction
原因分析:當執行func(add)時,要進行參數傳遞,相當於
func=add,此時的func有兩種類型,編譯器無法推斷出模板函數add的類型,所以編譯器報錯。
4. 函數模板的顯式實參的探討
(1)假如我們想設計一個加法函數模板,函數模板的參數類型可以不同,但返回類型應該是參數類型的最高類型,如
template<typename T1,typename T2>
add(const T1 &a, const T2 &b);
現在的問題是函數模板add的返回類型是什麼?
假如 int a=1; double b=2.11; add(a, b);
我們希望add返回double類型
解決方案:重新定義模板
template<typenameT1,typename T2,typename T3>
T1 add(const T2&a,const T3 &b)
{
return a+b;
}
但此時會出現問題,那就是編譯器無法推斷出T1的類型,解決方法很簡單,我們寫下如下代碼:
cout<<add<double>(1,2.111)<<endl;
按照以上的提示,輸入「<」後第一個類型就是我們希望函數模板返回的類型T1,其他兩個是參數類型,可以省略不寫,編譯器會根據後面實參的類型來推斷形參的類型。
(2)解決以上指向函數的指針二義性的方案
void func(int(*)(const int&, const int&));
void func(string(*)(const string&, const string&));
func(add); //編譯器報錯
解決方案:顯式指定實參,如下代碼所示
template<typenameT>
T add(const T&a, const T &b)
{
return a+b;
}
void func(int(*ptrfun)(const int& a, const int& b),const int& a, const int&b) //後面兩個參數的聲明必須寫
{
ptrfun=add<int>;
cout<<ptrfun(a,b)<<endl;
}
void func(string(*ptrfun)(const string& a,const string& b),const string& a,conststring& b)
{
ptrfun=add<string>;
cout<<ptrfun(a,b)<<endl;
}
void main()
{
func(add,1,2);
func(add,"hello","world");
}
5. 函數模板與重載函數的探討
代碼實例如下:
int add(int a, intb)
{
cout<<"調用 int add(int a, int b)"<<endl;
return a+b;
}
double add(doublea, double b)
{
cout<<"調用 void fun(double a, double b)"<<endl;
return a+b;
}
template<typename T>
T add(T a,T b)
{
cout<<"調用模板函數"<<endl;
return a+b;
}
在上面的代碼中,我們寫了三個重載的函數,其中有一個是函數模板,這三個函數都是完成返回兩個加數的和。但這樣的代碼在調用時很容易出錯,
cout<<add(1,2);
//絕對匹配,調用int add(int a, int b)
cout<<add(1.0,2.0);
//絕對匹配,調用double add(double a, double b)
cout<<add(1.0,2);//編譯器報錯
//error C2782: "T__cdecl add(T,T)" : template parameter "T" is ambiguous
原因分析:編譯器在編譯第一、二行代碼的時候,能夠找到與實參絕對匹配的函數,編譯通過,但是編譯器在編譯第三行代碼的時候,它會做出如下檢查:
第一步,編譯器會找尋有沒有與實參絕對匹配的函數,發現沒有;
第二步,編譯器會找尋能夠實例化的函數模板,因為源代碼的模板只有一個類型T,而實參提供了兩種類型,所以,編譯器找不到能夠實例化的函數模板。
解決方案:強制類型轉換
cout<<add(1.0,static_cast<double>(2));
或cout<<add(static_cast<int>(1.0),2);
在此,當代碼中出現重載、函數模板時,我們總結下編譯器選擇的次序:
(1)選擇與實參完全匹配的函數;
(2)選擇能夠根據實參類型推斷出形參的函數模板並實例化;
(3)選擇能夠根據實參進行隱式轉換的函數。
其實第三步一些編譯器不支持,如VC6.0
三、 有關類模板的探討
有了以上對函數模板的認識,我們對類模板的理解就會比較容易了。就像函數模板一樣,我們可以把類模板理解為具有一定類型的類(不要與抽象類混淆),而這種類要根據類型來實例化,以完成特定的功能。
從一個有關鏈表的例子講解
鏈表的操作也許我們並不陌生,在C語言中,我們就學習過了,但那時的鏈表要想實現通用的效果,我們往往在代碼中寫下如下的語句:typedef int ElemType; 這樣我們要想創建一個double類型的鏈表,只需要修改int為double即可。現在我們用C++的類模板來寫一個雙向循環鏈表程序,代碼如下所示:
#include <iostream>#include <cstdlib>
using namespace std;
template<class T>class DLNode //結點類{public:T data; //數據域DLNode<T>*lLink; //前驅指針域,指向當前結點的直接前驅DLNode<T>*rLink; //後繼指針域,指向當前結點的直接後繼
DLNode(DLNode<T>* left=0,DLNode<T>* right=0):lLink(left),rLink(right){}DLNode(const T& val,DLNode<T>* left=0,DLNode<T>* right=0){data=val;lLink=left;rLink=right;}};
const int Max=100;template<class T>class DList //雙向循環鏈表類{private:DLNode<T>* head; //頭指針public:DList(){head=0;}DList(int N) //N表示要構造的鏈表長度{if(N<=0 || N>Max){cerr<<"參數不合法!"<<endl;exit(1);}head=new DLNode<T>; //生成頭結點DLNode<T>* p;DLNode<T>* current=head; int i;T data;for(i=0;i<N;i++){cout<<"請輸入第"<<i<<"個數據:";cin>>data;if(!cin.good()){cerr<<"輸入不合法!"<<endl;exit(1);}p=new DLNode<T>(data); //生成一個新的結點current->rLink=p;p->lLink=current;current=p;}current->rLink=head;head->lLink=current;}
bool IsEmpty() const
{ return head==0 || head->rLink==head || head->lLink==head;
}void Print() const //列印鏈表{if(IsEmpty()) return;for(DLNode<T>* p=head->rLink;p!=head;p=p->rLink){cout<<p->data<<" ";}cout<<endl;}
int Length() const{DLNode<T>*p;int n=0;if(IsEmpty()) return 0;for(p=head->rLink;p!=head;p=p->rLink) {n++;}return n;}DLNode<T>* Search(const T& val) //查找val,函數返回指向val結點的指針{DLNode<T>* p;if(IsEmpty()) return 0;p=head->rLink;while(p->data!=val && p!=head) p=p->rLink;if(p==head) return 0;else return p;}DLNode<T>* Search(int pos) //按元素的位置進行查找{if(IsEmpty()) return 0; if(pos<0){cerr<<"參數錯誤!"<<endl;return 0;}int i=0;DLNode<T>* p=head->rLink;while(i<pos && p!=head){i++;p=p->rLink;}return p;}void Insert(int pos,const T &val) //在指定的位置pos後插入值為val的結點{if(IsEmpty()) return;DLNode<T>* p=Search(pos); //尋找插入位置的指針if(!p) return; //找不到則返回DLNode<T>* q=new DLNode<T>(val); //q指向要插入的結點if(!q) return; //存儲空間不足則返回q->rLink=p->rLink; //進行後插q->lLink=p;p->rLink->lLink=q;p->rLink=q;}void Removw(int pos,T& val) //移除位置pos的結點,val帶回此結點的元素值{if(IsEmpty()) return;DLNode<T>* p=Search(pos); //尋找刪除位置的指針if(!p) return; //找不到則返回p->lLink->rLink=p->rLink;p->rLink->lLink=p->lLink;val=p->data;delete p;}
};
測試:
DList<int>L(3); //定義鏈表
注意:將類模板實例化時,必須提供類型
cout<<L.Length(); //輸出鏈表的長度
int a;
cin>>a;
DLNode<int>*p=L.Search(a); //查找a
cout<<p->data<<endl;
L.Insert(2,18); //在鏈表的第2個位置後插入18
L.Print();
L.Removw(3,a); //刪除鏈表第3個位置處的結點
L.Print();
在上面的實例代碼中,我們將類模板的聲明和定義放在同一個cpp文件中。但有時代碼量很大時,我們將類模板的聲明放在頭文件,而實現則放在cpp文件中,如果是這樣做,我們的代碼就要寫成下面的形式:
template<class T> //此行必須要寫
bool DList<T>::IsEmpty() const //類型T不能丟失
{
returnhead==0 || head->rLink==head || head->lLink==head;
}
推薦閱讀:
※心理防禦機制如何保護我們? | 安娜·弗洛伊德
※中藥治療糖尿病機制
※生死得失之間:因果的平衡機制
※中國科大揭示乙肝病毒逃逸免疫攻擊的新機制
※你為什麼不會臉盲:華人科學家揭秘人臉識別機制