爆笑教程 《C++要笑着学》 火速订阅
写在前面
我是柠檬叶子C,首先要说的是 —— 我们的《C++要笑着学》排版升级了!阅读体验更上一层楼,强烈建议电脑端阅读!本章将正式开始介绍C++中的模板,为了能让大家更好地体会到用模板多是件美事!我们将会举例说明,大家可以试着把自己带入到文章中,跟着思路去阅读和思考,真的会很有意思!如果你对网络流行梗有了解,读起来将会更有意思!
在C语言中,我们实现两数交换,不用花的方法(异或啥的),中规中矩的写法是通过 tmp 交换。
比如我们这里想交换 变量a 和 变量b 的值,我们可以写一个 Swap 函数:
void Swap(int* px, int* py) {
int tmp = *px; // 创建临时变量,存储a的值
*px = *py; // 将b的值赋给a
*py = tmp; // 让b从tmp里拿到a的值
}
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 = 2.2; // 浮点型
Swap(&a, &b);
Swap(&c, &d);
return 0;
}
似乎不太行,因为我们实现的 Swap 函数接受的是整形数据,这里传的是浮点数了。
我们可以再写一个浮点数版本的 Swap 函数…… 叫 SwapDouble
void SwapDouble(double* px, double* py) {
double tmp = *px;
*px = *py;
*py = tmp;
}
不错,问题是解决了。但是我现在又出现了字符型的 变量e 和 变量f 呢?
那我现在又出现了各种乱七八糟的类型呢?
SwapInt、SwapDouble、SwapChar 真是乱七八糟的,
❓ 能不能实现一个通用的 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 = 2.2;
char e = 'e', f = 'f';
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
好像靠函数重载来调用不同类型的 Swap,只是表面上看起来 "通用" 了 ,
实际上问题还是没有解决,有新的类型,还是要添加对应的函数……
❌ 用函数重载解决的缺陷:
① 重载的函数仅仅是类型不同,代码的复用率很低,只要有新类型出现就需要增加对应的函数。
② 代码的可维护性比较低,一个出错可能导致所有重载均出错。
你看我做表情,有些是可以靠模板去制作的,比如这种 "狂粉举牌" 表情:
这就是模板!如果在C++中也能够存在这样一个模板该有多好?
就像这里,只要在板子上写上名字(类型),
就可以做出不同的 "举牌表情"(生成具体类型的代码)。
巧妙的是!C++里面有这种神器!
而且大佬已经把神器打造好了,你只要学会如何使用就能爽到飞起!
下面让我们开始函数模板的学习!在这之前我们再来科普一下什么是泛型编程。
泛型编程: 编写与类型无关的调用代码,是代码复用的一种手段。 模板是泛型编程的基础。
上面我们提到了 "神器" ,现在我们来学会如何去使用它,我们先来介绍一下概念。
函数模板代表了一个函数家族,该函数模板与类型无关,
在使用时被参数化,根据实参类型产生函数的特定类型版本。
template
返回值类型 函数名(参数列表){}
① template 是定义模板的关键字,后面跟的是尖括号 < >
② typename 是用来定义模板参数的关键字
③ T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。
解决刚才的问题:
① 我们来定义一个叫 Swap 的函数,我们这不给具体的类型:
void Swap();
② 然后在它的前面定义一个具体的类型:
template // template +
void Swap();
③ 这时候,我们就可以用这个模板名来做类型了:
template // 模板参数列表 ———— 参数类型
void Swap(T& rx, T& ry) { // 函数参数列表 ———— 参数对象
T tmp = rx;
rx = ry;
ry = tmp;
}
这,就是函数模板!虽然参数的名字我们可以自己取 (你写成 TMD 也没人拦你 )
但是我们一般喜欢给它取名为 T,因为 T 代表 Type(类型),
有些地方也会叫 TP、TY、X ,或者 KV结构(key-value-store)我们还会给它取名为 KING,
当然,如果你需要多个类型,也是可以定义多个类型的:
template
注意事项:
① 函数模板不是一个函数,因为它不是具体要调用的某一个函数,而是一个模板。就像 "好学生",主体是学生,"好" 是形容 "学生" 的;这里也一样,"函数模板" 是模板,所以 函数模板表达的意思是 "函数的模板" 。所以,我们一般不叫它模板函数,应当叫作函数模板。
"函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。" —— 《百度百科》
② 我们在用 template< > 定义模板的时候,尖括号里的 typename 其实还可以写成 class:
template // 使用class充当typename (具体后面会说)
void Swap(T& rx, T& ry) {
T tmp = rx;
rx = ry;
ry = tmp;
}
现在我们把完整的代码跑一下看看:
template
void Swap(T& rx, T& ry) {
T tmp = rx;
rx = ry;
ry = tmp;
}
int main(void)
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
char e = 'e', f = 'f';
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
调试,打开监视看看是否都成功交换了:
搞定!我们使用模板成功解决了问题,实现了通用的 Swap 函数!
如果是自定义类型,函数里面就要是拷贝构造,你要实现好就行。
因为 T 没有规定是什么类型,所以任意类型都是可以的,内置类型和自定义类型都可以的。
❓ 思考:这下面三个调用调用的是同一个函数吗?
不是同一个函数。这三个函数执行的指令是不一样的,你可以这么想,
它们都需要建立栈帧,栈帧里面是要开空间的,你就要给 rx 开空间,
rx 的类型都不一样(double int char)。所以当然调用的不是同一个函数了。
比如说我现在想把杜甫写的《登高》做出一万份出来,怎么做?
最后我们传递出去的也不是印诗的模具,而是印出来的纸,
不管是手抄还是印刷,传递出去的都是纸。
所以我们再来看这里的代码:
template
void Swap(T& rx, T& ry) {
T tmp = rx;
rx = ry;
ry = tmp;
}
int main(void)
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
char e = 'e', f = 'f';
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
和上面说的一样,我们不会把印诗的模具传递出去,而是印出来的纸,
而函数模板造出 "实际要调用的" 的过程,叫做模板实例化。
编译器在调用之前会干一件事情 —— 模板实例化。
我们下面就来探讨一下模板实例化。
int a = 0, b = 1;
Swap(a, b);
编译器在调用 Swap(a, b) 的时候,发现 a b 是整型的,编译器就开始找,
虽然没有找到整型对应的 Swap,但是这里有一份模板 ——
template // 大家好我是模板,飘过~
void Swap(T& rx, T& ry) {
T tmp = rx;
rx = ry;
ry = tmp;
}
这里要的是整型,编译器就通过这个模板,推出一个 T 是 int 类型的函数。
这时编译器就把这个模板里的 T 都替换成 int,生成出一份 T 是 int 的函数。
char e = 'e', f = 'f';
Swap(e, f);
一样的,如果要调用 Swap(e, f) ,e f 是字符型,编译器就会去实例化出一个 char 的。
你调的函数还是那些函数,只是你写一份模板出来,让编译器去用模板生成那些函数。
前面注意事项那里我们说过,函数模板本身不是函数。
它是是编译器使用方式产生特定具体类型函数的模具,在编译器编译阶段,
对于模板函数的使用,编译器需要根据传入的实参类型来推演,生成对应类型的函数以供调用。
比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,
将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于字符类型也是如此。
我们刚才调试的时候在监视窗口已经看到了,它们的值成功交换了。
现在我们再调试一次,这次转到反汇编,去验证一下编译器通过模板生成函数这件事:
模板将我们本来应该要重复做的活,交给了编译器去做。
编译器不是人,它不会累,让编译器拿着模板实例化就完事了。
自己手写舒服,还是编译器自己去生成舒服?
用不同类型的参数使用模板参数时,成为函数模板的实例化。
模板参数实例化分为:隐式实例化 和 显式实例化 ,下面我们来分别讲解一下这两种实例化。
定义:让编译器根据实参,推演模板函数的实际类型。
我们刚才讲的 Swap 其实都是隐式实例化,就是让编译器自己去推。
现在我们再举一个 Add 函数模板做参考:
#include
using namespace std;
template
T Add(const T& x, const T& y) {
return x + y;
}
int main(void)
{
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 这种情况呢?实例化能成功吗?
Add(a1, d2);
这必然是失败的, 因为会出现冲突。一个要把它实例化成 int ,一个要把它实例成 double,
解决方式
① 传参之前先进行强制类型转换,非常霸道的解决方式:
template
T Add(const T& x, const T& y) {
return x + y;
}
int main(void)
{
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
using namespace std;
template
T1 Add(const T1& x, const T2& y) { // 那么T1就是int,T2就是double
return x + y; // 范围小的会像范围大的提升,int会像double "妥协"
} // 最后表达式会是一个double,但是最后返回值又是T1,是int,又会转
int main(void)
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, d2) << endl; // int,double
return 0;
}
当然,这种问题严格意义上来说是不会用多个参数来解决的,
这里只是想从语法上演示一下,我们还有更好地解决方式,我们继续往下看。
③ 我们还可以使用 "显式实例化" 来解决:
Add(a1, d2); // 指定实例化成int
Add(a1, d2) // 指定实例化成double
我们下面先来详细介绍一下显式实例化,然后再回来看看它是如何解决的。
定义:在函数名后的 < > 里指定模板参数的实际类型。
简单来说,显式实例化就是在中间加一个尖括号 < > 去指定你要实例化的类型。
(在函数名和参数列表中间加尖括号)
函数名 <类型> (参数列表);
代码:解决刚才的问题
template
T Add(const T& x, const T& y) {
return x + y;
}
int main(void)
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
cout << Add(a1, d2) << endl; // 指定T用int类型
cout << Add(a1, d2) << endl; // 指定T用double类型
return 0;
}
运行结果:
解读:
像第一个 Add<int>(a1, a2) ,a2 是 double,它就要转换成 int 。
第二个 Add<double>(a1, a2),a1 是 int,它就要转换成 double。
这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。
像 double 和 int 这种相近的类型,是完全可以通过隐式类型转换的。
总结:
函数模板你可以让它自己去推,但是推的时候不能自相矛盾。
你也可以选择去显式实例化,去指定具体的类型。
我们还是用刚才的 Add 函数模板来举例,现在我需要对整型的 a1 和 a2 进行加法操作:
template
T Add(const T& x, const T& y) {
return x + y;
}
int main(void)
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl;
return 0;
}
我们是通过这个 Add 函数模板,生成 int 类型的加法函数的。
如果我们有一个现成的、专门用来处理 int 类型加法的函数:
// 专门处理int的加法函数
int Add(int x, int y) {
return x + y;
}
// 通用加法函数
template
T Add(const T& x, const T& y) {
return x + y;
}
int main(void)
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl;
return 0;
}
❓ 思考:如果你是编译器,当 Add(a1, a2) 时你会选择用哪一个?
是用函数模板印一个 int 类型的 Add 函数,还是用这现成的 Add 函数呢?
我们继续往下看……
匹配原则:
① 一个非模板函数可以和一个同名的模板函数同时存在,
而且该函数模板还可以被实例化为这个非模板函数:
// 专门处理int的加法函数
int Add(int x, int y) {
cout << "我是专门处理int的Add函数: ";
return x + y;
}
// 通用加法函数
template
T Add(const T& x, const T& y) {
cout << "我是模板参数生成的: ";
return x + y;
}
int main(void)
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl; // 默认用现成的,专门处理int的Add函数
cout << Add(a1, a2) << endl; // 指定让编译器用模板,印一个int类型的Add函数
return 0;
}
② 对于非模板函数和同名函数模板,如果其他条件都相同,
在调用时会优先调用非模板函数,而不会从该模板生成一个实例。
如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
// 专门处理int的加法函数
int Add(int x, int y) {
cout << "我是专门处理int的Add函数: ";
return x + y;
}
// 通用加法函数
template
T1 Add(const T1& x, const T2& y) {
cout << "我是模板参数生成的: ";
return x + y;
}
int main(void)
{
cout << Add(1, 2) << endl; // 用现成的
//(与非函数模板类型完全匹配,不需要函数模板实例化)
cout << Add(1, 2.0) << endl; // 可以,但不是很合适,自己印更好
//(模板参数可以生成更加匹配的版本,编译器根据实参生产更加匹配的Add函数)
return 0;
}
就比如 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 int STDataType;
class Stack {
public:
Stack(STDataType capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new int[capacity];
}
~Stack() {
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _arr;
int _top;
int _capacity;
};
如果需要改变栈的数据类型,直接改 typedef 那里就可以了。
它最大的问题是不能同时存储两个类型,你就算是改也没法解决:
int main(void)
{
Stack st1; // 存int数据
Stack st2; // 存double数据
return 0;
}
你只能做两个栈,如果需要更多的数据类型……
class StackInt {...};
class StackDouble {...};
……
这和文章开头提到的问题(Swap)本质上是一个问题,就是不支持泛型。
它们类里面的代码几乎是完全一样的,只是类型的不同。
函数我们可以使用模板,类也是可以的,我们下面就来讲解一下类模板。
定义:和函数模板的定义方式是一样的,template 后面跟的是尖括号 < > :
template
class 类模板名 {
类内成员定义
}
代码:解决刚才的问题
template
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;
};
int main(void)
{
Stack st1; // 存储int
Stack st2; // 存储double
return 0;
}
它不像函数模板,不指定它也可以根据传入的实参去推出对应的类型的函数以供调用。
函数模板之所以能推,是因为有实参传形参这么一个 "契机" ,让编译器能帮你推。
你定义一个类,它能推吗?没这个能力你知道吧!
所以这里只支持显示实例化,我们继续往下看。
基于上面的原因,我们想要对类模板实例化,我们可以使用显示实例化。
类模板实例化在类模板名字后跟 < >,然后将实例化的类型放在 < > 中即可。
类名 <类型> 变量名;
代码演示:解决刚才的问题
template
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;
};
int main(void)
{
Stack st1; // 指定存储int
Stack st2; // 指定存储double
return 0;
}
注意事项:
① Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。
template
class Stack {...};
类模板名字不是真正的类,而实例化的结果才是真正的类。
② Stack 是类名,Stack<int> 才是类型:
Stack s1;
Stack s2;
❓ 思考问题:下面的 Push 为什么会报错?
template
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;
};
/* 类外 */
void Stack::Push(const T& x) { ❌
...
}
解答:
① Stack 是类名,Stack
② 类模板中的函数在类外定义,没加 "模板参数列表" ,编译器不认识这个 T 。类模板中函数放在类外进行定义时,需要加模板参数列表。
这段代码第一个问题是没有拿 Stack<T> 去指定类域,
最大问题其实是编译器压根就不认识这个T!
即使你用拿类型 Stack<T> 指定类域,编译器也一样认不出来:
我们拿析构函数 ~Stack 来演示一下:
template
class Stack {
public:
Stack(T capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new T[capacity];
}
// 这里我们让析构函数放在类外定义
~Stack();
private:
T* _arr;
int _top;
int _capacity;
};
/* 类外 */
Stack::~Stack() { ❌ // 即使是指定类域也不行
...
}
代码演示:我们现在来看一下如何添加模板参数列表!
template
class Stack {
public:
Stack(T capacity = 4)
: _top(0)
, _capacity(capacity) {
_arr = new T[capacity];
}
// 这里我们让析构函数放在类外定义
~Stack();
private:
T* _arr;
int _top;
int _capacity;
};
// 类模板中函数放在类外进行定义时,需要加模板参数列表
template
Stack::~Stack() { // Stack是类名,不是类型! Stack 才是类型,
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
本章完!
文章信息
[ 笔者 ] 王亦优
[ 更新 ] 2022.4.8
❌ [ 勘误 ] 暂无
[ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
本人也很想知道这些错误,恳望读者批评指正!
参考资料
Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
. C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.
百度百科[EB/OL]. []. https://baike.baidu.com/.
比特科技. C++[EB/OL]. 2021[2021.8.31]. .