模板初阶
认识函数模板
我们已经知道C++里面支持函数重载,我们现在利用函数重载来写一个交换函数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
int main()
{
//我们现在想对整型使用交换函数
int a = 1, b = 2;
Swap(a, b);
//这时候我们又想对浮点型使用交换函数
double c = 1.1, d = 2.2;
Swap(c, d);
return 0;
}
函数重载使得函数可以同名,对于不同的数据类型感觉像在使用同一个函数,用起来感觉还不错,但是我们还要实现针对不同类型的交换函数,终归是有不小代价的。就针对这种情况而言有没有一种办法能写一种通用的函数,能够让它“自动识别”类型呢?
接下来引入模板
C++中也存在这样一种模子,只要为其填充材料(模板代码),它就能够生成适应具体类型(char、int、double....)的代码。这是一种泛型编程理念,何为泛型编程?——编写与类型无关的通用代码,是代码复用的一种手段,而模板正是泛型编程的基础。
//接下来介绍函数模板:
//格式:template<typename/class T>
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
double c = 1.1, d = 2.2;
Swap(c, d);
return 0;
}
值得注意的是:这里调用的并不是模板函数,而是模板函数推演生成出来的具体类型函数,从反汇编的角度:调用的两个函数地址不同,所以证明调用的不是同一个函数,而是由编译器根据实际调用,利用模板推演生成的不同函数。
换句话说:函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
隐式实例化
***类型的推演就是函数模板实例化的过程:在实际调用中,编译器将模板中的T替换成对应的类型,从而生成对应类型的函数,这就叫做函数模板的隐式实例化(后面称为推演实例化)。 ***
//还有一个简单的问题:
int main()
{
int a = 1, b = 2;
Swap(a, b);
int x = 1, y = 2;
Swap(x, y);
//这里调用的Swap是同一个函数吗?
/*解答:由于第一次根据具体类型调用的时候推演出是int类型,编译器根据该类型生成
对应函数,函数在编译过程中生成指令,之后若是再使用int类型Swap函数,再去调用该
指令即可,所以在这里是同一个函数*/
}
显示实例化
//实际当中还可能存在如下调用
int main()
{
int a1 = 1, a2 = 2;
double d1 = 1.1, d2 = 2.2;
//我们将int类型和double类型进行交换:
Swap(a1, d1);
return 0;
}
//这是不可行的,报错如下图所示:
报错原因通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错可能会有这样的提问:不能在传参的过程当中发生隐式类型转换吗,将类型转换为一致吗?
解答:这里有先后顺序的概念,类型的转换在传参或者赋值的时候才会发生,而这里的报错其实并没有走到传参赋值这一步,而是在推演实例化的时候(即在语法层面)就发生了错误。一般地,在模板中,编译器不会进行类型转换操作,因为一旦转化出问题,就是编译器的责任
但是实际上,就算是能够走到传参这一步,编译器依然会报错:
//隐式类型转换会发生临时拷贝,即从int->double要发生临时拷贝,生成的临时变量具有常性,应该用const接收
void Swap(const double& left, const double& right);
//但是用const修饰,就不允许被更改,就不能完成交换,所以传参这一步编译器仍然会报错
//再举一个例子:
template<class T>
T Add(const T& left, const T& right) //这里不改变参数,使用const修饰
{
return left + right;
}
int main2()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
//这里正常调用,编译器正常推演实例化出对应函数
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
/*这里用int和double传参,在模板函数上已经使用const修饰过形参(即可以发生隐式类型转化),如果说编译器能够推演实例化成功,那么如下的这种语法也能够编译通过:*/
cout << Add(a1, d2) << endl;
/*那么有没有什么办法能让这种语法在推演实例化的过程中不报错?*/
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
//1.
cout << Add((double)a1, d2) << endl;
cout << Add(a1, (int)d2) << endl;
//2.
//前面的方式目的在于让其能够正常的推演实例化,接下来引入显示实例化
//显示实例化:在函数名和参数列表之间加<类型>
cout << Add<int>(a1, d2) << endl;
cout << Add<double>(a1, d2) << endl;
return 0;
}
显示实例化和隐式实例化的区别:后者在于让编译器根据参数调用的情况来推演出具体的函数,前者直接让编译器不再推演,直接使用调用者给定的<类型>来实例化函数
函数模板的匹配原则
int Add(int left, int right)
{
return left + right;
}
template<class T>
T Add(T& left, T& right)
{
return left + right;
}
//具体的函数和模板函数能够同时存在吗? ——可以
//那么如何调用?
int main()
{
int a = 1, b = 2;
Add(a, b); //调用谁?
//调用已经写好的,也就是第一个
//我们也可以显示调用模板:
Add<int>(a, b);
}
Add(a, b) 和 Add<int>(a, b)同时存在,可以反过来印证模板的函数名修饰规则和普通函数名修饰规则不同
认识类模板
以栈(Stack)为例,存在需要我们存储不同类型数据的场景(int, char, double...),这也是一种泛型的应用场景,所以对于Stack类,我们也需要一种模板
//有这样的可能性:在同一份代码中,我们需要两个不同类型的栈
int main
{
Stack st1; //存double
st1.push(1.1);
Stack st2; //存int
st2.push(1);
}
/*如果没有类模板,那么我们需要实现两种Stack类来分别存放int和double的数据类型,这样的成本是很高的
所以我们需要一种模板来"自动识别"类型*/
//类模板的实现:
template<class T>
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (T*)malloc(sizeof(T) * capacity);
_capacity = capacity;
_top = 0;
}
Stack(Stack& d)
{
_a = (T*)malloc(sizeof(T) * d._capacity);
memcpy(_a, d._a, sizeof(T) * d._capacity);
_top = d._top;
_capacity = d._capacity;
}
void Push(const T& x);
void Pop();
T Top();
bool Empty();
int Size();
void Print();
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
T* _a;
int _top;
int _capacity;
};
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
//Stack类名, Stack<int>才是类型
Stack<int> st1;
Stack<double> st2;
好了,这篇文章就先到这里了,还有关于模板一些高阶的内容之后的文章会来介绍,例如:非类型模板参数、类模板的特化、模板的分离编译问题等等......
版权归原作者 林知有雨 所有, 如有侵权,请联系我们删除。