1. 泛型编程
如果我们需要实现一个不同类型的交换函数,如果是学的C语言,你要交换哪些类型,不同的类型就需要重新写一个来实现,所以这是很麻烦的,虽然可以cv一下,有了模板就可以减轻负担。
下面写一个适合所有类型的交换就可以这样写。
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 1.0, d2 = 2.2;
swap(a1, a2);
swap(d1, d2);
return 0;
}
让我们先从文字上来了解什么是泛型编程,泛型指的是广泛类型的意思。
泛型编程:编写与类型无关的调用代码,是代码复用的一种手段。 模板是泛型编程的基础。
问题:我们其实如果用函数重载也能解决问题,但是为什么我们还是有模板这个东西呢?
1.重载的只是函数类型不同,代码相同的部分很多,代码复用率很高。
2.如果有一行代码是有问题的话,这些重载的代码都是需要修改的
那我们就可以给编译器一个例子,然后让编译器自己去生成,就像古代的磨具一样,我们再磨具上印出东西,然后就拿的这个磨具去印出相同的东西,这不是很方便的东西。
函数模板
1.函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特 类型版本。
2.函数模板的格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
我们可以拿Swap这个例子来模拟
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
1.template是关键字
2.typename是修饰后面T的关键字,也有class这个关键字,class这个关键字比较短,所以我们用这个比较多。
3.T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。
注意事项:函数模板不是一个函数,而是我们的编译器拿的这个函数模板去实例化出一个一个的函数来的,我们可以理解为函数的模板
函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模 板就是将本来应该我们做的重复的事情交给了编译器
#include<iostream>
using namespace std;
template<class T>
void Swap(T& x, T& y)
{
T tmp(x);
x = y;
y = tmp;
}
int main()
{
int x = 1;
int y = 2;
double d1 = 2.2;
double d2 = 3.3;
cout << "交换前->" << x << " " << y << endl;
cout << "交换前->" << d1 << " " << d2 << endl;
Swap(x, y);
Swap(d1, d2);
cout << "交换后->" << x << " " << y << endl;
cout << "交换后->" << d1 << " " << d2 << endl;
return 0;
}
我们可以看到的是我们的数据也是成功的进行交换了,那我们来想想他的原理是什么呢,首先编译器是会根据函数模板生成不同的函数,他们的类型是不同的。而且他们的函数栈帧不是同一个。
编译器是会根据这个函数模板去实例化不同的函数出来,所以在函数栈帧上调用的不是同一个函数栈帧,我们也可以来看汇编代码,看看call的地址是不是同一个地址。
所以可以看出我们不是调用的用一个函数。
那我们下面就来探讨编译器是怎么进行实例化的。
函数模板的实例化
其实过程是很简单的,我们在编译阶段的时候,告诉我们的函数模板你要去根据类型进行实例化,然后因为T是函数模板的参数,所以如果我们传int过去的时候他就知道T是int,所以的T改成int去实例化出一个函数出来。
但是函数模板在实例化的过程中也是会出现问题的,比如我们可以来下面的这种情况,我们下一个简单的Add函数模板,然后在main函数里面进行相加计算出结果,我们可以来看看如果不是同一个类型的化会出现怎样的问题。
#include<iostream>
using namespace std;
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int x = 1;
int y = 2;
double d1 = 2.2;
double d2 = 3.3;
int ret1 = Add(x, y);
double ret2 = Add(d1, d2);
cout << ret1 << " " << ret2;
return 0;
}
首先这样的代码是没有问题的,但是如果我们是x+d1呢,我们来看看他的报错信息。、
如果是这样写的化报错信息就是这个样子的,所以我们需要怎么进行修改才行呢。
显式实例化:在函数名后的<>中指定模板参数的实际类型
没错,我们是需要进行显示实例化的,但是我们应该如何进行显示实例化呢,规则很简单。
上面的Add就可以写成。
Add<int>(x,d1);
我们的代码是可以运行的,但是会有这样的警告,其实是可以忽略的,因为我们本生就是不同类型的相加,肯定会产生强转的。
对于模板函数的使用,编译器需要根据传入的实参类型来推演,生成对应类型的函数以供调用。但是我们可以显示的去实例化,规则和Add是一样的道理。
像第一个 Add<int>(a1, a2) ,a2 是 double,它就要转换成 int 。
第二个 Add<double>(a1, a2),a1 是 int,它就要转换成 double。
这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。
像 double 和 int 这种相近的类型,是完全可以通过隐式类型转换的。
🔺 总结:
- 函数模板你可以让它自己去推,但是推的时候不能自相矛盾。
- 你也可以选择去显式实例化,去指定具体的类型。
模板参数的匹配原则
场景:我们会写一个关于Add的函数模板和实现一个Add的函数,类型是int那他到底会配对那个呢。
#include<iostream>
using namespace std;
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
int x = 1;
int y = 2;
double d1 = 2.2;
double d2 = 3.3;
int ret1 = Add(x, y);
double ret2 = Add(d1, d2);
Add<int>(x, d1);
cout << ret1 << " " << ret2;
return 0;
}
就是像这样的场景,那我们如果函数是Add(int ,int)的时候是调用哪个呢。
规则:有现成的就用现成的呗,我们函数模板进行实例化是要根据类型去实例化的,但是我们已经有一个关于它的函数了,这个函数是最适合你的,你还要去生成一个,都多余了。
所以我们就不会再去麻烦编译器去再生成一个函数来实现了。
总结:
① 一个非模板函数可以和一个同名的模板函数同时存在,
而且该函数模板还可以被实例化为这个非模板函数:
② 对于非模板函数和同名函数模板,如果其他条件都相同,
在调用时会优先调用非模板函数,而不会从该模板生成一个实例。
如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
第三点解释一下,就是我们再根据模板生成的时候,只会根据你给的类型去生成,而不存在强转这些,除非是隐式类型转换,隐式类型转换是会存在强转的可能性的。
类模板
1 类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
规则其实和函数模板是差不多的。
1.template是关键字
2.typename是修饰后面T的关键字,也有class这个关键字,class这个关键字比较短,所以我们用这个比较多。
3.T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。
这里要强调一下函数模板和类模板都是不支持分离声明和定义的,你可以再同一个文件里,但是不能在不同的文件进行声明和定义(指的是在一个.h进行声明,在一个.cpp进行定义)这个情况是会我们程序进行链接的时候出现找不到这个地址的现象,因为我们的模板函数是不知我们要实例化的类型是什么,所以就会出现最后链接的时候Call(没地址),所以就会链接错误,后面会深入的讲解。
继续回归我们对类模板的认识,首先是引出问题,我们没有类模板的栈是怎么写的。来看看吧。
class Stack {
public:
Stack(int capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new int[capacity];
}
~Stack() {
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
private:
int* _arr;
int _top;
int _capacity;
};
这个栈是只能来存int,有人就会说,如果我们对int进行typedef不就行了,如果我想要其他类型的时候就只需要改类型就行了,但是这样就有了第二个问题,那就是如果我们需要的是一个存放int的栈,一个存放的是node* 节点的栈,或者一个日期的时候,那问题就很大了,每当我们需要这个类型的时候就需要ctrl c + v然后改一下类型这个操作其实很简单,也很快,但是最终结果就是造成代码相同的还是很多,这样和我们之前的函数模板是一样的问题,所以就有了我们的类模板,那我们来改造一下上面的代码吧。
template<class T>
class Stack {
public:
Stack(int capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new T[capacity];
}
~Stack() {
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
private:
T* _arr;
int _top;
int _capacity;
};
int main(void)
{
Stack<int> st1; // 存储int
Stack<double> st2; // 存储double
return 0;
}
这样就可以解决了我们要存放int和double或者其他类型的问题了。
但是我们发现,类模板他好像不支持自动推出类型,
它不像函数模板,不指定它也可以根据传入的实参去推出对应的类型的函数以供调用
这就是为什么我们需要在类模板后面根生类型,这里大家就要记住的是类模板必须要显示实例化的方法写,它不能像函数模板一样去推演类型。
类模板实例化
模板实例化在类模板名字后跟 < >,然后将实例化的类型放在 < > 中即可。
注意事项:
① Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。
② Stack 是类名,Stack<int> 才是类型:
我们上面说过类模板不能在两个文件里声明和定义分离,但是没说不能在同一个文件了,但是在同一个文件里有些讲究,我们得来探究一下。
就继续拿我们栈来说话。
#include<iostream>
using namespace std;
template<class T>
class Stack {
public:
Stack(int capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new T[capacity];
}
// 这里我们让析构函数放在类外定义
void Push(const T& x);
~Stack();
private:
T* _arr;
int _top;
int _capacity;
};
/* 类外 */
void Stack::Push(const T& x) {
//::::
}
如果我们是这样写的化就是会存在一些小的问题,编译器是不认识外面的这个T,那我们要改的话是需要在下面函数上加上模板的参数的,
template<class T>
class Stack {
public:
Stack(T capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new T[capacity];
}
// 这里我们让析构函数放在类外定义
void Push(const T& x);
~Stack();
private:
T* _arr;
int _top;
int _capacity;
};
/* 类外 */
template<class T>
void Stack<T>::Push(const T& x) {
//::::
}
虽然是能编译通过,但是链接的时候又是会存在问题的,所以我的建议就是大家声明和定义都放在类模板里,多一事不如少一事。
对于这个需要记住的是----------> Stack 是类名,不是类型,Stack<T> 才是类型!
初阶模板就分享到这里了,我们后面还有进阶的模板,今天的分享就到这里了,下次再见了~