模版
模版相关知识对下学期的数据结构学习有重要作用,比如在链表、栈、队列的学习中频繁应用。下面是一段较为完整的链栈结构:
ps. 不需要看懂这段代码,它只是为了说明模版在后续学习中的作用
#include <iostream>
using namespace std;
template<class T> //模版
class linkStack{
private:
struct Node{
T data; //模版
Node* next;
Node(){next=NULL;}
Node(const T &value, Node *p=NULL){ //模版
data=value, next=p;
}
};
Node* top;
public:
linkStack(){top=NULL;}
~linkStack(){clear();}
void clear();
bool empty(){return top==NULL;}
int size();
void push(const T &value); //模版
void pop();
T getTop(); //模版
};
template<class T> //模版
void linkStack<T>::push(const T &value) { //模版
Node *p=new Node();
p->data=value;
p->next=top;
top=p;
}
template<class T> //模版
void linkStack<T>::pop() { //模版
if(empty())return;
top=top->next;
}
template<class T> //模版
T linkStack<T>::getTop() { //模版
return top->data;
}
template<class T> //模版
void linkStack<T>::clear() { //模版
Node *p=NULL;
while (!empty()){
top=top->next;
}
}
template<class T> //模版
int linkStack<T>::size() { //模版
int count=0;
Node *p=top;
while(p){
count++;
p=p->next;
}
return count;
}
由于模版并不是课本上的要求课程(貌似),而更像是一个辅助理解的工具,因此在下文中,我将介绍模版的含义,函数模版、类模版的基本使用等知识,不会包含非类型形参等更深的知识,且由于是自己瞎总结的,所以逻辑上可能不是很严谨,知识也不是很成体系。如果有任何问题,或发现任何错误,可以评论或直接找我。
引例
直接讲或许会有点难懂,我们先看一道例题:
编写一个函数findMax,使它可以返回两个同类型数/字符的较大值。
如果只考虑三个类型int
、char
和double
,我们需要用到函数重载,写三个同名函数:
int findMax(int a, int b){
if(a>b)return a;
else return b;
}
char findMax(char a, char b){
if(a>b)return a;
else return b;
}
double findMax(double a, double b){
if(a>b)return a;
else return b;
}
这样才能保证在主函数中,填入任何一个类型的数,函数都能正常运行:
cout<<findMax(1,2)<<endl; //整型
cout<<findMax(1.1,2.2)<<endl; //双浮点数
cout<<findMax('a','c'); //字符
输出:
2
2.2
c
但事实上,需要考虑的类型远不止这三种,除了系统自带的float
long long
unsigned
等,还需要有我们自己定义的类/结构体
难道我们必须把每种类型都列上,才能实现目的吗?
不难发现,上面的三个函数除了函数类型、参数类型不一样之外,其他的操作都是一样的。没错我就是复制粘贴的。
那我们自然可以提出一个问题:可不可以先用一个抽象的类型T写函数,然后调用的时候再把它具像化,就像下面这样:
T findMax(T t1, T t2){
if(t1>t2)return t1;
else return t2;
}
答案当然是可以的,这就要用到这篇文章想讲的核心知识——模版template
模版的正式介绍
模版是一种对类型进行参数化的工具,使用模版可以实现为函数或类声明一种一般模式,使得函数的参数、返回值或类的数据成员、成员函数取得任意类型,即可以编写与类型无关的代码
从上面的介绍中,可以看出,当我们用模版时:
- 使用之前,声明“一种模式”
- 具体调用时,代入“任意类型”
声明模版
使用关键字template <class T1, class T2, ...>
来声明模版,在这个关键字出现之后的一定区域内,可以使用抽象类型T1
、T2
、…
我们来尝试用模版解决引例中findMax函数的问题:
template<class T> //声明抽象类型T
T findMax(T t1, T t2){ //使用抽象类型T
if(t1>t2)return t1;
else return t2;
}
int main()
{
cout<<findMax(1,2)<<endl;
//填入整数,相当于告诉程序,T具像化为整型,下同
cout<<findMax(1.1,2.2)<<endl;
cout<<findMax('a','c');
return 0;
}
输出:
2
2.2
c
ps. 这里的“抽象类型”只是我为了更形象地描述它的作用,因而自己起了个名字,与面向对象中的“抽象类”不是一个概念。
模版的分类
模版一般分为两种,函数模版和类模版
- 函数模版用于仅参数类型/返回值类型不同的函数
- 类模版用于仅数据成员和成员函数不同的类
上面的引例属于函数模版
函数模版
template <class T1, class T2, ...>
函数类型 函数名(参数1, 参数2, ...){
函数体
}
调用时,不需额外操作,只需直接填入相应参数即可。除上述引例外,再给出一个例子:
template<class T> //声明抽象类型T
T Swap(T &t1, T &t2){ //交换两个数
T tmp;
tmp=t1;
t1=t2;
t2=tmp;
}
int main()
{
int a=1,b=2;
Swap(a,b); //直接填入——整型的a和b
cout<<"a="<<a;
return 0;
}
输出
a=2
类模版
类模版与函数模版相似,也是在类的定义中引入抽象类型T,并在后续的使用中具像化不同的类型。
类模版的基本格式
template <class T> //声明抽象类T
class point{
private:
T a,b; //T类型的成员变量
public:
point(T A, T B){ //参数类型为T的有参构造函数
a=A, b=B;
}
point(){} //无参构造函数
T findMax(){ //返回值类型为T的内联成员函数
if(a>b)return a;
else return b;
}
};
需要特别注意外联函数和在类体外创建对象/函数的语法:
在类体外创建对象/函数
基本格式:类名<类型> 对象名;
其中:
- 类名即为你定义的类的名字
- 类型可以为:
- 抽象类型
- 具体类型,此时相当于将具体类型代入模版
由于声明语句template <class T>
只对它后面紧挨着的函数/类有效,也就是说,紧挨着的类体/函数结束后,程序就不知道T是什么了。所以在类体外创建对象/函数的时候,我们有两种选择:
1.再次声明抽象类型
template <class T> //名字不一定非要是T
point<T> t;
2.代入具体类型
point<int> t; //将抽象类型具像化为整型int
事实上,后面会学到STL中的几种“自带”数据结构:先进后出的栈stack
、先进先出的队列quque
等,它们的定义格式是这样的:
#include <stack> //栈的头文件
#include <queue> //队列的头文件
using namespace std;
stack<int> s; //定义一个名为s的栈
queue<char> q; //定义一个名为q的队列
如果你恰好懂模版,你就会真正理解,这两句定义是什么意思。你甚至可以不写头文件,自己编写相关成员函数,实现它的功能。
外联函数
外联函数在类体外有一个实现的过程:
class point{
private:
T a,b;
public:
point(T A, T B){
a=A, b=B;
}
void Swap(); //外联函数,类体内声明
};
//类体外实现:
template<class T>
void point<T>::Swap() {
T tmp;
tmp=a;
a=b;
b=tmp;
}