🔑🔑博客主页:阿客不是客
🍓🍓系列专栏:从C语言到C++语言的渐深学习
欢迎来到泊舟小课堂
😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注
一、泛型编程
1.1 引入
C 语言中实现两数交换,中规中矩的写法是通过 tmp 交换。比如我们这里想交换 变量a 和 变量b 的值,我们可以写一个 Swap 函数:
void Swap(int* px, int* py)
{int tmp = *px;*px = *py; *py = tmp;
}int main(void)
{int a = 0, b = 1;Swap(&a, &b); // 传址return 0;
}
变量a 和 变量b 是整型,如果现在有了是浮点型的 变量c 和 变量d。还可以用我们这个整型的 Swap 函数交换吗?
void Swap(int* px, int* py)
{int tmp = *px;*px = *py;*py = tmp;
}int main(void)
{int a = 0, b = 1;double c = 1.1, d = 1.2;Swap(&a, &b); // 传址Swap(&c, &d);return 0;
}
❓ 数据的类型是多种多样的,那我们能不能实现一个通用的 Swap 函数呢?
那我们不用C语言了!我们用 C++,C++ 里面不是有函数重载嘛!用 C++ 我们还能用引用的方法交换呢,直接传引用,取地址符号都不用打了,多好!
💬 test.cpp: 于是改成了C++之后 ——
void Swap(int& rx, int& ry) {int tmp = rx;rx = ry;ry = tmp;
}
void Swap(double& rx, double& ry) {double tmp = rx;rx = ry;ry = tmp;
}
void Swap(char& rx, char& ry) {char tmp = rx;rx = ry;ry = tmp;
}int main(void)
{int a = 0, b = 1;double c = 1.1, d = 1.2;Swap(a, b); // 传址Swap(c, d);return 0;
}
好像靠函数重载来调用不同类型的 Swap,只是表面上看起来 "通用" 了 ,实际上问题还是没有解决,有新的类型,还是要添加对应的函数,看起来并没有比C语言好到哪去……
❌ 用函数重载解决的缺陷:
① 重载的函数仅仅是类型不同,代码的复用率很低,只要有新类型出现就需要增加对应的函数。
② 代码的可维护性比较低,一个出错可能导致所有重载均出错。
正好,在C++中有一种被称为模板的东西可以巧妙解决我们的问题。那什么叫模板呢?在生活中我们制作一样东西可以在一定基础的模板上进行改造,就像下列表情包所为:
下面让我们开始函数模板的学习!在这之前我们再来科普一下什么是泛型编程。
1.2 什么是泛性编程
泛型编程是一种编程风格,其中算法以尽可能抽象的方式编写,而不依赖于将在其上执行这些算法的数据形式。这个概念在 1989 年由 David Musser 和 Alexander A. Stepanov 首次提出。
泛型,就是针对广泛的类型的意思。
泛型编程: 编写与类型无关的调用代码,是代码复用的一种手段。 模板是泛型编程的基础。
二、函数模板
2.1 函数模板的概念
在C++中,模板(template)是一种通用的编程工具,允许程序员编写通用代码以处理多种数据类型或数据结构,而不需要为每种特定类型编写重复的代码,通过模板,可以实现代码的复用和泛化,提高代码的灵活性和可维护性———简而言之就是可以写一份通用的模板,便于阅读和管理
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 函数模板的格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
- template是声明模板的关键字,告诉编译器开始泛型编程。
- 尖括号<>中的typename是定义形参的关键字,用来说明其后的形参名为类型 参数,(模板形参)。Typename(建议用)可以用class关键字代替,两者没有区别。
- T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。
💬 解决刚才的问题,构造一个通用的Swap函数:
template<class T>
void Swap(T& rx, T& ry)
{T tmp = rx;rx = ry;ry = tmp;
}
2.3 函数模板的原理
学会了如何使用函数模版之后,我们就可能会有一个疑问:那就是不同的类型调用的函数模版是同一个函数吗?这时我们继续利用上面代码,通过调用汇编来观察一下:
通过反汇编观察我们可以知道,当调用实参类型不同时,调用的函数也不同。当然也是符合逻辑的,不同类型的大小不同,调用的函数栈帧大小也就不同,自然也不可能调用同一个函数。
那么函数模版到底是如何调用的呢?其实也非常简单,函数模版就是一个蓝图,根据不同的参数类型生成对应的函数,只不过这件事我们将它交给了编译器来做了。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于其他类型也是如此
2.4、函数模板实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
2.4.1 模板的隐式实例化
我们刚才讲的 Swap 其实都是隐式实例化,就是让编译器自己去推。
💬 现在我们再举一个 Add 函数模板做参考:
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.2;cout << Add(a1, a2) << endl;cout << Add(d1, d2) << endl;return 0;
}
❓ 现在思考一个问题,如果出现 a1 + d2 这种情况呢?实例化能成功吗?
这必然是失败的, 因为会出现冲突。一个要把它实例化成 int ,一个要把它实例成 double,
① 传参之前先进行强制类型转换:
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.2;cout << Add(a1, a2) << endl;cout << Add(d1, d2) << endl;cout << Add((double)a1, d2) << endl;return 0;
}
② 写两个参数,那么返回的参数类型就会起决定性作用:
#include <iostream>
using namespace std;template<class T1, class T2>
T1 Add(const T1& x, const T2& y) // 那么T1就是int,T2就是double
{ return x + y; // 范围小的会像范围大的提升,int会像double "妥协"
} // 最后表达式会是一个double,但是最后返回值又是T1,是int,又会发生强制类型转换int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.2;cout << Add(a1, d2) << endl; return 0;
}
但是这种方法的类型非常混乱,很可能写着写着就不知道输出的是什么类型了,而且强制类型转换还会发生精度丢失,出现一些奇奇怪怪的bug,所以我们还有更好的写法:
③ 我们还可以使用 "显式实例化" 来解决:
Add<int>(a1, d2); // 指定实例化成int
Add<double>(a1, d2) // 指定实例化成double
2.4.2 模板的显式实例化
定义:在函数名后的 < > 里指定模板参数的实际类型。
函数名 <类型> (参数列表);
简单来说,显式实例化就是在中间加一个尖括号 < > 去指定你要实例化的类型。(在函数名和参数列表中间加尖括号)
- 如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.2;cout << Add(a1, a2) << endl;cout << Add(d1, d2) << endl;cout << Add<int>(a1, d2) << endl; // 指定T用int类型cout << Add<double>(a1, d2) << endl; // 指定T用double类型return 0;
}
除了以上场景需要显式实例化外,还有一种场景也需要我们显式实例化:
template <class T1, class T2>
void Add(T1 x)
{T2 y = 10;cout << a + b<< endl;
}
存在模板无法推演的情况,因此必须要显示实例化
2.4.3 模板参数的匹配
我们还是用刚才的 Add 函数模板来举例,现在我需要对整型的 a1 和 a2 进行加法操作,
💬 如果我们有一个现成的、专门用来处理 int 类型加法的函数,但同时有 Add 函数模板,生成 int 类型的加法函数的:
匹配原则:
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
int Add(int left, int right)
{cout << "int Add(int left, int right)" << endl;return left + right;
}template<class T>
T Add(T left, T right)
{cout << "T Add(T left, T right)" << endl;return left + right;
}int main()
{Add(1, 2); // 与非模板函数匹配,编译器不需要特化Add<int>(1, 2); // 调用编译器特化的Add版本
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板.
四、类模板
4.1 引入
💬 就比如 Stack,如果我们定它是 int,那么它就是存整型的栈:
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;
};
❓ 如果我想改成存 double 类型的栈呢?
当时我们在讲解数据结构的时候,是用 typedef 来解决的,如果需要改变栈的数据类型,直接改 typedef 那里就可以了。
但问题是我需要同时存两种不同的栈呢?这是改也无法解决的。
这和文章开头提到的问题(Swap)本质上是一个问题,就是不支持泛型。它们类里面的代码几乎是完全一样的,只是类型的不同。函数我们可以使用模板,类也是可以的,我们下面就来讲解一下类模板。
4.2 类模板的定义格式
定义:和函数模板的定义方式是一样的,template 后面跟的是尖括号 < > :
template<class T1, class T2, ..., class Tn>
class 类模板名{
类内成员定义
}
💬 代码演示:解决刚才的问题
template<class T>
class Stack
{
public:Stack(T 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;
};
但是由于类模板没有传参的功能,所以无法像普通函数模板一样,根据传入的实参来推测对应类型的函数以供使用:
所以,类模板只支持显示实例化
4.3 类模板的显示实例化
基于上面的原因,我们想要对类模板实例化,我们可以使用显示实例化。类模板实例化在类模板名字后跟 < >,然后将实例化的类型放在 < > 中即可。
类名 <类型> 变量名;
📌 注意事项:
- Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。类模板名字不是真正的类,而实例化的结果才是真正的类。
- Stack 是类名,Stack<int> 才是类型。
- 类模板中的函数在类外定义,没加 "模板参数列表" ,编译器不认识这个 T 。类模板中函数放在类外进行定义时,需要加模板参数列表。
template<class T>
class Stack {
public:Stack(T capacity = 4) : _top(0) , _capacity(capacity) {_arr = new T[capacity];}// 这里我们让析构函数放在类外定义,在类中只进行声明~Stack();
private:T* _arr;int _top;int _capacity;
};// 类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T> 👈 必须要加!!!
Stack<T>::~Stack() // Stack是类名,不是类型! Stack<T> 才是类型,
{delete[] _arr;_arr = nullptr;_capacity = _top = 0;
}