泛型
了解C++的人都知道,在C++中有函数重载,可以通过函数重载来实现相同功能,不同类型的函数。
比如说下面的交换函数
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void Swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
void Swap(char& a, char& b)
{
char tmp = a;
a = b;
b = tmp;
}
我们可以通过将相同功能,不同数据类型的函数命名相同函数名,在使用时调用不同的函数来实现。
但是这也有两点不足:
1、重载的函数仅仅是类型不同,但是其他都是一样的,代码的复用率比较低。有新类型出现,就需要增加一个新的重载函数。
2、代码的可维护性比较低,如果这些重载的函数有一个出错,那么可能所有的重载都会有问题。
那么能不能告诉编译器有这样一个模板,让编译器根据不同的数据类型来生成相应的代码呢?
答案是可以的。C++中可以通过定义模板,来实现不同的代码。
模板
模板分为函数模板和类模板。
函数模板
函数模板相当于一个家族,它与类型无关,在使用的时候根据参数类型来生成特定的类型版本。也就是说,模板在定义的时候,其中的变量不写具体的变量名,而是通过一个泛型参数代替。
1、函数模板格式
格式如下:
template <typename T1, typename T2,…,typename Tn>
返回值类型 函数名 参数列表
解释一下:第一行是用来说明下面的函数定义是通过模板定义的,其中T1、T2、…Tn是用来代表形参的数据类型;第二行是函数的声明,其中参数列表这块用上面模板说明中的T1、T2、…、Tn表示。
这里的typename也可以用class代替,实际上,在日常的使用中class是用的要更多一点(切记:不能用struct代替class)。
比如,上面的交换函数可以通过定义模板来完成。
//模板实现
template <class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
从上图可以看出,用模板可以对不同类型的数据类型的变量进行交换。
2、模板原理
那么模板的底层到底是什么呢?它是怎样产生不同的代码?
函数模板其实可以看作是一个图纸,本身并不是真正的函数,是编译器通过使用方式来产生具体类型函数的模具。也就是说,模板就相当于将本来应该我们自己做的重复的事情让编译器来做。
它的实现原理是这样的:
在编译器的编译阶段,通过在某一个地方调用函数模板传入实参,编译器会根据传入的实参来推演生成对应类型的函数。比如:当double类型使用函数模板时,编译器通过对实参的推演,将T确定为double类型,然后生成一份专门处理double类型的代码。
3、函数模板实例化
用不同的参数使用函数模板称为模板的实例化,其分为隐式实例化和显式实例化。
(1) 隐式实例化
隐式实例化就是让编译器根据实参来推演模板参数的实际类型,在调用时不专门给出。
template <class T>
T Add(const T left, const T right)
{
return left + right;
}
int main()
{
int a = 10, b = 20;
double c = 2.3, d = 9.8;
Add(a, b);
Add(c, d);
return 0;
}
在上面的代码中写了一个加法函数的模板。
然后在调用的时候给定实参,但是不具体给定类型,让编译器自己推演。比如上面的两次调用编译器会把Add函数分别生成int类型和double类型。
但是这里要注意,如果在函数模板中给定两种不同类型的实参,编译是无法通过的。
因为在编译的时候,当编译器看到函数模板被实例化之后,会去推演其实参的类型,通过推演实参a将T确定为int类型,通过实参d将T确定为double类型,但是我们给定的参数列表中只有一个T,那么编译器在这里无法确定将T确定为int类型还是double类型。
那么如果我们一定要这样做,可以采用以下两种方式:
1、对其中一个类型进行强制类型转换,将其转换成和另一个实参相同的类型。
2、使用显式实例化。
(2)显式实例化
显式实例化顾名思义就是在调用函数模板时给定参数类型。
指定a和b为int类型,指定的类型用<类型>来表示。
显式实例化在指定类型之后,无论实参是什么类型,都会实例化为指定的类型。
4、模板参数的匹配原则
(1) 一个非模板函数可以和同名模板函数同时存在。如果调用的函数的类型和非模板函数匹配,那么编译器就不需要调用函数模板。如果没有匹配的非模板函数,编译器就会调用函数模板。
//针对int类型的加法函数
int Add(int a, int b)
{
cout << "int" << endl;
return a + b;
}
//模板类加法函数
template <class T>
T Add(T a, T b)
{
cout << "template" << endl;
return a + b;
}
int main()
{
Add(1, 2);
Add (1.2, 2.6);
return 0;
}
可以看到Add(1, 2)有现成的非模板函数,直接调用。而Add(1.2, 2.6)属于double类型,编译器会去调用函数模板生成一份double类型的代码。
(2) 对于非模板函数和同名函数模板,如果其他条件都相同,那么在调用时会优先调用非模板函数;但是如果模板能够匹配出一个更好的函数,那么编译器会调用模板。
//针对int类型的加法函数
int Add(int a, int b)
{
cout << "int" << endl;
return a + b;
}
//模板加法函数
template <class T1, class T2>
T1 Add(T1 a, T2 b)
{
return a + b;
}
对于Add(1, 2),有对应的非模板函数,那么编译器就不会去用模板生成。
而对于Add(1, 2.0),函数模板能够匹配的更好,那么编译器就会调用函数模板。
(3) 模板函数不允许自动转换类型,但是普通函数可以进行自动类型转换。
类模板
类模板的定义格式
类模板的定义格式如下所示:
template<class T1, class T2, …, class Tn>
class 类模板名
{
类中的成员定义
};
简单的写一个顺序表的类模板来解释一下:
//动态顺序表
template <class T>
class Vector
{
public:
Vector(int capacity = 10)
: _a(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
~Vector();
void Pushback(const T& data);
void Popback();
int Size()
{
return _size;
}
private:
T* _a;
int _size;
int _capacity;
};
类模板中,所有需要用到类型T的地方用T表示,也就是生成能够存储不同类型的顺序表。
那么我们如果想在类模板的外面定义,必须加上参数列表,否则在进行定义的时候编译器不知道T是干什么的。
参数模板的实例化
类模板实例化需要在类模板的后面加上<>,然后将实例化的类型放在<>中即可。类模板的名字不是真正的类,实例化的结果才是真正的类。
//Vector是类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;