當(dāng)我們?cè)絹?lái)越多的使用C++的特性, 將越來(lái)越多的問(wèn)題和事物抽象成對(duì)象時(shí), 我們不難發(fā)現(xiàn):很多對(duì)象都具有共性。 比如 數(shù)值可以增加、減少;字符串也可以增加減少。 它們的動(dòng)作是相似的, 只是對(duì)象的類型不同而已。
C++ 提供了“模板”這一特性, 可以將“類型” 參數(shù)化, 使得編寫的代碼更具有通用性。 因此大家都稱模板編程為 “通用編程”或 “泛型編程”。
一般而言, 模板分為 函數(shù)模板 和 類模板,下面就讓我們分別來(lái)了解一下它們。
一、 函數(shù)模板
1、 函數(shù)模板的定義和使用
定義一個(gè)模板函數(shù)的格式并不復(fù)雜, 如下:
template <模板參數(shù)列表>
返回類型 函數(shù)名(函數(shù)參數(shù)列表)
{
// code ...
}
下面, 讓我們來(lái)舉一個(gè)例子來(lái)說(shuō)明 模板函數(shù)的作用和用法(具體代碼見 Exp01)。
// 定義一個(gè)函數(shù)模板, 用來(lái)實(shí)現(xiàn)任意類型數(shù)據(jù)的相互交換。
template <typename T> // 聲明一個(gè) T 數(shù)據(jù)類型, 此類型隨 調(diào)用方 類型的變化而變化
void swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
上面的代碼, 說(shuō)明了模板函數(shù)的用法。下面再給出調(diào)用的代碼, 我們看看如何使用這個(gè)函數(shù)模板:
int main(int argc, char* argv[])
{
int nNum1 = 50;
int nNum2 = 30;
double dfNum1 = 2.1;
double dfNum2 = 3.0;
char *pszFirst = "Hello ";
char *pszSec = "world!";
swap <int> (nNum1, nNum2); // 將swap函數(shù)模板實(shí)例化為 int類型的模板函數(shù)再調(diào)用
printf("nNum1 = %d, nNum2 = %d\r\n", nNum1, nNum2);
swap<double> (dfNum1, dfNum2);
printf("dfNum1 = %f, pszSec = %f\r\n", dfNum1, dfNum2);
swap<char *> (pszFirst, pszSec);
printf("pszFirst = %s, pszSec = %s\r\n", pszFirst, pszSec);
return 0;
}
具體的執(zhí)行結(jié)果如下:

