C++模板類
C++類模版為生成通用的類宣告提供了一種更好的方法。模版提供引數化型別,即能通過型別名作為引數傳遞給接收方來簡歷類或函式,例如將型別名int傳遞給Queue模版,可以讓那個模版構造一個對int進行排隊的Queue類。
1.定義類模版
#ifndef STACKTP_H_ #define STACKTP_H_ template <typename T> class Stack { public: Stack(); bool IsEmpty(); bool IsFull(); bool Push(const T& item);bool Pop(T& item); private: enum{MAX = 10}; T Items[MAX]; int m_iTop; }; #endif
#include "stacktp.h" template <typename T> Stack<T>::Stack() { m_iTop = 0; } template <typename T> bool Stack<T>::IsEmpty() { return m_iTop == 0; } template <typename T> boolStack<T>::IsFull() { return m_iTop == MAX; } template <typename T> bool Stack<T>::Push(const T& item) { if (m_iTop < MAX) { Items[m_iTop++] = item; return true; } else { return false; } } template <typename T> boolStack<T>::Pop(T& item) { if (m_iTop > 0) { item = Items[m_iTop--]; return true; } else { return false; } }
如上程式碼,關鍵字template告訴編譯器,將要定義一個模版。尖括號中的內容相當於函式的引數列表。可以把關鍵字typename看作是變數的型別名,該變數接受型別作為其值,把T看作是該變數的名稱。
2.使用模版類
僅在程式包含模版並不能生成模版類時,必須請求例項化。為此,需要宣告一個型別為模版類的物件,方法是使用所需的具體型別替換通用型別名。例如下面的程式碼建立兩個堆疊一個用於儲存int,一個用於儲存string物件。
Stack<int> stackInt;
Stack<string> stackString;
看到上述聲明後,編譯器將按Stack<T>模版來生成兩個獨立的類宣告和兩組獨立的類方法。類宣告Stack<int>將使用int替換模版中所有的T,而類神明Stack<string>將使用string替換模版中所有的T。當然使用的演算法必須與型別一致。
通用型別標示符,例如這裡的T,稱為型別引數,這意味著它們類似於變數,但賦給他們的不能是數字,而只能是型別。
注意,必須顯示地提供所需的型別,這與常規的函式模版是不同的,因為編譯器可以根據函式的引數型別來確定要生成那種函式。
3.探討模版類
可以將內建型別或類物件用作類模版Statck<T>的型別。指標可以嗎?例如可以使用char指標替換string物件嗎?畢竟,這種指標是處理C++字串的內建方式。答案是可以建立指標堆疊,但是如果不對程式做重大修改,將無法很好的工作。編譯器可以建立類,不過使用效果如何就因人而異了。
- 不正確的使用指標堆疊
將要介紹三個範例,這幾個範例揭示了設計模版時應牢記的一些教訓,切忌盲目使用模版。
版本1將string,替換為char *
旨在用char指標而不是string物件來接收鍵盤輸入。這種方法很快就失敗了,因為僅僅建立指標,沒有建立用於儲存字串的空間。
版本2將string,替換為char po[40],這位輸入的字串分配了空間。另外,po的型別為char *,因此可以被放在堆疊中。但陣列完全與pop()方法的假設相沖突。
版本3將string po,替換為char *po = new char[40];
這為輸入的字串分配了空間。另外po是變數,因此與pop的程式碼相容。不過,這裡將會遇到最基本的問題:只有一個po變數,改變數總是指向相同的記憶體單元,確實在每當讀取新字串時,記憶體的內容都將發生改變,但每次執行壓入操作時,加入到堆疊中的地址都相同。因此,對堆疊執行彈出操作時,得到的地址總是相同的。因此,對堆疊執行彈出操作時,得到的地址總是相同的,它總是指向讀入的最後一個字串。具體地說,堆疊並沒有儲存每一個新字串,因此沒有任何用途。
- 正確使用指標堆疊
使用指標堆疊的方法之一是,讓呼叫程式提供一個數組,其中每個指標都指向不同的字串。把這些指標放在堆疊中是有意義的,因為每個指標都將指向不同的字串。注意建立不同指標是呼叫程式的職責,而不是堆疊的職責。堆疊的職責是管理指標,而不是建立指標。·
4.陣列模版範例和非型別引數
模版常被用作容器類,這是因為型別引數的概念非常適合於將相同的儲存方案用於不同的型別。確實,為容器類提供可重用程式碼是引入模版的主要動機。下面介紹一個允許指定陣列大小的簡單陣列模版。程式碼例項:
#ifndef ARRAYTP_H_ #define ARRAYTP_H_ template <typename T,int n> class ArrayTP { public: ArrayTP(){}; explicit ArrayTP(const T& v); virtual T& operator[](int i); virtual T operator[](int i) const; private: T ar[n]; }; #endif
#include "arraytp.h" template <typename T,int n> ArrayTP<T,n>::ArrayTP(const T& v) { for (int i=0 ;i < n;++i) { ar[i] = v; } } template <typename T,int n> T &ArrayTP<T,n>::operator[](int i) { if (i < 0 || i >= n) { std::cerr << "Error Hanppens" << std::endl; std::_Atexit(EXIT_FAILURE); } return ar[i]; } template <typename T,int n> T ArrayTP<T,n>::operator[](int i) const { if (i < 0 || i >= n) { std::cerr << "Error Hanppens" << std::endl; std::_Atexit(EXIT_FAILURE); } return ar[i]; }
如上程式碼,關鍵字 typename指出T為型別引數,int指出n的型別為int,這種引數--指定特殊的型別而不是用作通用型別名,稱為非型別或表示式引數。
下面的宣告:
ArrayTP<double,12> arrayDouble;
將導致編譯器定義名為ArrayTP<double,12>類,並建立一個型別為ArrayTP<double,12>的arrayDouble的物件。定義類時,編譯器將使用double替換T,使用12替換n。
表示式引數有一些限制,表示式引數可以是整型,列舉,引用或指標。因此double m是不合法的,但double * pm,double *pn,是合法的。另外,模版引數不能修改引數的值,也不能使用引數的地址。所以,在ArrayTP模版中不能使用諸如n++或&n,等表示式,另外在例項化模版時,用作表示式引數的值,必須是常量表達式。
表示式引數方法的主要缺點是,每種陣列大小將生成自己的模板。也就是說,下面的宣告:
ArrayTP<double,12> arrayDouble;
ArrayTP<double,13> arrayDouble2;
將生成兩個獨立的宣告。
5.模板的多功能性
可以將用於常規類的技術應用於模板類。模板類可用作基類,也可用作元件類,還可用作其它模板的型別引數。例如,可以使用陣列模板實現堆疊模板,可以使用陣列模板來構造陣列--陣列元素是基於堆疊模板的堆疊。即可以編寫下面的程式碼:
template <typename T>
class Array
{
private:
T entry;
};
template <typename Type>
class GrowArray : public Array<Type>{};用作繼承
template <typename TP>
class Stack
{
Array<TP> ar;用Array<>作為一個元件
};
Array<Stack<int> > asi;一個int型別堆疊的陣列。
在最後一條語句中,必須使用一個空白字元將兩個>分開,以避免與》操作符混淆。
- 遞迴使用模板
可以遞迴使用模板:
ArrayTP<ArrayTP<int,5>,10> twodee;
這使得twodee是一個包含是個元素陣列,而每一個元素又是包含5個元素的陣列,與之等價得宣告如下:
int[10][5];
- 使用多個型別引數
模板可以包含多個型別引數,如下的定義方法:
template <typename T1,typename T2>
class Temp
{
}
- 預設型別模板引數
類模板的另一項特性是可以為型別引數提供預設值:
template <typename T1,typename T2 = int>
class Topo
{
}
這樣,如果省略T2,編譯器將使用int。
雖然可以為類模板引數提供預設值,但不能為函式模板引數提供預設值。不過可以為非型別引數提供預設值,這對於類模板和函式模板都是適用的。
6.模板的具體化
類模板與函式模板很相似,因為可以有隱士例項化、顯示例項化、和顯示具體化,它們統稱為具體化。模板以通用型別的方式描述類,而具體化使用具體的型別生成類宣告。
- 隱式例項化
到目前為止,所有的模板範例使用的都是隱式例項化,即它們宣告一個或多個物件,指出所需的型別,而編譯器使用通用模板提供的處方生成具體的類定義;
ArrayTP<double,10> stuff;隱式例項化
編譯器在需要物件之前,不會生成類的隱式例項化。
- 顯示例項化
當使用關鍵字template並指出所需型別來宣告類時,編譯器將生成類宣告的顯示例項化,例如下面的宣告:
template class ArrayTP<string,100>;//生成Array<string,100>類
將ArrayTP<string,100>宣告為一個類。在這種情況下,雖沒有建立或提及類物件,編譯器也將生成類宣告。和隱式例項化一樣,也將根據通用模板來生成具體化。
- 顯示具體化
顯示具體化是特定型別的定義。有時候,可能在為特殊型別例項化時,對模板進行修改,使其行為不同。在這種情況下可以建立顯示具體化。具體化類模板定義的格式如下:
template <> 類名<特殊的型別>,例如:
template <> Swap<int>
{
};
- 部分具體化
C++還允許部分具體化,即部分限制模板的通用性。例如,部分具體化可以給型別引數之一指定具體的型別:
template <typename T1,typename T2>
class Pair{};//通用型別模板
template <typename T1> class Pari<typename T1,int>{};//部分具體化
關鍵字template後面的<>宣告的是沒有被具體化的型別引數。因此,上述第二個宣告將T2具體化為int,但是T1保持不變。注意,如果指定所有的型別,則<>內將為空,這將導致顯示具體化。
如有多個模板可供選擇,則編譯器將使用具體化程度最高的模板:
Pair<double,double> p1;//使用通用模板
Pair<double,int> p2//使用Pair<double,int>部分具體化模板
Pari<int,int> P3;//使用Pari<int,int>顯示具體化模板
也可以通過為指標提供特殊版本來部分具體化現有的模板:
template <typename T>
class Feeb{};
template <typename T*>
class Feeb{};
如果提供的型別不是指標,則編譯器將使用通用版本;如果提供的是指標,則編譯器將使用指標具體化版本。
部分具體化特性使得能夠設定各種限制。例如,可以這樣做:
template <typename T1,typename T2,typename T3> class Trio{};通用版本
temlate <typename T1,typename T2> class Trio<T1,T2,T2>{};//把T3設為T2
template <typename T1> class Trio<T1,T1*,T1*>{};
根據上述宣告,編譯器將作出如下選擇:
Trio<int,short,char *> t1;//使用通用模板
Trio<int,short> t2//使用Trio<T1,T2,T2>
Trio<char,char*,char*>//使用Trio<T1,T1*,T1*>
7.成員模板
C++模板支援的另一個特性是:模板可用作結構、類或模板類的成員。如下程式碼例項:
template <typename T> class Beta { private: template <typename V> class hold { private: V val; public: hold(V v=0) :val(v){} void show()const{std::cout << val << std::endl;} V Value() const{return val;} }; hold<T> q; hold<int> n; public: Beta(T t,int i) : q(t),n(i){} template <typename U> U blab(U u,T t) { return (n.Value() + t.Value()) * u /t; } void Show() { q.show(); n.show(); } };
Beta<double> guy(3.5,3); guy.Show(); std::cout<<guy.blab(10,2.3);
如上程式碼:
hold<T> q;
hold<int> n;
n是基於int型別的物件,q是基於T型別(Beta模板引數)的hold物件。在使用中
Beta<double> guy(3.5,3);,使得T表示的是double,因此q的型別是hold<double>。
blab()方法的U型別由該方法被呼叫時的引數值顯示確定,T型別由物件的例項化型別確定。
另外,也可以在beta模板中宣告hold類和blab方法,並在Beta的外面定義它們:
class Beta { private: template <typename V> class hold; hold<T> q; hold<int> n; public: Beta(T t,int i) : q(t),n(i){} template <typename U> U blab(U u,T t); void Show() { q.show(); n.show(); } }; template <typename T> template <typename V> class Beta<T>::hold { private: V val; public: hold(V v=0) :val(v){} void show()const{std::cout << val << std::endl;} V Value() const{return val;} }; template <typename T> template <typename U> U Beta<T>::blab(U u,T t) { return (n.Value() + t.Value()) * u /t; }
上述定義將T、V 、U用作模板引數,因為模板是巢狀的,因此必須使用句法:
template <typename T>
template <typename V>
而不是句法:
template <typename T,typename V>
定義還必須指出hold和blab是Beta<T>類的成員,這是通過使用作用域解析操作符來完成的。
8.將模板用作引數
已經知道,模板可以包含型別引數和非型別引數。模板還可以包含本身就是模板的引數。如下例項:
template <template <typename T> class thing> class Crab { private: thing<int> s1; thing<double> s2; public: Crab(){} bool Push(int a,double b) { return s1.Push(a) && s2.Push(b); } bool Pop(int& a,int& b) { return s1.Pop(a) && s2.Pob(b); } };
上如Crab類的宣告對thing代表的模板類做了另外三個假設,即這個類包含一個Push方法,一個Pop方法,且這些方法有特定的介面。Crab類可以使用任何與thing型別宣告匹配,幷包含方法Push和Pop的模板類。
可以混合使用模板引數和常規引數,如:
template <template <typename T> class thing,typename U,typename V> class Crab { private: thing<U> s1; thing<V> s2; ..................................
現在成員s1,與s2可儲存的為通用型別,而不是硬編碼指定的特定型別。
上述內容參考C++ Primer Plus。