引言
我们定义了一个Add函数,希望用这个函数来处理两个int类型数据相加,得到它们相加后的值
int Add(int a,int b)
{
return a+b;
}
当我们又向处理两个double类型的数据呢
我们可以用函数重载,再次书写一个类似的函数
double Add(double a, double b)
{
return a+b;
}
我们可以用函数重载来实现同一个函数处理不同类型的数据,但是,我们不可否认,不断的书写类似的代码是枯燥的,这就是重载函数的缺点。
- 重载的函数仅仅是类型不同,代码复用率比较低下,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错开门所有的重载都出错。
为了避免 重复书写类似的代码带来的枯燥和低收益,我们可以用模板来达到事半功倍的效果。
模板
模板是c++中新引入的一种操作,它是专门针对重复性高的代码书写准备的,望文生义,模板就是一个模子,而用这个模子打造出来的物品,都有极高的相似性。
函数模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型参数产生的特定类型版本。
语法格式
template<class T,class T2........>
返回值类型 函数名(参数列表)
{
}
我们来具体举例一下
template<class T>
T Add(T pa,T pb)
{
return pa+pb;
}
这就是一个正式的函数模板,我们的模板参数是T,在书写基础模板函数时,用T作返回值类型,T作函数参数类型,这就是基本的模子,后面就根据这个基本模板函数产生特定的函数。
我们可以这样理解,基本模板函数相当于一把普通的Ak47,但你不想要普通的Ak47,你可以加料进去,这一操作相当于传实参实例化模板,让原本普通的函数变成处理特定数据类型的函数,也就是让普通Ak47变成青花瓷Ak47,纯金 Ak47等等。
Add(1,2);产生特定的处理int类型数据的函数
Add(2.0,3.0);产生特定的处理double类型数据的函数
由此可见,我们只需要调用函数,再传实参就可以了,编译器会在编译阶段根据我们传的实参的数据类型,将基础模板函数实例化,产生特定的符合我们需求的函数,这样就大大减少了函数重载带来的代码复用率低下,很效率低,枯燥的问题。
函数模板的实例化
函数模板的实例化分为两种,由编译器根据我们传的实参的数据类型推演而实例化,我们称之为隐式实例化,由我们自己显式规定的,称为显式实例化
显式实例化格式:函数名<数据类型>(参数列表)
模板参数的匹配规则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个废模板函数(用显式实例化)
- 对于非模板函数和同名函数模板,如果其他条件都相同,再调用时会优先调用非模板函数而不会从该模板产生出一个实例,如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
- 函数模板不允许自动类型转换,但普通函数可以
第二点说明,编译器也是会偷懒的,既然已经有了适配的函数,那么编译器就不会费力再去实例化模板函数。
类模板
template<class T>
class 类模板名
{
//类内成员定义
};
类模板的实例化
类模板实例化只有显式实例化这一条路可以走。类模板实例化需要在类模板名字后面跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
//Vector类名,Vector<int>才是类型
Vector<int> s1;
注意:类模板中函数放在类外进行定义,需要添加模板参数列表
如 Vector<T>::~vector()
{
}
非类型模板参数
模板参数分类类型形参与非类型参数。
类型形参:出现在模板参数列表中,跟在calss或者typename之类的参数类型名称
非类型形参,就是用一个常量作为类(模板)的一个参数,在类(函数)模板中可将该参数当成常量来使用:
template<calss T,size_t N=10>
class array
{
.......
private:
T _array[N];
size_t _size;
};
- 浮点数,类对象以及字符串是不允许作为非类型模板参数的。(long,int,short等整型可以,同时char也是被认定为整形)
- 费类型的模板参数必须在编译期就确认结果。(即必须给缺省值)
模板的特化
模板函数可以根据实参的数据类型实例化出不同的函数来应对不同的数据,这很方便,但也存在特殊情况,导致会发生结果不是我们想要的结果。
如图片所示,我们要比较Date*类型的数据,出现的结果不是我们想要的,这是因为less对比的是指针存储的地址的大小,不是比较他们存储的地址代表的空间的值的相对大小,又因为地址的值是随机的,所以是无法准确判断p1和p2的大小的,这时候就需要用到我们的模板特化。
为了使模板函数面对特殊情况而做出的特殊处理
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同,编译器可能会报一些奇怪的错误。
temeplate<class T>
bool Less(T left, T right )
{
return left<right;
}//基础模板函数
template<>
bool Less<Date*>(Date* left,Date*right>
{
return *left<*right;
}//特化模板函数
特化的模板函数其实类似于重载了一个函数。
函数模板的特化类似于函数重载,一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是直接将该函数重载
template<class T>
bool Less(const T& left,const T&right)
{
return left<rihght;
}//基础函数模板
template<>
bool Less<Date*>(Date* const& left ,Date* const &right)
{
.......
}
这就是典型的代表,特化起来十分麻烦,不如直接重载。
函数模板不建议特化,建议直接重载。
类模板的特化
全特化
全特化即是将模板参数列表中所有的参数都确定化
template<class T1,class T2>
class Data
{
......;
};
//全特化
template<>
class Data<int,char>
{
......;
};
偏特化
偏特化:对模板参数进行进一步的条件限制设计的特化版本
template<class T1,class T2>
class Data
{
....;
};
部分特化:将模板参数列表中的一部分参数特化
//将第二个参数化为int
template<class T1>
class Data<T1,int>
{
.....;
};
参数进一步的限制也算偏特化
偏特化不仅仅是指特化部分对象,而是针对模板参数更进一步的条件限制所设计处理的一个特化版本
//将两个参数偏特化为指针类型
template<typename T1, typename T2>
class Data<T1*,T2*>
{
......;
};
//将两个参数偏特化为引用类型
template<typename T1.typename T2>
class Data<T1&,T2&>
{
........;
}
模板总结
优点
- 模板复用了代码,节省资源,更快的迭代开发
- 增强了代码的灵活性
缺点
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。