我相信,如果你是第一次見到模板的代碼,那你一定也會(huì)像我一樣好奇,這個(gè)功能是怎么實(shí)現(xiàn)的,它是怎么做到讓一段代碼來(lái)兼容各種類型的呢?
當(dāng)我要反匯編該EXE得時(shí)候,無(wú)意間查看了下編程生成的map文件,讓我看到了如下的內(nèi)容:
Address
|
Publics by Value
|
Rva+Base
|
Lib:Object
|
0001:00000140
|
?swap@@YAXAAH0@Z
|
00401140 f i
|
Exp01.obj
|
0001:00000190
|
?swap@@YAXAAN0@Z
|
00401190 f i
|
Exp01.obj
|
0001:000001f0
|
?swap@@YAXAAPAD0@Z
|
004011f0 f i
|
Exp01.obj
|
由此可見, 我們編寫的void swap(T& a, T& b), 只是一個(gè)“函數(shù)模板”, 要使用它需要先將它實(shí)例化為一個(gè)“模板函數(shù)”(如:swap <int>)。編譯器在編譯此程序的時(shí)候,為每個(gè)調(diào)用此模板的代碼生成了一個(gè)函數(shù)。而且在后期的使用過(guò)程中,更加讓我認(rèn)識(shí)到:
a、 函數(shù)模板 并不是函數(shù),它僅在編譯時(shí)根據(jù)調(diào)用此模板函數(shù)時(shí)傳遞的實(shí)參類型來(lái)生成具體的函數(shù)體!若 函數(shù)模板沒(méi)有被調(diào)用責(zé)不生成任何代碼也不對(duì)模板代碼作任何語(yǔ)法檢查。
b、 在模板類型參數(shù)中, 實(shí)參與形參要求嚴(yán)格匹配,它不會(huì)進(jìn)行任何的類型轉(zhuǎn)換。
c、 對(duì)于函數(shù)模板,我們?cè)谡{(diào)用時(shí)不一定必須先進(jìn)行將它實(shí)例化為一個(gè)函數(shù)也是可以的,編譯器會(huì)自動(dòng)的識(shí)別模板的類型。(換句話說(shuō):Exp01中的代碼可以直接使用swap, 而不需要<>)
2、 函數(shù)模板的重載
當(dāng)編寫的一個(gè)模板無(wú)法滿足所有需要的情況時(shí),就需要對(duì)模板進(jìn)行重載(或叫 特例化),例如:我們編寫了一個(gè)較大值的模板Max:
template <typename T>
T const& Max(T const& a, T const& b)
{
return a < b ? b : a;
}
A、 當(dāng)我們需要傳入兩個(gè)指針類型的實(shí)參時(shí),該模板就失效了,需要重載該模板:
template <typename T>
T const& Max(T* const& a, T* const& b)
{
return *a < *b ? *b : *a;
}
B、 倘若我們?cè)傩枰容^兩個(gè)字符串大小時(shí),上面兩個(gè)模板都失效了。因?yàn)?font face="Times New Roman">char* 并沒(méi)有提供 operator < 運(yùn)行,我們只能通過(guò)調(diào)用strcmp庫(kù)函數(shù)自己實(shí)現(xiàn)一個(gè)Max模板的特例(見Exp02):
const char* Max(const char*& a, const char*& b)
{
return strcmp(a, b) < 0 ? b : a;
}
說(shuō)明:
C++模板機(jī)制規(guī)定,如果一個(gè)調(diào)用,即匹配普通函數(shù),又能匹配模板函數(shù)的話,則優(yōu)先匹配普通函數(shù)。因此,當(dāng)我們模板特例化的時(shí)候,會(huì)先匹配特例化的函數(shù)。
二、 類模板
1、 基本概念
類模板一般應(yīng)用于容器類中,使得容器能夠處理各種類型的對(duì)象,如(詳見Exp03):
struct Node
{
Node( int nData = 0 )
{
m_nData = nData;
m_pPrev = m_pNext = NULL;
}
int m_nData; // 數(shù)據(jù)元素
Node* m_pPrev; // 指向上一個(gè)元素的指針
Node* m_pNext; // 指向下一個(gè)元素的指針
};
class CDList
{
private:
int m_nCount;
Node* m_pHead;
Node* m_pTail;
int m_nMessage;
public:
CDList();
virtual ~CDList();
public:
int GetLen() const
{
m_nCount;
}
Node* GetHead() const
{
return m_pHead;
}
Node* GetTail() const
{
return m_pTail;
}
public:
bool Change(int nIndex1,int nIndex2);
void Release();
//增加
Node* AddTail( int nData );
Node* AddHead( int nData );
Node* operator[](int nIndex);
//刪除
bool DeleteNode( int nIndex );
void PrintAll();
//查找
Node* FindNode( int nIndex );
};
對(duì)于這樣的鏈表,其節(jié)點(diǎn)的元素只能存放整型數(shù)據(jù)。如果要想讓此雙向鏈表能夠存放任何一種類型的元素,那此時(shí)我們需要的問(wèn)題與函數(shù)模板就一樣了,將此類修改成類模板,現(xiàn)在先不管類模板的寫法,讓我們按照函數(shù)模板的方法將類修改一下:
template <typename T>
struct Node
{
Node( T Data )
{
m_Data = Data;
m_pPrev = m_pNext = NULL;
}
T m_Data; // 通用類型的數(shù)據(jù)元素
Node<T>* m_pPrev; // 指向上一個(gè)元素的指針
Node<T>* m_pNext; // 指向下一個(gè)元素的指針
};
這樣,我們每個(gè)節(jié)點(diǎn)都可以使用通用的類型了,當(dāng)然,對(duì)節(jié)點(diǎn)的處理也一樣得處理。按照這個(gè)樣子將類型參數(shù)化(為節(jié)省篇幅,具體的實(shí)現(xiàn)部分請(qǐng)參考Exp04):
template <typename T>
class CDList
{
private:
int m_nMessage; // 消息號(hào)
int m_nCount; // 鏈表中 元素的數(shù)量
Node<T>* m_pHead; // 鏈表頭指針
Node<T>* m_pTail; // 鏈表尾指針
public:
CDList();
virtual ~CDList();
public:
int GetLen() const
{
m_nCount;
}
Node<T>* GetHead() const
{
return m_pHead;
}
Node<T>* GetTail() const
{
return m_pTail;
}
public:
bool Change(int nIndex1,int nIndex2);
void Release();
//增加
Node<T>* AddTail( T Data );
Node<T>* AddHead( T Data );
Node<T>* operator[](int nIndex);
//刪除
bool DeleteNode( int nIndex );
void PrintAll();
//查找
Node<T>* FindNode( int nIndex );
};
這樣就修改好了,很簡(jiǎn)單吧,貌似類模板沒(méi)有什么太多的新語(yǔ)法規(guī)范。完整的模板代碼,大家可以參考Exp04,下面我們總結(jié)一下類模板的一些語(yǔ)法小細(xì)節(jié)。
2、 類模板的定義
通過(guò)上面的一番修改,我相信你一定對(duì)類模板有了一定的了解,下面我們大致的總結(jié)一下類模板的定義格式:
Template <typename T>
Class 類名
{
// code,可以使用模板參數(shù)T來(lái)指定通用的數(shù)據(jù)類型。
}
正如上面的Exp04中一樣,我們的模板寫好了,但是它不能直接使用,就像函數(shù)模板一樣,我們需要先將模板實(shí)例化成一個(gè)模板類才可以使用。在函數(shù)模板中,編譯器會(huì)針對(duì)我們傳遞的實(shí)參類型等信息自動(dòng)的給我們實(shí)例化函數(shù)模板為模板函數(shù),但是類模板就沒(méi)有這么智能了,必須手工實(shí)例化:
int main(int argc, char* argv[])
{
CDList<int> MyList; // 將CDList實(shí)例化為一個(gè)int類型,也就是說(shuō)鏈表中數(shù)據(jù)元素為整型
//(20) (80) 100 200 50 60
MyList.AddTail(20);
MyList.AddTail(80);
MyList.AddTail(100);
MyList.AddTail(200);
MyList.AddTail(50);
MyList.AddTail(60);
MyList.PrintAll();
MyList.Change(0,1);
MyList.PrintAll();
return 0;
}
程序執(zhí)行結(jié)果:

