一、为什么使用类模板
如果一个类,它对自己要使用到的数据类型不确定,也可以理解成为了提高类的通用性,碰到这种情况时,我们就可以考虑使用类模板。例如C++的类List、Vector,它们实际上就是一种类模板。
以队列类为例进行说明:
队列是一种专门用于对象集合的数据结构,它的行为是先进先出。
Class Queue
{
public:
Queue( );
~Queue( );
int& remove( );
void add(const int&);
bool is_empty( );
bool is_full( );
private:
//………..
}
这样我们就定义了一个
整形变量的队列类,该队列的节点是整形变量。当然该队列也能操作double型变量,因为在使用成员方法add时,double变量只需进行一个隐式类型转换double――
>int
即可。但是,该类就不能操作
String
类型变量,因为不存在一个
String
――
>int
的隐式类型转换。即:
Queue qobj;
String str(“abcd”);
qobj.add(1.25);//
正确
qobj.add(str);//
错误
即使该类操作
double
型变量没有错误,但是在使用
add
方法无疑会破坏
doubld
型变量的精度,同时隐式类型转换也会增加系统负担,所以说上面的
Queue
类在操作
double
型变量时,也是不太理想的。
那么怎么解决这些问题呢,一个最容易想到的办法就是针对每一种数据类型,我们都定义一个队列类,而
C++
类名是不能重载的,因此我们必须取不同的名字,比如
IntQueue
,
DoubleQueue
,
StringQueue
等,然后将这些类的定义做一下拷贝,然后稍做修改就可以了。可以感觉到这样做起来重复性工作比较多,也比较繁琐。
而
C++
为我们提供的类模板机制就是为了解决这种类型的问题的,它为定义通用类的类型提供了条件。
二、类模板的定义格式:
上述情况,用类模板进行声明时,格式如下:
template<class Type>
class Queue
{
Queue( );
~Queue( );
Type& remove( );
void add(const Type&);
bool is_empty( );
bool is_full( );
}
这样,就可以定义
int
、
double
、
String
型队列:
Queue<int> qi;
Queue<complex<double>> qd;
Queue<String> qs;
通过使用类模板,在不同类型的队列定义上,就会变得比较统一了,也比原来简洁很多。
三、怎样声明一个类模板:
最简单的格式如下:
template<class T1
,
class T2>
class A;
说明:类模板的声明都是以关键字
template
开头,然后是一个用逗号分隔的模板参数列表,用尖括号(
<>
)括起来,这个列表不能为空。其中,模板参数可以是类型参数,也可以是非类型参数。如果是非类型参数,则代表一个常量表达式。
模板的类型参数
:由关键字
class
或
typename
及其后的标志符构成,这两个关键字表明后面的参数名代表一个内置的或用户定义的类型。任何内置的或用户定义的类型,如
int
,
double
,
char*
,
complex
或
String
都可以是上面
A
模板的参数
T1
和
T2
的实参。
一个类模板可以有多个类型参数:
template<class T1,class T2,class T3>
但是,每个类型参数前,必须有关键字
class
或
typename
。
例如,下面的模板声明就是错误的:
template<class T,U> //
错误:参数
U
之前没有关键字
class
或
typename
class collection;
模板的非类型参数
:由一个普通参数声明构成。一个非类型参数指示该参数代表了一个潜在的值,而这个值又代表类模板定义中的一个常量。
例如缓冲区类
Buffer
:
template<class Type,int size>
class Buffer;
其中非类型参数
size
表示缓冲区大小的常量。
一个类声明或定义紧跟在类模板参数后面,除了模板参数外,类模板的定义看起来和普通类的定义相似。
类模板参数可以有缺省实参。这对类型参数和非类型参数都一样,缺省实参可以是一个类型或值。
例如:
template<class Type=String,int size=1024>
class Buffer;
四、怎样定义一个类模板:
在类模板定义中,类模板的名字(比如前面的
Buffer,Collection,Queue
等,注意不带实参),可以被用作一个类型指示符,凡是可以使用普通类名的地方,都可以使用它。
例如:
template<class Type>
class QueueItem
{
public:
QueueItem(const Type&);
private:
Type item;
QueueItem* next;
}
但是,上面的模板名
QueueItem
只能用在自己的定义和类模板之外出现的成员函数定义中。
当然,为了统一起见,我们将上面的QueueItem用QueueItem<Type>代替,也是正确的,只不过会显得稍微复杂点而已。
而当
QueueItem
被用在其他模板中,用作一个类型指示符时,我们必须指定完整的参数列表。例如:
template<class Type>
void display(QueueItem<Type>& qi)
{
QueueItem<Type>* pqi=&qi; //
带模板参数列表的完整的
//…..
}
代码示例:
QueueItem
和
Queue
类模板的定义如下:
//QueueItem
的声明
template<class T> class QueueItem;
template<class Type>
class Queue
{
public:
Queue( ):front(0),back(0){ };
~Queue( );
Type& remove( );
Void add(const Type&)
Bool is_empty( )
{
return front==0;
}
}
private:
QueueItem<Type>* front; //
若这样声明
QueueItem* front;
便是错误的。
QueueItem<Type>* back;
}
通过上面代码可以看到:在类模板定义中,使用名字
Queue
时,可以省略参数表
<Type>
,但是当
Queue
的定义引用到类模板
QueueItem
时,不能省略参数表。
五、怎样实例化一个类模板:
从通用的类模板定义中生成类的过程,被称为模板实例化,每一个针对
int
型对象的
Queue
类被实例化时,类模板
Queue<class Type>
定义中的
Type
均被
int
取代,也就是用实参
int
取代形参
Type
,这样类模板实例就可以向普通类一样使用了。
关键是模板非类型参数的实例化时,赋给非类型参数的实参必须是一个常量表达式,即它必须在编译时便能计算出结果。例如,下面的类模板实例化就是错误的:
template<int* ptr> class BufPtr{…….};
BufPtr<new int[24]> bp; //
错误,该非类型参数的实参只有在运行时刻才知道结果
Sizeof
表达式的结果是一个常量,所以它可以被用作非类型参数的实参。
六、如何具体定义类模板的成员函数
与普通类一样,类模板的成员函数也可以在类模板定义中定义具体实现,这时,该函数是内联的(
inline
)。
然而,当在类模板定义之外具体定义成员函数时,必须使用特殊语法,来指明它是一个类模板的成员。成员函数定义的前面必须加上关键字
template
及模板参数
。例如:
template<class Type>
class Queue<Type>::Queue( )
{
front=back=0;
}
}
在这个成员函数定义中,凡是可以使用普通类型的地方,我们均可以使用模板参数
Type
。也就是说类模板函数本身也是一个模板。
用来实例化成员函数的类型,就是其成员函数要调用的那个类对象的类型。例:
Queue<String> qs;
这时,用来实例化构造函数的模板实参就是
String
。
七、常犯错误:
1、
进行模板声明时,比如
template<class Type>
这里的“
<>
”,往往一不小心会写成普通函数声明中用的“
( )
”。另外在一个类模板定义中用到另一个类模板时,常常只写那个模板的模板名,却忘记了写上模板参数,就像前面的
Queue
模板中使用到
QueueItem
模板时,用到
QueueItem
的地方,应该使用
QueueItem<Type>
而不是使用
QueueItem
,或者是将这里的“
<>
”写成了“
( )
”,这都是错误的。
2、
看一个关于模板声明的题目:
指明下面拿一个
template class
的声明或定义是错误的:
(
a
)
template<class Type>
class Container1;
template<class Type,int size>
class Container1;
答:错误,因为两个声明的模板参数不相同。
(
b
)
template<class T,U,typename V>
class Container2;
答:错误,因为模板参数
U
的前面没有用
class
或
typename
修饰。
(
c
)
template<class T,typename T>
class Container3{};
答:错误,因为一个模板参数名称在模板参数列表中只能出现一次,即模板参数列表中不能有重复的参数名。
(
d
)
template<class Type,int* ptr>
class Container4;
template<class T,int* pi>
class Container4;
答:正确。模板参数名不同(注意不是模板参数类型不同),这种情况,即使模板名相同也是可以的。
(
e
)
template<class Type,int value=0>
class Container5;
template<class Type=complex<double>,int value>
class Container5;
答:正确。类模板的后继声明可以为模板参数提供额外的默认值,就像函数参数的默认值一样,一旦你指定某个参数的默认值,其右边参数也都必须具有默认值。
3、
请找出下面哪一个模板实例化的使用,会引起模板确实被实例化出来?
Template<class Type>
Class Stack{};
Void f1(Stack<char>);//a
、仅是一个函数声明,不需实例化类模板
Class Exercise
{
stack<double>& rsd;//b
、声明一个类模板引用,不需实例化
stack<int> si;//c
、
si
是一个
stack
类型的对象,需要实例化类模板
}
}
int main()
{
stack<char>* sc;//d
、仅声明一个类模板指针,不需实例化
f1(*sc);//e
、需要实例化,因为传递给函数
f1
的是一个
stack<int>
对象。
int iobj=sizeof(stack<String>);//
需要实例化,因为
sizeof
会计算
stack<String>
对象的
//
大小,为了计算大小,编译器必须根据类模板定义产生该类型。
}
答:首先分析题意,是模板实例化,也就是产生具体的类型,因为上面定义的
stack
只不过是一个通用模板,它不是一个确切的类型
。不要理解成产生某种类型的对象,这是关键。
而只有当程序使用了一个模板实例化名称,而当时的环境需要一个类定义,这时候类模板才会被实例化出来。如果只是声明了一个类的指针或引用,不需要实现知道该类的定义。
4、
说明为什么下面的模板实例化是非法的?
Template<int* ptr> class Ptr{…….};
Template<class Type,int size> class Fixed_Array{……..};
Template<int hi,int wid> class Screen{……};
A、
const int size=1024;
Ptr<&size> bp1;
错误:因为
int*
-
>const int*
是正确的,但反之错误。
B、
Ptr<0> bp2;
错误:
0
属于一个
int
型,它需要透过一个隐式转换编程
null
指针,才能被认可为
int*
。
C、
const double db=3.1415;
Fixed_Array<double,db> fa1;
错误:编译器无法将
const double
转换成
int
。
D、
unsigned int fasize=255;
Fixed_Array<String fasize> fa2;
错误:非类型参数的实参必须是常量表达式,如果将
unsigned
改为
const
就正确。