C++模板編程
原文鏈接:https://www.cppblog.com/besterChen/archive/2010/07/22/121000.html
當我們越來越多的使用C++的特性, 將越來越多的問題和事物抽象成對象時, 我們不難發現:很多對象都具有共性。 比如 數值可以增加、減少;字符串也可以增加減少。 它們的動作是相似的, 隻是對象的類型不同而已。
C++ 提供了“模板”這一特性, 可以將“類型” 參數化, 使得編寫的代碼更具有通用性。 因此大家都稱模板編程為 “通用編程”或 “泛型編程”。
一般而言, 模板分為 函數模板 和 類模板,下麵就讓我們分別來了解一下它們。
一、 函數模板
1、 函數模板的定義和使用
定義一個模板函數的格式並不複雜, 如下:
template <模板參數列表>
返回類型 函數名(函數參數列表)
{
// code ...
}
下麵, 讓我們來舉一個例子來說明 模板函數的作用和用法(具體代碼見 Exp01)。
// 定義一個函數模板, 用來實現任意類型數據的相互交換。
template <typename T> // 聲明一個 T 數據類型, 此類型隨 調用方 類型的變化而變化
void swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
上麵的代碼, 說明了模板函數的用法。下麵再給出調用的代碼, 我們看看如何使用這個函數模板:
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函數模板實例化為 int類型的模板函數再調用
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;
}
具體的執行結果如下:
我相信,如果你是第一次見到模板的代碼,那你一定也會像我一樣好奇,這個功能是怎麼實現的,它是怎麼做到讓一段代碼來兼容各種類型的呢?
當我要反匯編該EXE得時候,無意間查看了下編程生成的map文件,讓我看到了如下的內容:
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), 隻是一個“函數模板”, 要使用它需要先將它實例化為一個“模板函數”(如:swap <int>)。編譯器在編譯此程序的時候,為每個調用此模板的代碼生成了一個函數。而且在後期的使用過程中,更加讓我認識到:
a、 函數模板 並不是函數,它僅在編譯時根據調用此模板函數時傳遞的實參類型來生成具體的函數體!若 函數模板沒有被調用責不生成任何代碼也不對模板代碼作任何語法檢查。
b、 在模板類型參數中, 實參與形參要求嚴格匹配,它不會進行任何的類型轉換。
c、 對於函數模板,我們在調用時不一定必須先進行將它實例化為一個函數也是可以的,編譯器會自動的識別模板的類型。(換句話說:Exp01中的代碼可以直接使用swap, 而不需要<>)
2、 函數模板的重載
當編寫的一個模板無法滿足所有需要的情況時,就需要對模板進行重載(或叫 特例化),例如:我們編寫了一個較大值的模板Max:
template <typename T>
T const& Max(T const& a, T const& b)
{
return a < b ? b : a;
}
A、 當我們需要傳入兩個指針類型的實參時,該模板就失效了,需要重載該模板:
template <typename T>
T const& Max(T* const& a, T* const& b)
{
return *a < *b ? *b : *a;
}
B、 倘若我們再需要比較兩個字符串大小時,上麵兩個模板都失效了。因為char* 並沒有提供 operator < 運行,我們隻能通過調用strcmp庫函數自己實現一個Max模板的特例(見Exp02):
const char* Max(const char*& a, const char*& b)
{
return strcmp(a, b) < 0 ? b : a;
}
說明:
C++模板機製規定,如果一個調用,即匹配普通函數,又能匹配模板函數的話,則優先匹配普通函數。因此,當我們模板特例化的時候,會先匹配特例化的函數。
二、 類模板
1、 基本概念
類模板一般應用於容器類中,使得容器能夠處理各種類型的對象,如(詳見Exp03):
struct Node
{
Node( int nData = 0 )
{
m_nData = nData;
m_pPrev = m_pNext = NULL;
}
int m_nData; // 數據元素
Node* m_pPrev; // 指向上一個元素的指針
Node* m_pNext; // 指向下一個元素的指針
};
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 );
};
對於這樣的鏈表,其節點的元素隻能存放整型數據。如果要想讓此雙向鏈表能夠存放任何一種類型的元素,那此時我們需要的問題與函數模板就一樣了,將此類修改成類模板,現在先不管類模板的寫法,讓我們按照函數模板的方法將類修改一下:
template <typename T>
struct Node
{
Node( T Data )
{
m_Data = Data;
m_pPrev = m_pNext = NULL;
}
T m_Data; // 通用類型的數據元素
Node<T>* m_pPrev; // 指向上一個元素的指針
Node<T>* m_pNext; // 指向下一個元素的指針
};
這樣,我們每個節點都可以使用通用的類型了,當然,對節點的處理也一樣得處理。按照這個樣子將類型參數化(為節省篇幅,具體的實現部分請參考Exp04):
template <typename T>
class CDList
{
private:
int m_nMessage; // 消息號
int m_nCount; // 鏈表中 元素的數量
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 );
};
這樣就修改好了,很簡單吧,貌似類模板沒有什麼太多的新語法規範。完整的模板代碼,大家可以參考Exp04,下麵我們總結一下類模板的一些語法小細節。
2、 類模板的定義
通過上麵的一番修改,我相信你一定對類模板有了一定的了解,下麵我們大致的總結一下類模板的定義格式:
Template <typename T>
Class 類名
{
// code,可以使用模板參數T來指定通用的數據類型。
}
正如上麵的Exp04中一樣,我們的模板寫好了,但是它不能直接使用,就像函數模板一樣,我們需要先將模板實例化成一個模板類才可以使用。在函數模板中,編譯器會針對我們傳遞的實參類型等信息自動的給我們實例化函數模板為模板函數,但是類模板就沒有這麼智能了,必須手工實例化:
int main(int argc, char* argv[])
{
CDList<int> MyList; // 將CDList實例化為一個int類型,也就是說鏈表中數據元素為整型
//(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;
}
程序執行結果:
總結:
a、 類模板 同樣也不是類,它僅在編譯時根據實例化本模板時傳遞的實參來生成具體的類代碼!若 類模板沒有被實例化也沒有被調用,那編譯器不會為本模板生成任何代碼也不對模板代碼作任何語法檢查。
3、 類模板的特化
類模板的特化又被叫做類模板的定做,首先讓我們來了解下什麼叫作定做。
通過上麵幾個小節的學習,我相信,大家都知道模板不能直接被使用:必須先給模板傳遞一個實參,將它實例化為一個模板類,然後才可以用它來定義具體的對象。這樣就隱含了一個問題:
我們通過給模板傳遞一個實參來實例化的模板類中的代碼都是在模板中定義好的,如果我們不能用與定義好的模板代碼來生成模板類,這時就需要使用模板的特化,也就是“定做”。
比如,我們剛才寫好的雙向鏈表模板中,對於某一個類(比如CStudent)來說,不允許添加重複的節點,但是對於像普通的int,double等數據類型以及其它一些類時,又不需要有這類的限製。這時我們就需要將此雙向鏈表模板針對這個不允許有重複節點的類(如:CStudent)進行特化,大致代碼如下:
template <>
class CDList<CStudent>
{
private:
int m_nMessage; // 消息號
int m_nCount; // 鏈表中 元素的數量
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>實現而拋棄編譯器為我們生成的CDList<CStudent>類。
當一個模板擁有多個模板參數時,如果我們隻對其部分參數定做則稱為“局部定做”,這樣定做出來的“物件”仍然是一個模板,因為我們隻特化了一部分模板參數….
說明:
剛才,我們為CStudent類定做的CDList模板類,其實我們沒有必要將整個CDList<CStudent>類都寫一遍,隻需要重寫Add函數即可,例如:
Template<>
CDList<CStudent>::Add(CStudent& n)
{
……
}
當然,這樣的代碼,需要寫在Cpp文件中,並在.h文件中聲明。
三、 模板程序的組織
當然,如果你足夠細心,你一定會好奇,為什麼我給的例子中,模板的代碼都寫在頭文件中(.h文件),這裏我們就討論模板代碼的組織方式。C++支持兩種模板代碼的組織方式,分別是包含方式(我們使用的就是包含方式)和分離方式。這兩種組織方式沒有太根本的區別,就是一個將代碼全寫在頭文件中,分離方式是像寫類一樣聲明和定義分別寫在頭文件(.h文件)和實現文件(cpp文件)中。
下麵我們分別討論下這兩種代碼組織方式。
1、 包含方式
本專題中,所有的實例代碼中的模板代碼都是以包含方式組織的。因為好多的編譯器(如VC6的cl)並不支持分離方式組織代碼,將代碼寫在頭文件也隻是方便編譯器的預處理工作能方便的將.h文件中的代碼根據模板實參的類型生成相應的模板類。
當然,將代碼都寫在頭文件中還有一點點小要求:
A、 如果模板的成員函數寫在類外,則需要寫成如下樣式(見Exp04):
template <typename T> // 每個類外成員函數前都要有這句
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、 對於特化的代碼則需要在.h文件中聲明並在.cpp文件中定義,如果都寫在.h文件中編譯會報重定義錯誤。
2、 分離方式
上麵已經提到過,所謂的分離方式組織代碼,就是將模板的聲明和定義分別寫在頭文件(.h文件)和實現文件(cpp文件)中,需要注意的是,並不是所有的編譯器都支持這種寫法,目前我隻知道GCC支持這種寫法。
當然,分離方式組織代碼也有個小要求,就是在模板的聲明和定義的template關鍵字前都加上export關鍵字。比如:
// .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 );
};
在實現文件(cpp文件)中。
export template <typename T> // 每個類外成員函數前都要有這句
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;
}
….
四、 學習小結
經過上麵各個小節的學習,我相信大家一定像我一樣,對模板有了一點認識。大家也一定都知道,模板隻是在編譯期間編寫,所有的代碼都隻有效與編譯期。
因此,模板的重載、特化等多態性也都是在編譯期間體現出來的,如果我們對編譯生成的可執行文件進行反匯編時,我們不會找到任何與模板有關的代碼,因為模板隻是編譯期間的產物。
關於模板的作用,我相信大家也一定都體會到了,它可以大大的減輕我們的編碼負擔,提高編程效率。關於模板的用法和技巧還有很多,單單模板特性足可以出一本書的篇幅來描述其特性及用法。
因此本專題也隻是帶領大家了解模板的基礎用法,關於模板的更多更深入知識,請參考 “模板元編程”相關內容。
最後更新:2017-04-03 12:56:43