文章目录
1.什么是模板?
模板是一种C++特性,允许创建可重复利用的代码,无需编写多个版本。通过模板可以用来处理数据类型不同而实现代码和功能基本相同的程序。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。 模板是泛型编程的基础。
之前我们学习过函数重载,函数重载常用来处理功能类似而数据类型不同的问题。针对不同类型的对象,我们可以写多个函数重载来满足需求,但是这样不仅麻烦而且效率低。
例如,我们以交换函数为例,如果要交换两个int型,就写一个Swap函数,交换double型就再写一个函数重载,交换char型…等,用到几种类型,就需要手动增加对应的函数;并且代码的可维护性比较低,一个出错可能所有的重载均出错。
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
void Swap(double& x, double& y)
{
double tmp = x;
x = y;
y = tmp;
}
因此,C++衍生了模板的玩法。告诉编译器一个模板,让编译器根据不同的类型利用该模板来生成代码。 不仅针对函数,也适用于类。模板分为函数模板和类模板。
模板的声明
这两种声明方式都可以
template<typename T> template<class T>
注:
1.这里的T是模板形参,定义的是数据类型,形参可以用任意名字,只是我们一般用T。
2.typename是用来定义模板参数的关键字 ,也可以使用class,但是不能用struct代替!
模板类型参数可以有多个,如果有多个类型需要传参,就可以这样定义:template<typename T1, typename T2, .....>//或者用class
2.函数模板
(1)什么是函数模板?
函数模板实际上是定义一个通用函数,所用到的类型(返回值类型、参数类型、变量类型)在使用时被参数化,根据实参类型来产生函数的特定类型。
(2)函数模板的格式
函数参数定义的是变量或者对象,而模板参数(T)定义的是类型!
template<typename T1, typename T2,......,typename Tn>//用class也可以
返回值类型 函数名(参数列表)
{
函数体
}
函数模板的使用场景
实现一个通用的交换函数
template<typename T> //函数模板
void Swap(T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
double c = 3.14, d = 1.23;
Swap(c, d);
return 0;
}
(3)函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。 在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于其他类型也是如此。
上面int和double类型分别调用的Swap函数是同一个函数吗?
可以看到,两个call指令调用的地址是不一样的,所以调用的不是同一个函数。
函数模板根据调用,自己推导模板参数的类型,实例化出对应的具体函数。
(4)函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。
模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化:让编译器根据实参推演模板参数的实际类型。
定义一个相同类型相加的函数模板
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
函数模板根据调用,自己推演模板参数的类型,实例化出对应的具体函数。
int main()
{
int a1 = 100, a2 = 200;
double d1 = 3.14, d2 = 1.23;
//函数模板实例化
Add(a1, a2);//实参类型int,推演T为int
Add(d1, d2);//实参类型double,推演T为double
return 0;
}
但是,以下情况就会出问题。由于模板参数只有一个T,函数调用传入了两个实参类型,所以编译器无法推演出T的类型,就会报错。
Add(a1, d1);//报错!!!
此时有两种处理方式:1. 用户自己来强制转换 2. 显式实例化
强制转换:将其中一种类型强制转换为另一种类型Add((double)a1, d1);//推演T为double Add(a2, (int)d2);//推演T为int
下面介绍第二种方法显示实例化
显式实例化:在函数名后的< >中指定模板参数的实际类型。
对于上述问题代码,还可以用显示实例化来改进
Add<int>(a1, d1);//推演T为int
Add<double>(a2, d2);//推演T为double
对于某些无法自动推演模板参数的类型时,只能用显示实例化。
(5)普通函数与函数模板的调用规则
1.如果有一个普通函数和一个同名的函数模板同时存在,如果其他条件都相同,那么调用时会优先调用普通函数;
2.可以使用参数列表来强制调用函数模板。
int Add(const int& x, const int& y)
{
return x + y;
}
//函数模板
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
Add(10, 20);//调用普通函数
Add<int>(10, 20);//强制调用模板
return 0;
}
3.如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
int Add(const int& x, const int& y)
{
return x + y;
}
//函数模板
template<class T1, class T2>
T1 Add(const T1& x, const T2& y)
{
return x + y;
}
int main()
{
Add(10, 20);//调用普通函数
Add(1, 2.2);//调用模板
return 0;
}
3.类模板
(1)类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
//...
};
以栈为例,实现一个简单的类模板。
template<class T>
class Stack
{
public:
Stack(T capacity = 4)
: _a(new T[capacity])
, _capacity(capacity)
, _top(0)
{}
~Stack()
{
if (nullptr != _a)
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
}
private:
T* _a;
int _top;
int _capacity;
};
(2)类模板的作用
之前在C语言中,栈存储的数据类型我们是用关键字typedef
来给类型起个新名字,这样方便随时改动栈的存储类型,想要int栈就改成int,想要char栈就改为char。
typedef int DateType;
但是,如果我们想要同时使用多个不同类型的类(栈),怎么解决呢?
1.再定义一个栈,也就是再复制一份代码,然后改里面的名字和类型,使用几种类型就编写几个版本,这种方法非常麻烦,代码复用性也很低,非常不建议。
2.使用类模板,如上述代码。
(3)类模板的实例化
使用类模板创建对象时,必须指明具体的数据类型。
//Stack s;//没有参数列表,会报错
Stack<int> s1(10);//实例化一个int类型的栈,并初始化容量为10
Stack<char> s2;//实例化一个char类型的栈
类模板中,类名(Stack)不是真正的类,而实例化的结果(Stack< T >)才是真正的类。
普通类:类名和类型一样
类模板:类名和类型不一样
类名:Stack
类型:Stack< T > (比如上述的Stack<int>
和Stack<char>
)
(4)声明和定义分离
//类模板
template<class T>
class Stack
{
public:
Stack(T capacity = 4)
: _a(new T[capacity])
, _capacity(capacity)
, _top(0)
{}
void Push(const T& val);//声明
T Top();//声明
~Stack()
{
if (nullptr != _a)
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
}
private:
T* _a;
int _top;
int _capacity;
};
注意:
1.类模板的类型是Stack<T>
而不是Stack
;
2.每定义一个模板template<class T>
只有当前后面紧跟着的函数能用;每定义一个函数或者类,前面都需要定义一个模板。
//定义
template<class T>
void Stack<T>::Push(const T& val)
{
_a[_top++] = val;
}
//定义
template<class T>
T Stack<T>::Top()
{
assert(_top > 0);
return _a[_top - 1];
}