目录
一.泛型编程
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。
泛型编程是一种编程范式,它允许程序员编写不依赖于特定数据类型的代码。
在泛型编程中,程序员可以定义一些通用的算法和数据结构,这些可以在不同的数据类型中使用。
比如交换函数,如果我们没有学习泛型编程,则我们就需要根据类型的交换,造出多个轮子:
typedef int Type;
void Swap(Type& left, Type& right)
{
Type temp = left;
left = right;
right = temp;
}
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
使用函数重载固然可以实现这一问题,但是有几个不好的地方:
- 代码空间会变大
- 重载的函数只是类型不同,代码的复用率较低
- 只要有新的类型需要使用这个函数,就需要重载新的函数
- 代码的可维护性较低,一个出错可能全部的重载都出错
因为函数重载存在上述缺点,因此我们提出了”函数模板“
在现实生活中,我们可以通过往模具中填充不同的材料生成不同的铸件。
C++的开发者受到了启发,发明了模板。
模板:告诉编译器一个模子,让编译器根据不同的类型利用该模子生成代码。
模板可以分为函数模板和类模板:
模板是泛型编程的基础。
二.函数模板
1.函数模板概念
函数模板代表了一个函数家族,该家族模板与类型无关。
函数模板在使用时被参数化,根据实参类型产生的特定类型版本。
2.函数模板格式
我们用template关键字来声明模板:
template <typename T1, typename T2,......typename Tn>
返回值类型 函数名(参数列表)
{
//函数体
}
这里需要大家注意的是,我们的第一行后面并没有分号,也就代表着它并不是一条语句。
现在我们举出一个实例:
template <typename T>//函数模板的声明
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
此外,我们也可以使用class取代typename
template <class T>//函数模板的声明
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
现在我们使用一下我们定义的函数模板
#include <iostream>
using namespace std;
template <class T>//函数模板的声明
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
cout << a << ' ' << b << endl;
double c = 1.3, d = 2.5;
Swap(c, d);
cout << c << ' ' <<d << endl;
return 0;
}
可以看到,这里圆满的完成了交换逻辑。
3.函数模板的原理
那么,上述问题是如何解决的呢?
大家都知道,瓦特改良蒸汽机,人类开始了工业革命,解放了生产力。
机器生产淘汰掉了很多手工产品。
本质是什么,重复的工作交给了机器去完成。
因此有人给出了论调:懒人创造世界。
函数模板的本质也是如此
现在我们进入汇编来看一下上述代码运行的过程中编译器都干了什么事
可以看到,这里调用函数时,显式的规定了参数的类型。
因此我们可以得到结论:函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
我们的编译器根据这个模具帮我们做了这个事情。
注意:Swap在调用时,调用的不是void Swap(T& left, T& right),而是编译器预先根据要调用的类型进行推演。
编译器负责在编译时分析模板定义,并在需要时生成特定类型的代码,之后编译器会检查模板的语法,并确保模板的使用是合法的,之后编译器会根据实际使用的类型参数生成相应的函数或类的实现。
例如上图中的这两行代码:
00007FF6E7122423 call Swap<int> (07FF6E7121352h)
00007FF6E7122480 call Swap<double> (07FF6E7121398h)
这两个函数模板就是编译器生成的。
在编译器的编译阶段,编译器就会根据传入的实参类型来推演生成对应类型的函数以供调用。
就比如上图: 当用double
类型使用函数模板时,编译器通过对实参类型的推演,将T
确定为double
类型,然后产生一份专门处理double
类型的代码。
三.函数模板的实例化
用不同类型的参数使用函数模板称为函数模板的实例化。
模板参数的实例化可以分为:隐式实例化和显式实例化。
1.隐式实例化
隐式实例化即我们刚刚实例化的方法,这里不再过多赘述。
#include <iostream>
using namespace std;
template <class T>//函数模板的声明
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
Add(a1, a2);
cout << Add(a1,a2) << endl;
return 0;
}
现在我们来看一下这段代码:
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, d2);
cout << Add(a1, d2) << endl;
return 0;
}
这段代码在大部分编译器下是无法运行的,在VS2022中爆出了如下警告:
为什么在大部分编译器下无法通过编译呢?
这是因为在编译期间,当编译器看到该实例化后,会去推演其实参的类型。
但通过实参a1将T推演为了int,通过实参d1将T推演为double类型。
但是模板参数列表中只有一个T,编译器就无法判断T在这里是int还是double。
为什么在vs2022中可以编译成功呢?
这是因为编译器进行了类型转换操作,但是类型转换操作的风险是极大的,因为不知道此处你想要的是double还是int。
那么我们应该处理这个问题呢?
处理方式1:用户自己强制转换
Add(a1, (int)d2);//想要int类型的,我们直接将d2强转为int
处理方式2:采用显式实例化
那么,如何显式实例化呢?
2.显式实例化
显式实例化:在函数名的后面,参数列表的前面加一对尖括号<>,尖括号内部指定模板参数的实际类型。如下:
Add<int>(a, b);
如果参数类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
3.模板参数的匹配原则
- 在我们的程序中,一个非模板函数是可以和一个同名的函数模板同时存在的,而且该函数模板还可以被实例化为这个非模板函数
T Add(const T& left, const T& right)
{
cout << "T Add(T& left,T& right)" << endl;
return left + right;
}
int Add(const int& left, const int& right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
int main()
{
Add(1, 2);
Add<int>(1, 2);
return 0;
}
我们运行之后,可以看到如下的结果:
我们发现,第一个Add函数调用了专门处理int类型的加法函数,而第二个Add函数调用了模板。
那么,为什么第一个Add函数不调用函数模板呢?
这是因为如下内容:
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。
- 如果条件不同的话,则会选择模板。
- 也就是说,编译器会优先调用更加匹配的版本调用!
我们可以看一下下面的这段代码:
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
// 通用加法函数
template<class T1, class T2>//注意这里有两个类型
T1 Add(T1 left, T2 right)
{
cout << "T1 Add(T1 left, T2 right)" << endl;
return left + right;
}
int main()
{
Add(1, 2);// 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0);// 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
return 0;
}
可以看到,第一个调用了非模板函数,第二个调用了模板函数。
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
对于模版T1 Add(T1 left, T2 right)
不知道返回值是T1或T2,可以选择auto,auto虽然不太适合做返回值,但是对于简单普通函数操作,可以进行自动类型转换。
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
//auto可作简单处理的函数返回值
template<class T1, class T2>
auto Add(const T1& left, const T2& right)
{
cout << "auto Add(const T1& left, const T2& right)" << endl;
return left + right;
}
int main()
{
Add(1, 2);// 与非函数模板类型完全匹配,不需要函数模板实例化
cout << Add(1, 2) << endl;
Add(1, 2.0);// 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
cout << Add(1, 2.0) << endl;
return 0;
}
四.类模板
1.类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public:
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
~Vector();//类外定义
void PushBack(const T& data);
void PopBack();
// ...
size_t Size() { return _size; }
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if (_pData)
delete[] _pData;
_size = _capacity = 0;
}
模版Vector中只是提供了一个模具,具体印刷出什么模型,是由编译器最终实例化决定的。
注意:模版不建议声明和定义分离到.h
和.cpp,
会出现链接错误,要分离也分离在.h。
2.类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字和变量名字中间加一个尖括号<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
Vector<int> intvector;
Vector<string> stringvector;
码字不易,如果你觉得博主写的不错的话,可以关注一下博主哦。