總結(jié):
a、 類模板 同樣也不是類,它僅在編譯時(shí)根據(jù)實(shí)例化本模板時(shí)傳遞的實(shí)參來(lái)生成具體的類代碼!若 類模板沒(méi)有被實(shí)例化也沒(méi)有被調(diào)用,那編譯器不會(huì)為本模板生成任何代碼也不對(duì)模板代碼作任何語(yǔ)法檢查。
3、 類模板的特化
類模板的特化又被叫做類模板的定做,首先讓我們來(lái)了解下什么叫作定做。
通過(guò)上面幾個(gè)小節(jié)的學(xué)習(xí),我相信,大家都知道模板不能直接被使用:必須先給模板傳遞一個(gè)實(shí)參,將它實(shí)例化為一個(gè)模板類,然后才可以用它來(lái)定義具體的對(duì)象。這樣就隱含了一個(gè)問(wèn)題:
我們通過(guò)給模板傳遞一個(gè)實(shí)參來(lái)實(shí)例化的模板類中的代碼都是在模板中定義好的,如果我們不能用與定義好的模板代碼來(lái)生成模板類,這時(shí)就需要使用模板的特化,也就是“定做”。
比如,我們剛才寫好的雙向鏈表模板中,對(duì)于某一個(gè)類(比如CStudent)來(lái)說(shuō),不允許添加重復(fù)的節(jié)點(diǎn),但是對(duì)于像普通的int,double等數(shù)據(jù)類型以及其它一些類時(shí),又不需要有這類的限制。這時(shí)我們就需要將此雙向鏈表模板針對(duì)這個(gè)不允許有重復(fù)節(jié)點(diǎn)的類(如:CStudent)進(jìn)行特化,大致代碼如下:
template <>
class CDList<CStudent>
{
private:
int m_nMessage; // 消息號(hào)
int m_nCount; // 鏈表中 元素的數(shù)量
Node<CStudent>* m_pHead; // 鏈表頭指針
Node<CStudent>* m_pTail; // 鏈表尾指針
public:
bool Change(int nIndex1,int nIndex2);
void Release();
//增加
Node<CStudent>* AddTail( CStudent Data );
Node<CStudent>* AddHead( CStudent Data );
Node<CStudent>* operator[](int nIndex);
//刪除
bool DeleteNode( int nIndex );
void PrintAll();
//查找
Node<CStudent>* FindNode( int nIndex );
};
Node<CStudent>* CDList<CStudent>::AddTail( CStudent Data )
{
Node<CStudent>* pNewNode = new Node<CStudent>(Data);
if ( m_pTail )
m_pTail->m_pNext = pNewNode;
pNewNode->m_pPrev = m_pTail;
if ( m_pTail == NULL )
m_pHead = pNewNode;
m_pTail = pNewNode;
m_nCount++;
return pNewNode;
}
…
由此可知,為CStudent類定做的CDList模板類,就是以CDList<CStudent>為類名重寫一份CDList<CStudent>實(shí)現(xiàn)而拋棄編譯器為我們生成的CDList<CStudent>類。
當(dāng)一個(gè)模板擁有多個(gè)模板參數(shù)時(shí),如果我們只對(duì)其部分參數(shù)定做則稱為“局部定做”,這樣定做出來(lái)的“物件”仍然是一個(gè)模板,因?yàn)槲覀冎惶鼗艘徊糠帜0鍏?shù)….
說(shuō)明:
剛才,我們?yōu)?font face="Times New Roman">CStudent類定做的CDList模板類,其實(shí)我們沒(méi)有必要將整個(gè)CDList<CStudent>類都寫一遍,只需要重寫Add函數(shù)即可,例如:
Template<>
CDList<CStudent>::Add(CStudent& n)
{
……
}
當(dāng)然,這樣的代碼,需要寫在Cpp文件中,并在.h文件中聲明。
三、 模板程序的組織
當(dāng)然,如果你足夠細(xì)心,你一定會(huì)好奇,為什么我給的例子中,模板的代碼都寫在頭文件中(.h文件),這里我們就討論模板代碼的組織方式。C++支持兩種模板代碼的組織方式,分別是包含方式(我們使用的就是包含方式)和分離方式。這兩種組織方式?jīng)]有太根本的區(qū)別,就是一個(gè)將代碼全寫在頭文件中,分離方式是像寫類一樣聲明和定義分別寫在頭文件(.h文件)和實(shí)現(xiàn)文件(cpp文件)中。
下面我們分別討論下這兩種代碼組織方式。
1、 包含方式
本專題中,所有的實(shí)例代碼中的模板代碼都是以包含方式組織的。因?yàn)楹枚嗟木幾g器(如VC6的cl)并不支持分離方式組織代碼,將代碼寫在頭文件也只是方便編譯器的預(yù)處理工作能方便的將.h文件中的代碼根據(jù)模板實(shí)參的類型生成相應(yīng)的模板類。
當(dāng)然,將代碼都寫在頭文件中還有一點(diǎn)點(diǎn)小要求:
A、 如果模板的成員函數(shù)寫在類外,則需要寫成如下樣式(見Exp04):
template <typename T> // 每個(gè)類外成員函數(shù)前都要有這句
Node<T>* CDList<T>::AddTail( T Data )
{
Node<T>* pNewNode = new Node<T>(Data);
if ( m_pTail )
m_pTail->m_pNext = pNewNode;
pNewNode->m_pPrev = m_pTail;
if ( m_pTail == NULL )
m_pHead = pNewNode;
m_pTail = pNewNode;
m_nCount++;
return pNewNode;
}
B、 對(duì)于特化的代碼則需要在.h文件中聲明并在.cpp文件中定義,如果都寫在.h文件中編譯會(huì)報(bào)重定義錯(cuò)誤。
2、 分離方式
上面已經(jīng)提到過(guò),所謂的分離方式組織代碼,就是將模板的聲明和定義分別寫在頭文件(.h文件)和實(shí)現(xiàn)文件(cpp文件)中,需要注意的是,并不是所有的編譯器都支持這種寫法,目前我只知道GCC支持這種寫法。
當(dāng)然,分離方式組織代碼也有個(gè)小要求,就是在模板的聲明和定義的template關(guān)鍵字前都加上export關(guān)鍵字。比如:
// .h 頭文件中
export template <typename T>
class CDList
{
public:
CDList();
virtual ~CDList();
public:
bool Change(int nIndex1,int nIndex2);
void Release();
//增加
Node<T>* AddTail( T Data );
Node<T>* AddHead( T Data );
Node<T>* operator[](int nIndex);
//刪除
bool DeleteNode( int nIndex );
void PrintAll();
//查找
Node<T>* FindNode( int nIndex );
};
在實(shí)現(xiàn)文件(cpp文件)中。
export template <typename T> // 每個(gè)類外成員函數(shù)前都要有這句
Node<T>* CDList<T>::AddTail( T Data )
{
Node<T>* pNewNode = new Node<T>(Data);
if ( m_pTail )
m_pTail->m_pNext = pNewNode;
pNewNode->m_pPrev = m_pTail;
if ( m_pTail == NULL )
m_pHead = pNewNode;
m_pTail = pNewNode;
m_nCount++;
return pNewNode;
}
….
四、 學(xué)習(xí)小結(jié)
經(jīng)過(guò)上面各個(gè)小節(jié)的學(xué)習(xí),我相信大家一定像我一樣,對(duì)模板有了一點(diǎn)認(rèn)識(shí)。大家也一定都知道,模板只是在編譯期間編寫,所有的代碼都只有效與編譯期。
因此,模板的重載、特化等多態(tài)性也都是在編譯期間體現(xiàn)出來(lái)的,如果我們對(duì)編譯生成的可執(zhí)行文件進(jìn)行反匯編時(shí),我們不會(huì)找到任何與模板有關(guān)的代碼,因?yàn)槟0逯皇蔷幾g期間的產(chǎn)物。
關(guān)于模板的作用,我相信大家也一定都體會(huì)到了,它可以大大的減輕我們的編碼負(fù)擔(dān),提高編程效率。關(guān)于模板的用法和技巧還有很多,單單模板特性足可以出一本書的篇幅來(lái)描述其特性及用法。
因此本專題也只是帶領(lǐng)大家了解模板的基礎(chǔ)用法,關(guān)于模板的更多更深入知識(shí),請(qǐng)參考 “模板元編程”相關(guān)內(nèi)容。