100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > 【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板

【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板

时间:2022-12-31 01:54:19

相关推荐

【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板

​​​​​​🤣爆笑教程👉《C++要笑着学》👈 火速订阅🔥

💭 写在前面

本章将正式开始介绍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(类型),

有些地方也会叫TPTYX,或者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;}

和上面说的一样,我们不会把印诗的模具传递出去,而是印出来的纸,

所以这里调用的当然不是模板,而是这个模板造出来的东西。

而函数模板造出 "实际要调用的" 的过程,叫做模板实例化。

编译器在调用之前会干一件事情 —— 模板实例化。

我们下面就来探讨一下模板实例化。

Ⅲ. 函数模板实例化

0x00 引入:这些不同类型的Swap函数是怎么来的

int a = 0, b = 1;Swap(a, b);

编译器在调用 Swap(a,b) 的时候,发现a b是整型的,编译器就开始找,

虽然没有找到整型对应的 Swap,但是这里有一份模板 ——

template<typename T> // 大家好我是模板,飘过~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 类型的代码,对于字符类型也是如此。

0x01 转到反汇编观察

🐞 我们刚才调试的时候在监视窗口已经看到了,它们的值成功交换了。

现在我们再调试一次,这次转到反汇编,去验证一下编译器通过模板生成函数这件事:

0x01 模板实例化的定义

模板将我们本来应该要重复做的活,交给了编译器去做。

编译器不是人,它不会累,让编译器拿着模板实例化就完事了。

用手搓衣服舒服,还是用洗衣机洗舒服?

自己手写舒服,还是编译器自己去生成舒服?

📚用不同类型的参数使用模板参数时,成为函数模板的实例化。

模板参数实例化分为:隐式实例化显式实例化,下面我们来分别讲解一下这两种实例化。

0x02 模板的隐式实例化

📚定义:让编译器根据实参,推演模板函数的实际类型。

我们刚才讲的 Swap 其实都是隐式实例化,就是让编译器自己去推。

💬 现在我们再举一个Add 函数模板做参考:

#include <iostream>using namespace std;template<class T>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<class T>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 <iostream>using namespace std;template<class T1, class T2>T1 Add(const T1& x, const T2& y) { // 那么T1就是int,T2就是doublereturn 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<int>(a1, d2);// 指定实例化成intAdd<double>(a1, d2) // 指定实例化成double

我们下面先来详细介绍一下显式实例化,然后再回来看看它是如何解决的。

0x03 模板的显式实例化

📚 定义:在函数名后的< >里指定模板参数的实际类型。

简单来说,显式实例化就是在中间加一个尖括号< >去指定你要实例化的类型。

(在函数名和参数列表中间加尖括号)

函数名 <类型> (参数列表);

💬 代码:解决刚才的问题

template<class T>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<int>(a1, d2) << endl;// 指定T用int类型cout << Add<double>(a1, d2) << endl; // 指定T用double类型return 0;}

🚩运行结果:

🔑 解读:

像第一个 Add<int>(a1, a2) ,a2 是 double,它就要转换成 int 。

第二个 Add<double>(a1, a2),a1 是 int,它就要转换成 double。

这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。

像 double 和 int 这种相近的类型,是完全可以通过隐式类型转换的。

如果无法成功转换,编译器将会报错。

🔺总结:

函数模板你可以让它自己去推,但是推的时候不能自相矛盾。

你也可以选择去显式实例化,去指定具体的类型。

0x04 模板参数的匹配原则

我们还是用刚才的 Add 函数模板来举例,现在我需要对整型的 a1 和 a2进行加法操作:

template<class T>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<class T>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<class T>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<int>(a1, a2) << endl; // 指定让编译器用模板,印一个int类型的Add函数return 0;}

② 对于非模板函数和同名函数模板,如果其他条件都相同,

在调用时会优先调用非模板函数,而不会从该模板生成一个实例。

如果模板可以产生一个具有更好匹配的函数,那么将选择模板。

// 专门处理int的加法函数int Add(int x, int y) {cout << "我是专门处理int的Add函数: ";return x + y;}// 通用加法函数template<class T1, class T2>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;}

Ⅳ. 类模板

0x00 引入:和本篇开头本质上是一样的问题

💬 就比如 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;}

你只能做两个栈,如果需要更多的数据类型……

那就麻烦了,你需要不停地CV做出各种数据类型版本的栈:

class StackInt {...};class StackDouble {...};……

这和文章开头提到的问题(Swap)本质上是一个问题,就是不支持泛型。

它们类里面的代码几乎是完全一样的,只是类型的不同。

函数我们可以使用模板,类也是可以的,我们下面就来讲解一下类模板。

0x01 类模板的定义格式

📚 定义:和函数模板的定义方式是一样的,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;};int main(void){Stack st1; // 存储intStack st2; // 存储doublereturn 0;}

但是我们发现,类模板他好像不支持自动推出类型,

它不像函数模板,不指定它也可以根据传入的实参去推出对应的类型的函数以供调用。

函数模板之所以能推,是因为有实参传形参这么一个 "契机" ,让编译器能帮你推。

你定义一个类,它能推吗?没这个能力你知道吧!

所以这里只支持显示实例化,我们继续往下看。

0x02 类模板实例化

基于上面的原因,我们想要对类模板实例化,我们可以使用显示实例化。

类模板实例化在类模板名字后跟< >,然后将实例化的类型放在< >中即可。

类名 <类型> 变量名;

💬 代码演示:解决刚才的问题

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;};int main(void){Stack<int> st1;// 指定存储intStack<double> st2; // 指定存储doublereturn 0;}

📌注意事项:

① Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。

template<class T>class Stack {...};

类模板名字不是真正的类,而实例化的结果才是真正的类。

② Stack 是类名,Stack<int> 才是类型:

Stack<int> s1;Stack<double> s2;

0x03 类外定义类模板参数

❓ 思考问题:下面的 Push 为什么会报错?

template<class T>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<int> 才是类型。这里要拿 Stack<T> 去指定类域才对。

②类模板中的函数在类外定义,没加 "模板参数列表" ,编译器不认识这个T。类模板中函数放在类外进行定义时,需要加模板参数列表。

这段代码第一个问题是没有拿 Stack<T> 去指定类域,

最大问题其实是编译器压根就不认识这个T

即使你用拿类型Stack<T> 指定类域,编译器也一样认不出来:

我们拿析构函数~Stack 来演示一下:

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;};/* 类外 */Stack<int>::~Stack() { ❌ // 即使是指定类域也不行 ...}

💬 代码演示:我们现在来看一下如何添加模板参数列表!

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;}

这样编译器就能认识了。

本章完!

📄文章信息

📌 [ 笔者 ] 王亦优📃 [ 更新 ] .4.18❌ [ 勘误 ] 暂无📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

. C++reference[EB/OL]. []. /reference/.

百度百科[EB/OL]. []. /.

比特科技. C++[EB/OL]. [.8.31]. .

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。