模版

模版相关知识对下学期的数据结构学习有重要作用,比如在链表、栈、队列的学习中频繁应用。下面是一段较为完整的链栈结构:

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

模版的正式介绍

模版是一种对类型进行参数化的工具,使用模版可以实现为函数或类声明一种一般模式,使得函数的参数、返回值或类的数据成员、成员函数取得任意类型,即可以编写与类型无关的代码

从上面的介绍中,可以看出,当我们用模版时:


  1. 使用之前,声明“一种模式”
  2. 具体调用时,代入“任意类型”

声明模版

使用关键字​​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​​​、先进先出的队列​​queue​​等,它们的定义格式是这样的:

#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;
}