💭 写在前面
本章将正式开始介绍C++中的模板,为了能让大家更好地体会到用模板多是件美事!我们将会举例说明,大家可以试着把自己带入到文章中,跟着思路去阅读和思考,真的会很有意思!如果你对网络流行梗有了解,读起来将会更有意思!
Ⅰ. 泛型编程
0x00 引入 - 通用的交换函数
在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++里面有这种神器!!!
而且大佬已经把神器打造好了,你只要学会如何使用就能爽到飞起!
下面让我们开始函数模板的学习!在这之前我们再来科普一下什么是泛型编程。
0x01 什么是泛型编程
泛型,就是针对广泛的类型的意思。
泛型编程: 编写与类型无关的调用代码,是代码复用的一种手段。 模板是泛型编程的基础。
Ⅱ. 函数模板
0x00 函数模板的概念
上面我们提到了 "神器" ,现在我们来学会如何去使用它,我们先来介绍一下概念。
📚 函数模板代表了一个函数家族,该函数模板与类型无关,
在使用时被参数化,根据实参类型产生函数的特定类型版本。
0x01 函数模板格式
template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名(参数列表){}
① template 是定义模板的关键字,后面跟的是尖括号 < >
② typename 是用来定义模板参数的关键字
③ T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。
👈 就像这个表情包模板,我给他取名为 "狂粉举牌" 表情。
💬 解决刚才的问题:
① 我们来定义一个叫 Swap 的函数,我们这不给具体的类型:
void Swap();
② 然后在它的前面定义一个具体的类型:
template<typename T> // template + <typename 模板名> void Swap();
③ 这时候,我们就可以用这个模板名来做类型了:
template<typename T> // 模板参数列表 ———— 参数类型 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<typename T1, typename T2, typename T3>
📌 注意事项:
① 函数模板不是一个函数,因为它不是具体要调用的某一个函数,而是一个模板。就像 "好学生",主体是学生,"好" 是形容 "学生" 的;这里也一样,"函数模板" 是模板,所以 函数模板表达的意思是 "函数的模板" 。所以,我们一般不叫它模板函数,应当叫作函数模板。
"函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。" —— 《百度百科》
② 我们在用 template< > 定义模板的时候,尖括号里的 typename 其实还可以写成 class:
template<class T> // 使用class充当typename (具体后面会说) void Swap(T& rx, T& ry) { T tmp = rx; rx = ry; ry = tmp; }
🚩 现在我们把完整的代码跑一下看看:
template<typename T> 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 没有规定是什么类型,所以任意类型都是可以的,内置类型和自定义类型都可以的。
真是太香了!这,就是模板!
0x02 模板函数的原理
❓ 思考:这下面三个调用调用的是同一个函数吗?
🔑 不是同一个函数。这三个函数执行的指令是不一样的,你可以这么想,
它们都需要建立栈帧,栈帧里面是要开空间的,你就要给 rx 开空间,
rx 的类型都不一样(double int char)。所以当然调用的不是同一个函数了。
我们来思考一下模板函数的原理是什么。
比如说我现在想把杜甫写的《登高》做出一万份出来,怎么做?
最后我们传递出去的也不是印诗的模具,而是印出来的纸,
不管是手抄还是印刷,传递出去的都是纸。
💬 所以我们再来看这里的代码:
template<typename T> 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; }
和上面说的一样,我们不会把印诗的模具传递出去,而是印出来的纸,
所以这里调用的当然不是模板,而是这个模板造出来的东西。
而函数模板造出 "实际要调用的" 的过程,叫做模板实例化。
编译器在调用之前会干一件事情 —— 模板实例化。
我们下面就来探讨一下模板实例化。