这一小节专门介绍类模板,先看一个简单的例子:
template <class Type> class Test
{
public:
Test(Type val):value(val){}
void set(const Type &val){value = val;}
Type get(){return value;}
private:
Type value;
};
int main()
{
Test<int> iTest(5);
cout<<iTest.get()<<endl;
Test<string> sTest("abc");
sTest.set("aaa");
cout<<sTest.get()<<endl;
return 0;
}
看起来也不是很复杂,它的定义也是以template后接模板形参表。在我们使用时,只需要指定模板实参就行了。
下面我们看一个稍微复杂一点的例子,使用stl标准库中的list来实现队列:
#ifndef QUEUE_H
#define QUEUE_H
#include <iostream>
#include <list>
using namespace std;
template <class Type> class Queue;
//重载输出操作符
template <class Type>
std::ostream& operator<<(std::ostream&,const Queue<Type>&);
template <class Type> class Queue
{
friend std::ostream& operator<< <Type>(std::ostream&,const Queue<Type>&);
public:
//构造函数:空函数,编译器会调用list的构造函数来初始化数据成员
Queue() {}
//接受一对迭代器来初始化:表用list的初始化函数
template <class It> Queue(It beg,It end):items(beg,end){}
template <class Iter> void assign(Iter beg,Iter end)
{
items.assign(beg,end);
}
//返回队列的第一个元素
Type& front()
{
return items.front();
}
const Type &front()const
{
return items.front();
}
void push(const Type &t)
{
items.push_back(t);
}
void pop()
{
items.erase(items.begin());
}
bool empty()const
{
return items.empty();
}
private:
std::list<Type> items;
};
template <class Type>
std::ostream& operator<<(std::ostream &os,const Queue<Type> &q)
{
os<<"<";
std::list<Type>::const_iterator beg = q.items.begin();
while(beg != q.items.end())
{
os<<*beg<<" ";
++beg;
}
os<<">";
return os;
}
#endif
这个设计的风格与标准库中的容器适配器类似,只不过标准库中是使用deque实现的。
最后给出一个比较复杂的例子:用类模板实现队列。不同于上面的设计,这个设计则并没有使用stl库,而是实实在在的动了底层的数据结构。
其实它的设计也不是很复杂,只用到了两个类:第一个类储存的是具体的元素和指向下个元素的指针,第二类储存的是队列的头部和尾部,并为这个队列设计了一些接口:
#ifndef QUEUE_H
#define QUEUE_H
#include <iostream>
//注意两个声明的顺序
//Queue模板类的声明:因为Queue的特例(不是全部实例)是QueueItem的友元
template <class Type> class Queue;
//模板函数声明:因为要使用它的特例作为友元类
template <class T> std::ostream& operator<<(std::ostream&, const Queue<T>&);
//模板函数声明
template <class T> std::istream& operator>>(std::istream&, Queue<T>&);
template <class Type> class QueueItem
{
//设为友元类,是得Queue能访问自己的成员
friend Queue<Type>;
//
friend std::ostream& operator<< <Type> (std::ostream&, const Queue<Type>&);
friend std::istream& operator>> <Type> (std::istream&, Queue<Type>&);
//构造函数:用传来的值初始化自己的数据,指针为空
QueueItem(const Type &t):item(t),next(0){}
//储存元素的值
Type item;
//指向下一个元素
QueueItem *next;
};
template<class Type>class Queue
{
friend std::ostream& operator<< <Type> (std::ostream&, const Queue<Type>&);
friend std::istream& operator>> <Type> (std::istream&, Queue<Type>&);
public:
//默认构造函数:空队列
Queue():head(0),tail(0){}
//复制构造函数:调用copy_elems函数复制队列
Queue(const Queue &Q):head(0),tail(0){copy_elems(Q);}
//类成员函数模板定义:
//接受一对迭代器的构造函数
//注意,这里没有使用引用
//当实参是数组时,传进来的是指针
template <class It>Queue(It beg, It end):head(0),tail(0)
{
copy_elems(beg,end);
}
//类成员函数模板声明
//接受一对迭代器,将其间的值传递给队列
template <class Iter> void assign(Iter,Iter);
//重载赋值操作符
Queue& operator=(const Queue&);
//析构函数:效用detory函数完成
~Queue(){destroy();}
//返回队列的头部元素,两个版本
Type& front(){return head->item;}
const Type &front()const {return head->item;}
//给队尾增加一个元素
void push(const Type &);
//删除队列头部的元素
void pop();
//判断队列是否为空
bool empty()const {return head == 0;}
private:
//指向队首
QueueItem<Type> *head;
//指向队尾
QueueItem<Type> *tail;
void destroy();
void copy_elems(const Queue&);
template <class Iter> void copy_elems(Iter,Iter);
};
#include "queue.cpp"
#endif
下面是部分接口的实现:
#include <iostream>
#include "queue.h"
using std::ostream;
using std::istream;
template <class Type> void Queue<Type>::destroy()
{
while(!empty())
pop();
}
template <class Type> void Queue<Type>::pop()
{
QueueItem<Type>* p = head;
head = head->next;
delete p;
}
template <class Type> void Queue<Type>::push(const Type &val)
{
//分配一个新的QueueItem对象,并用传递的值初始化它
QueueItem<Type>* pt = new QueueItem<Type>(val);
//如果是一个空队列:那么队首与队尾均指向这个元素
if(empty())
head = tail = pt;
//如果队列已经有元素了,那么将它放在队尾
else
{
tail->next = pt;
tail = pt;
}
}
template <class Type> void Queue<Type>::copy_elems(const Queue &orig)
{
//最后一个指针本身就为0
for(QueueItem<Type>* pt = orig.head;pt;pt = pt->next)
push(pt->item);
}
template <class Type> Queue<Type>& Queue<Type>::operator=(const Queue& orig)
{
//删除原来的队列
detroy();
//复制新的队列
copy_elems(orig);
return *this;
}
template <class Type>
ostream& operator<<(ostream &os, const Queue<Type> &q)
{
os << "< ";
QueueItem<Type> *p;
for (p = q.head; p; p = p->next)
os << p->item << " ";
os <<">";
return os;
}
template<class Type>
istream& operator>> (istream &is,Queue<Type> &q)
{
Type val;
while(is>>val)
q.push(val);
return is;
}
//类外定义成员函数模板:
//两个形参列表:类模板形参,自己模板的形参
template <class T> template <class Iter>
void Queue<T>::assign(Iter beg,Iter end)
{
destroy();
copy_elems(beg,end);
}
//类外定义成员函数模板:
//两个形参列表:类模板形参,自己模板的形参
template <class Type> template <class It>
void Queue<Type>::copy_elems(It beg,It end)
{
while(beg != end)
{
push(*beg);
++beg;
}
}
程序里值得说道的地方很多,我们慢慢缕一缕:
首先,在定义友元之前,我们对友元进行了声明。这一点跟我们之前学的不一样。以前为一个类定义友元的时候,是不需要声明,friend关键字会自动扩展作用域。但是这里我们却必须对其声明,原因在于,我们定义的友元,也是模板,如果不声明,会是得友元函数和友元类的所有实例都能访问类的私有成员,这并不是我们所希望的。当我们的QueueItem的内容是int型时,让Queue<string>类型也能访问它是没有必要,也是不安全的,所以这里的定义是得只有与QueueItem同类型的Queue类以及输入输出操作才能访问它。
其次类中声明的友元函数:输入输出操作符friend std::ostream& operator<< <Type> (std::ostream&, const Queue<Type>&);这里为什么要加<Type>?因为如果不加,编译器会将这个函数是为非模板函数,而对后面的定义视而不见。去掉<Type>以后,编译就会显示不能访问QueueItem的私有成员,因为主函数中调用的cout<< qi;用的是模板函数,而我们并没有把它声明为友元,所以就提示不能访问私有成员了。
一般情况下,使用类模板名字的时候,都得指定类模板形参,但是有一种情况是例外:当你在类的作用域内部的时候就不需要了。比如构造函数并没有写成:QueueItem<Type>(const Type &t):item(t),next(0){},Queue& operator=(const Queue&);这里的返回值也没有加上<Type>。但是,如果你使用的并不是自己类的类型,而是其他类,就必须加上了:比如Queue中私有成员的那两个指向QueueItem的指针。
类模板中的成员函数也是模板,只有在使用时才会被实例化。定义模板类型的对象时,模板被实例化,并且会实例化用于初始该对象的构造函数以及构造函数调用的其它函数。当你使用Queue<int> qi(a,a+4);初始化一个队列时,构造函数:template <class It>Queue(It beg, It end)以及它所调用的template <classIter> void copy_elems(Iter,Iter);就会被实例化;当你调用qi.push(m);时,void push(const Type &);才会被实例化。
与普通函数模板不同的是,由于类实例化时已经确定了成员函数的类型,所以这里不需要再进行模板实参推断了。这意味着,当调用函数的实参与模板实例化后的形参不完全匹配时,可以会发生常规类型转换,而不像函数模板,如果不匹配,就一棒子打死的情况。举一个例子,对于Queue<int> qi,你仍然可以short m = 4;qi.push(m);
还有一个之前没有见过的地方就是我们的类模板中,有一些成员或者函数本身就是模板。比如Queue中的接受一对迭代器的构造函数和assign函数。(标准的queue中并没有assign函数),如果它们在类外定义,就需要注意了:它们包含两个模板形参表,其中第一个是类模板形参,第二个才是自己的模板形参。
最后不得不提一下整个类的设计,虽然这个类提供了很多接口,但它们之间并不是相互独立的。单独定义的,只有pop,push和front三个操作。而其中pop和push操作就是最底层也是最基本的操作。其他触及元素的操作,都依赖它们两个实现。这样的设计非常简洁。但是一开始却不一定能想得到。
最后提一句题外话,由于编译模型的缘故,我们并没有把对应的.cpp加入工程。因为.h中包含了.cpp而.cpp中又包含了.h。如果把.cpp加入工程那么它的内容会被编译两次,自然就会出现重定义了。