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。