目录
1.泛型编程
在用模板之前考虑如何实现一个交换函数,需要用到函数重载。
void Swap(int& x1, int& x2)
{
int x = x1;
x1 = x2;
x2 = x;
}
void Swap(double& x1, double& x2)
{
double x = x1;
x1 = x2;
x2 = x;
}
void Swap(char& x1, char& x2)
{
char x = x1;
x1 = x2;
x2 = x;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.22;
Swap(a, b);
Swap(c, d);
char e = 'A', f = 'B';
Swap(e, f);
cout << a << b << c <<d<< e << f << endl;
return 0;
}
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载函数仅仅只是类型不同,代码的重复率比较低,只要有新类型出现时,就需要增加对应的函数
- 代码可维护性比较低,一个出错所有重载函数均出错
当用函数重载实现一个交换函数,发现代码过于冗余,函数内除了形参类型改变,其他基本一样,这样对于C++的开发者有了另一种想法,如何将代码简化,不需要每次都写重复代码,还能实现它呢,这就引出了模板。
模板分为两类:函数模板和类模板。
泛型模板:编写与类型无关的通用代码,是代码复用的一种手段。模板是反省编程的基础。
2.函数模板
【概念】:函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
函数模板格式
那么上面的交换函数怎么用模板实现呢?
template<class T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
【函数模板格式】:template<类型名称 函数参数>
可以定义为
- template<class T>
- template<typename T>
- 或者多个参数类型template<class T1,class T2>
template<class T>被称为模板参数列表,class T则是参数类型
而函数实现中void Swap(T& left, T& right)被称为函数参数列表,里面的参数被称作参数对象
函数模板原理
函数模板是一个蓝图,它本身并不是函数,是编译器产生具体类型的模具。其本质就是将本来我们应该做的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
函数模板实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
- 隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
Add(a1, d2);//不能通过编译
return 0;
}
对于最后一行代码,Add(a1,d2);并不能通过编译。因为在编译期间,当编译器看到该实例化,会推断a1和d2的类型,从而得出结果。但是两个不同的类型,编译器最后推出的不知道是a1类型的还是d2类型的,存在矛盾。
所以解决这个问题,有两种方式
- 用户自己来强制转化
- 使用显示实力转化
强制转化方式:
Add(a1, (int)d2);//将double类型强转成int类型
Add((double)a1, d2);//将int类型强转成double类型
- 显式实例化:在函数名的后面加上<类型>,类型为指定模板参数的实际类型
Add<int>(a2, d1);
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器会报错。
模板参数的匹配原则
- 一个非模板函数可以跟模板函数同时存在,函数模板可以实例化为非函数模板。当实例化函数可调用非模板和模板的时候,优先调用非模板函数。这就相当于用现成的,要不然调用函数模板需要系统先转换形参类型再调用,比非模板麻烦。
//专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
template<class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, a2) << endl;//与非模板函数匹配,编译器不需要特化
cout << Add(d1, d2) << endl;
Add<int>(1, 2);//调用编译器模板Add版本
return 0;
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个更好匹配的函数,那么函数会调用这个更好的。
int Add(int left, int right)
{
return left + right;
}
//通用加法函数
template<class T1,class T2>
T2 Add(T1 left, T2 right)
{
return left + right;
}
void Test1()
{
cout << Add(1, 2) << endl;//会调用非模板
cout << Add(1, 2.2) << endl;//调用模板
}
模板可以是多个参数,Add(1,2.2)调用模板,系统将T1和T2转换成各自类型,完成加法函数再转换成T2类型。
- 函数模板不允许自动类型转换,但普通函数可进行自动类型转换。
3.类模板
类模板定义格式
template<class T1,class T2 ,...>
class 类名模板
{
//内类成员定义
}
当实现一个栈类,如果想改变一个栈的类型,只能通过将类型重命名,然后改变重定义即可。或者,想要int的类型的栈,写一个StackInt类,想要StackDouble类型的栈,写一个StackDouble类型的栈,其本质代码都是一样的,只要改变类型,这样就造成了代码的大量冗余。
这样类模板就起到很大作用。
template<class T>
class Stack
{
public:
Stack(int capacity = 4)
:_top(0)
, _capacity(capacity)
{
_a = new T[capacity];
}
~Stack()
{
delete[] _a;
_a = nullptr;
_capacity = _top = 0;
}
void Push(const T& x);
private:
T* _a;
int _top;
int _capacity;
};
template<class T>
void Stack<T>::Push(const T& x)
{
//...
}
如果在类里面声明的函数,想要在类外面实现,不能单纯的将函数拿出来写。需要加上类模板,并指定类域。
类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化类型放在<>即可。类模板名字不是真正的类(Stack不是具体的类,是编译器根据被实例化的类型生成具体类型的模具),而实例化的结果才是真正的类。
void Test2()
{
Stack<int> st1; //储存int
Stack<double> st2;//储存double
st1.Push(1);
st1.Push(2);
}