本章主要讲解三个部分:泛型编程、函数模板、类模板
目录
泛型编程
函数模板
函数模板概念
函数模板的格式
函数模板的原理
函数模板的实例化
隐式实例化
显式实例化
模板参数的匹配原则
类模板
泛型编程
先来大致说一下什么是泛型编程.
在计算机程序设计领域,为了避免因数据类型的不同,而被迫重复编写大量相同业务逻辑的代码,人们发展的泛型及泛型编程技术.
接下来说一下泛型编程(模板)到底有什么用途或意义.
先来看C语言中这样的问题:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int x = 4;
int y = 6;
Swap(x, y);
}
这里没有问题,将x和y进行了交换,类型也是整型的.
但是如果有两个double类型数据进行交换呢?
此时的Swap显然不可以,因为它的参数数据类型是int,与实参不匹配.
这里可能会有人提出这样的一种解决办法:
既然C++支持了函数重载,那么我们直接再写一份形参为double类型的不就可以了吗?没有问题,我们写一份:
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
这样其实并没有解决本质,当后面数据类型是char,float,short等等甚至是自定义类型该怎么交换呢?不可能一个个写出来吧,那也会太麻烦了.
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
于是C++便提出了模板概念.
编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
这里再次说一下利用函数重载的一些问题:
1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
2. 代码的可维护性比较低,一个出错可能所有的重载均出错.
模板分为函数模板和类模板.
先说函数模板
函数模板
函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
函数模板的格式
这里用到一个新的关键字:template
用法:template<typename T1,typename T2 .....,typename Tn>
返回值类型 函数名(参数列表){}
注意:typename是用来定义模板参数关键字,也可以使用class(切记不可以使用struct).
那既然有了模板,我们再来尝试解决一下刚才的问题.
可以先定义一个模板参数,然后再写利用这个参数类型写函数.
//泛型编程 --- 模板
//模板参数(模板类型) --- 类似于函数参数(参数对象)
//typename后面的名字T随便取,Ty,K,V等等都可以,一般是大写字母或单词首字母大写
//T代表一个模板类型(虚拟类型)
template<typename T>
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
这个时候再次进行交换:
int main()
{
int x = 4;
int y = 6;
double a = 1.2;
double b = 1.5;
Swap(x, y);
Swap(a, b);
cout << x << endl << y << endl;
cout << a << endl << b << endl;
}
可以看到无论是int类型还是double类型,都已经完成了交换.
当然把typename改成class也可以.它俩几乎没有任何区别.
那么问题来了,int类型的两个数据交换和double类型的交换是不是调用了同一个函数呢?
其实调用的不是同一个函数,是先进行模板参数的推演,然后再进行推演参数的实例化.
相当于生成了两份数据类型不同代码.
下面将分别讲解这些.
函数模板的原理
经过上面的说明,可以大概的知道:
函数模板的本质是重复的工作交给了机器去完成.
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器.
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型(模板参数推演),然后产生一份专门处理double类型的代码(推演参数实例化),对于字符类型也是如此。
在编译器看来,double,int和char类型是三个不一样的函数.
可以看到函数的地址确实不同
当然大家可以直接用库里的swap函数,即可以完成所有类型的交换.
还有自己实现Swap注意的一点是,使用模板时不能再使用^实现交换操作,因为^只针对整型有用.
函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化
和显式实例化.
隐式实例化
让编译器根据实参推演模板参数的实际类型
上面也说了,编译器根据实参类型然后实例化出对应类型的函数.
如:
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
Add(1, 2);
Add(1.5, 2.5);
return 0;
}
这样都可以编译通过.
第一份被实例化成了int类型
第二份被实例化成了double类型
但是我如果这样写呢?
Add(1.5, 2);
还能不能编译通过?
答案是编译不通过.
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要
背黑锅
对于这种问题,当然可以再设置一个模板参数.
但只针对于一个模板参数,这里有两种方法:
1.用户自己来强制转化
2.使用显式实例化
先说第一种;
既然是两种类型,那么我们把它强转一下不就可以了吗?
如下:
Add((int)1.5, 2);
或者
Add(1.5, (double)2);
既让两种类型相同即可.
这样编译就没有问题了
显式实例化
在函数名后的<>中指定模板参数的实际类型
原来的写法就可以写成:
Add<int>(1.1, 2);
或者
Add<double>(1.1, 2);
这样就相当于提前告诉编译器模板参数的类型了,编译器就不用自己再推演了.
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
注意两个的过程不一样:
用户强制转化是先强制把类型转化成一样的,然后编译器再推导出参数类型,再实例化
显示实例化是实现告诉了编译器参数类型,已经确定好了,不需要再推导,传参时就直接隐式类型转化了.
有两个模板参数时,还会有一些值得注意的问题,我们来看一下.
template<class T1, typename T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.5, 2) << endl;
cout << Add(1, 2.5) << endl;
return 0;
}
先看Add(1,2)
首先T1 和 T2都会被推演为int类型,然后相加,类型还是int,此时函数返回值类型是T1,也是int类型,没有什么问题.
再来看Add(1.5,2)
T1会被推导为double类型,T2会被推导为int类型,相加之后,默认是从小到大提升,既int类型整型提升成double类型,所以结果此时为double类型,正好F返回类型T1也double类型,也没有问题.
Add(1,2.5)
T1会被推导为int类型,T2会被推导为double类型,相加之后,和第二个一样,结果是double类型,但此时返回类型是T1int类型,所以此时需要发生隐式类型转化为int(截断).
模板参数的匹配原则
那么这里会有一个问题:就是一个非模板函数可以和一个同名的函数模板同时存在吗?
答案是可以同时存在的,而且这个函数模板还可以实例化成为这个非模板函数.
这是怎么回事呢?
看下面这段代码:
//专门处理int类型的函数
int Add(int left, int right)
{
cout << "非模板函数" << endl;
return left + right;
}
//通用函数
template<class T>
T Add(T left, T right)
{
cout << "模板函数" << endl;
return left + right;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(2.5, 2.5) << endl;
return 0;
}
我们先运行一下看看结果.
可以看到Add(1,2)没有使用模板函数实例化,而是直接使用了那个非模板函数.
而Add(2.5,2.5)使用了模板函数实例化
这是为什么呢?
因为对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而
不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模
板
但是我们一定想用模板函数进行处理呢?
可以进行特化一下,这个后面也会将.
Add<int>(1, 2);
可以看到已经成功调用了模板函数.
类模板
类模板用的非常多.
先来说之前存在的问题.
比如一个栈,它的每个类里面的类型都是确定的,想同时存在多个不同的数据类型的栈类,必须用不同的类名,拷贝多份相同的代码,正如下所示:
class Stacki
{
private:
int* _a;
int size;
int capacity;
};
class Stackc
{
private:
char* _a;
int size;
int capacity;
};
int main()
{
Stacki sti;
Stackc stc;
return 0;
}
造成代码的冗余
这也是为什么c语言实现不了数据结构的库,因为每个数据类型都不一样.都需要重新写一个.
这个时候模板就发挥了大作用.
先写一下类模板的格式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
template<class T>
class Stack
{
public:
Stack(size_t capacity = 4)
:_a(nullptr)
,_capacity(0)
,_top(0)
{
if (capacity > 0)
{
_a = new T[capacity];
_capacity = capacity;
//_top = 0;
}
}
private:
T* _a;
size_t _top;
size_t _capacity;
};
int main()
{
Stack<int> sti;
Stack<char> stc;
Stack<double> stb;
return 0;
}
这样发现无论什么类型的类,都可以被实例化了.
这里需要注意的问题是:类模板实例化需要在类模板名字后跟<>(显式实例化),然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。因为如果不传参数的话,编译器就没法推导这个类是什么类型.
2.虽然他们用了同一个类模板,但是Stack<int>,Stack<char>是两个类型.
基本的类模板就到这了.还有一些进阶的模板内容将在后面讲解.
模板这一章就到这了,如果有错误或疑问,欢迎指正或提问哦~