上篇文章介紹了模版函數,本篇文章介紹下模版類。
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> bool Stack<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> bool Stack<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。