《模版初阶》
引言:
上次我们学习了内存管理,之后就要开始学STL了,但是在学习STL之前,我们还需要有一个前置知识->模版,只不过这里只是对模版有一个初步认知,后面还会深入学习。
一:泛型编程
如何实现一个让各种类型通用的交换函数呢?
这时很容易想到之前学过的函数重载,下面是函数重载实现:
使用函数重载虽然可以实现,但是有以下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
有的兄弟,有的,下面就来引入泛型编程这一概念:
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。
模板是泛型编程的基础。
二:函数模版
1. 概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2. 形式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
注意:typename
是用来定义模板参数关键字,也可以使用class
(切记:不能使用struct
代替class
)
3. 函数模版原理
函数模版就相当于是一个蓝图,将需要进行的重复性工作交给了编译器,编译器根据这个模版会生成不同类型(功能相同)的模版函数来解决问题。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double
类型使用函数模板时,编译器通过对实参类型的推演,将T
确定为double
类型,然后产生一份专门处理double
类型的代码,对于字符类型也是如此。
4. 函数模版的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
(1) 隐式实例化:让编译器根据实参推演模板参数的实际类型
由于隐式实例化时编译器不会自动进行类型转换,因此当编译器无法统一实参的类型时就会编译出错,如果转换出错的话编译器不是还得背锅吗。
比如这种情况下:
这时候可以自己进行强转,也可以使用显示实例化。
先来看强转:
(2)显示实例化:在函数名后的<>
中指定模板参数的实际类型
(3)小结:
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
5. 模板参数的匹配原则
(1)有成品就用成品
注:这里在Add
的函数模版与整形的Add
函数同时存在时,编译器会优先使用已有的,因为没有double
类型的Add
函数,因此编译器还是会自己生成。
但是如果你想调用编译器自己生成的加法函数的话就可以显示实例化:
(2)优先使用最匹配的函数
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
比如下面这个场景:
(3)模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
三:类模板
1. 格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
2. 举例
这里我们拿之前实现的栈来举例:
这个是之前实现的栈结构,如果我们现在需要创建不同的栈来存储不同类型的数据的话,就需要写额外的栈结构,因为之前实现的栈的数据类型是一键替换的,只支持一种类型。
下面是用类模版来实现的栈:
这个时候想创建不同的栈来存储数据的话就不需要写额外的栈结构了,因为编译器会根据这个栈的类模板去生成相应的栈来让我们使用。
3. 类模版的实例化
注:类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>
,然后将实例化的类型放在<>
中即可。
类模板名字不是真正的类,而实例化的结果才是真正的类。