1.泛型编程
我们先来看一个swap函数
void Swap(int& a,int& b)
{
int c = a;
a = b;
b = c;
}
void Swap(double& a, double& b)
{
double c = a;
a = b;
b = c;
}
void Swap(char& a, char& b)
{
char c = a;
a = b;
b = c;
}
像这么一个简单的Swap函数,因为参数的类型不同,我们就要针对不同的类型来创建多个Swap函数,这样的情况虽然是以函数重载可以实现,但有以下两点不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错,可能所有的重载均出错。
那我们能不能告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码 呢?
古人似乎也面对过这样的问题,一个人写出了一篇照耀古今的文章,想要将其传播出去,但又受限于科技的发展,只能照人手抄,这就限制了知识的传播,直到活字印刷的出现,这一现象发生革命性的改变
以框为模板,只要我们替换里面的模块,就能印刷出不同的文章,不得不佩服古人的智慧。
如果C++中,也能存在这样一个模具,那将会节省我们很多时间,推迟掉发的速度。刚好C++中就有这样的方法。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
在C++中,模板分为函数模板 和 类模板。
2.函数模板
函数模板代表一个函数家族,该函数模板与类型无关,在使用时背参数化,根据实参类型产生函数的特定类型版本。
函数模板格式如下:
template <typename T1,typename T2,…typename Tn>
返回值类型 函数模板名(T1 参数名, …, Tn 参数名){}
注意:
- typename是用来定义模板参数的关键字,也可以使用class,但不能用struct。
- template设定模板参数与下面紧挨着的函数统称为函数模板,对应的模板参数不能在其他函数内使用,想要用只能继续创建函数模板。
- 模板参数设置几个就要用几个,不能有剩余的,编译器会报错,如下:
template<class T,class T2> T Add(const T& a, const T& b) { return a + b; } int main() { int a1 = 1,a2 = 2; Add(a1,a2); return 0; }
template<typename T>
void Swap(T& a,T& b)
{
T c = a;
a = b;
b = c;
}
int main()
{
int a = 1;
int b = 2;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
2.1函数模板的原理
函数模板是一个模板,不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
我们编写如下代码查看调用函数模板是否地址相同(在汇编环境下查看,若是直接调试,调用的都是函数模板):
template<typename T>
void Swap(T& a,T& b)
{
T c = a;
a = b;
b = c;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
double c = 1.0, d = 2.0;
Swap(c, d);
return 0;
}
如上图,两次调用的函数的地址不同,
而如果是下面相同类型的两次函数的调用,则地址相同:
template<typename T>
void Swap(T& a,T& b)
{
T c = a;
a = b;
b = c;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
int c = 3, d = 4;
Swap(c, d);
return 0;
}
结论: 在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此,相同的类型调用相同的函数
拓展:
Swap函数是经常使用的函数,在库内也就定义好了,我们想要使用可以直接调用库内的swap函数即可
#include<iostream>
using namespace std;
int main()
{
int a = 1, b = 2;
swap(a, b);
cout << a << " " << b << endl;
return 0;
}
2.2函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化有分为:隐式实例化和显示实例化。
-
隐式实例化:让编译器根据实参推演模板参数的实际类型。
template<class T> T Add(const T& a, const T& b) { return a + b; } int main() { int a1 = 1, a2 = 2; cout << Add(a1, a2) << endl; double b1 = 1.1, b2 = 2.2; cout << Add(b1, b2) << endl; return 0; }
若是执行下面的语句,则会报错
Add(a1, b1);
因为在编译期间,当编译器看到该实例化时,需要推演其实参类型,通过实参a1将T推演为int,通过实参b1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定T到底是什么类型,因此报错。
注意: 在模板中编译器不会进行自动类型转换,因为编译器毕竟是编译器它没有那么智能,不会揣摩程序员的心思,知道是转int还是double所以直接不转。
想要解决这个问题有三种方法:
-
用户自己强制类型转换
Add(a1, (int)b1);//结果为int类型的值 Add((double)a1, b1);//结果为double类型的值
-
在创造一个模板参数,使用两个模板参数接收两个参数
template<class T1,class T2> T1 Add(const T1& a, const T2& b) { return a + b; } int main() { int a1 = 1; double b1 = 1.1; cout << Add(a1, b1) << endl; return 0; }
缺陷:但需要返回的是什么类型仍需要程序员自己挑动。
-
使用显示实例化
Add<int>(a1,b1);//将参数全部置为int类型 Add<double>(a1,b1);//将参数全部置为double类型
-
-
显式实例化:在函数名后的<>中指定模板参数的实际类型。
int main() { int a1 = 1; double b1 = 1.1; cout << Add<int>(a1, b1) << endl; cout << Add<double>(a1, b1) << endl; return 0; }
如果类型不匹配,编译器会尝试按照<>内的类型进行隐式类型转换,如果无法转换成功,编译器会报错。
2.3模板参数的匹配原则
-
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
template<class T> T Add(const T& a) { return a + b; } int Add(const int& a, const int& b) { return a + b; } int main() { Add(1, 2); Add<int>(1, 2); return 0; }
如上面的
Add(1,2)
,调用的是非模板函数,因为传到就是int型的参数,调用非模板函数直接就可以使用,而调用模板函数还需要实例化,编译器会选择调用更省时省力的函数。而
Add<int>(1,2)
,因为显示实例化的存在只会去调用函数模板,将其实例化为与非模板函数相同的函数之后执行函数。 -
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生一个实例,如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
template<class T1,class T2> T1 Add(const T1& a, const T2& b) { return a + b; } int Add(const int& a, const int& b) { return a + b; } int main() { Add(1, 2);//与非模板函数匹配,编译器不需要特化 Add(1, 2.0);//调用函数模板 return 0; }
Add(1,2)
与非模板函数完全吻合,调用非模板函数,省时省力。Add(1,2.0)
类型不同,只能通过模板实例化出相同类型的函数,调用模板。 -
模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
int Add(const int& a, const int& b) { return a + b; } int main() { cout << Add(1, 2.0) << endl; return 0; }
3.类模板
3.1类模板的定义格式
template<class T1,class T2,...,class Tn>
class 类模板名
{
//类内成员定义
};
使用类模板编写如下代码
template<class T>
class Stack
{
public:
Stack(int capacity = 4)
{
_a = new T[capacity];
_top = 0;
_capacity = capacity;
}
//声明和定义分离
~Stack();
private:
T* _a;
int _capacity;
int _top;
};
//在类外定义
template<class T>
Stack<T>::~Stack();
{
delete[] _a;
_top = _capacity = 0;
}
注意:
- 类模板内的函数在类内声明,类外定义的写法要注意,与正常类不同。
- 类模板如果声明和定义分离,需要在一个文件内完成,不能拆分为两个文件,比如:声明放在
.h
文件,定义放在.c
文件这是不允许的。
3.2类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板的名字不是真正的类实例化的结构才是真正的类。
使用上面编写的类模板
template<class T>
class Stack
{
public:
Stack(int capacity = 4)
{
_a = new T[capacity];
_top = 0;
_capacity = capacity;
}
~Stack()
{
delete[] _a;
_top = _capacity = 0;
}
private:
T* _a;
int _capacity;
int _top;
};
int main()
{
Stack<int> st1;
Stack<char> st2;
return 0;
}
- Stack不是类名,不是真正的类,Stack<类型>才是类