文章目录
- 1.编程范式
- 2.函数模板
- 2.1 函数模板概念
- 2.2 函数模板原理
- 2.3 函数模板实例化
- 2.3.1 隐式实例化
- 2.3.2 显式实例化
- 2.4 模板参数的匹配原则
- 3.类模板
- 希望读者们多多三连支持
- 小编会继续更新
- 你们的鼓励就是我前进的动力!
进入STL库学习之前我们要先了解有关模板的学习,以便在学习完STL库使用之后,能更深入的了解其底层工作原理
1.编程范式
编程范式
指的是我们使用编程的基本风格和方法
常见的方式有以下几种:
面向对象编程(OOP)
将数据和操作数据的方法封装在类中,通过类的实例(对象)来进行交互,强调数据的封装、继承和多态性
定义一个Shape
基类,包含计算面积的纯虚函数,再派生出Circle
和Rectangle
等类,重写计算面积的函数,体现了面向对象的继承
和多态
特性
函数式编程
将计算视为函数的组合和应用,强调不可变数据和纯函数,避免副作用,注重函数的输入输出关系
使用std::function
和lambda
表达式可以方便地进行函数式编程,如用lambda
表达式定义一个简单的加法函数
,不修改外部状态,只返回计算结果
过程式编程
以过程(函数)为中心,将程序分解为一系列的步骤和函数调用,数据和操作数据的函数相对独立
传统的C语言
风格的编程方式,如编写一个计算阶乘的函数,通过循环
和递归
来实现计算过程,就是典型的过程式编程
泛型编程
定义函数、类或其他程序结构时,不指定具体的数据类型,而是使用类型参数来代表未知的数据类型
在algorithm
头文件中的swap
函数就是一种常见的泛式编程,他不指定任何类型就能实现交换,依靠的就是泛式编程,也是我们接下来要学习的模板
2.函数模板
在还不知道头文件前实现swap函数
通常是这样的:
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;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
......
为了符合各个场景下实现参数互换,要对同一个函数实现不同类型的函数重载
,这种方式固然可行,但是每个类型都写一遍太过于冗余了
- 重载的函数仅仅是
类型不同
,代码复用率比较低
,只要有新类型出现时,就需要用户自己增加对应的函数 - 代码的
可维护性比较低
,一个出错可能所有的重载均出错
2.1 函数模板概念
我们知道文字的印刷是依靠活字印刷术的模板实现的,那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
这里用到的模板就是函数模板
,其语法形式为:
template<typename T1, typename T2,......,typename Tn>
template
就是模板的意思,是用来定义模板参数关键字,也可以使用class
,切记:不能使用struct
代替class
,因为struct
和class
的默认权限不同,会导致一些混淆和潜在的问题
2.2 函数模板原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
举个例子:
template<typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
实现一个Swap交换函数
对两个不同类型的函数进行同一个函数的调用,调试模式下转到反汇编可以发现,两个函数式模板示例化后被调用的
这直接说明了调用的不是同一个函数
在编译器编译阶段
,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数
以供调用。
比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码
,这个类型无论是内置类型
还是自定义类型
都可以
2.3 函数模板实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化
2.3.1 隐式实例化
让编译器根据实参推演模板参数的实际类型
叫作隐式实例化
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
return 0;
}
正常情况下的调用就是隐式实例化
🔥值得注意的是: Add函数前加const是因为这里如果像下面例子一样进行强制转化会生成临时变量,具有常性
该知识点在前面有提到过:
传送门:C++命运石之门代码抉择:C++入门(中)
2.3.2 显式实例化
在函数名后的<>中指定模板参数的实际类型
叫作显式实例化
Add(a1, d1);
还是上面的例子,如果既调用int,又调用double,到底是用哪种类型编译器无法决定
,就需要显式实例化
🚩用户自己来强制转化
Add(a1, (int)d1);
🚩使用显式实例化
Add<int>(a1, d1);
指定T的类型为int
这通常不是显式实例化的常用场景,举个例子:
template<class T>
T* Alloc(int n)
{
return new T[n];
}
int main()
{
Alloc<int>(5);
return 0;
}
如果写成Alloc(5)
,编译器不知道你要分配的是int数组
、double数组
还是其他类型的数组,所以无法自动推导T的类型
,这时候就需要显式指定模板参数,像Alloc<int>(5)
这样明确告诉编译器T是int类型
2.4 模板参数的匹配原则
🚩一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
🚩对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函
数
}
🚩模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
这里的自动转化就是上面的实例化中的转化,也要和auto自动推导区分开,不是同一个东西
3.类模板
类模板其实和函数模板是类似的
其语法形式为:
template<class T1, class T2, ..., class Tn>
因为类不像函数那样语法上支持自动类型转化,所以类模板调用必须显式实例化
// 动态顺序表
// 注意: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;
}
int main()
{
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
return 0;
}
我们在写模板类时尽量不要声明定义分离
,原因有些复杂放在模板进阶的时候讲,如果一定分离的话要注意:
- 对于
普通类
,类名和类型一样 - 对于
模板类
,Vector类名
,Vector<int>才是类型