【C++】泛型编程 | 函数模板 | 类模板

一、泛型编程

泛型编程是啥?

编写一种一般化的、可通用的算法出来,是代码复用的一种手段。

类似写一个模板出来,不同的情况,我们都可以往这个模板上去套。

举个例子:

void Swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}
int main()
{int a = 1, b = 2;Swap(a, b);cout << a << b<<endl;return 0;
}

这是一个交换函数。如果很多不同类型的数据需要交换,咋办?

🤔函数重载?

函数重载的确可以解决,但是每多一种数据,都要实现对应的重载函数。实在太麻烦了。

我们想要的是:有一个一般化的模板,不管是什么类型,往这个模板函数上套用就行。这就是泛型编程的思想。

当用上泛型编程:

template<typename T>
void Swap(T& a, T& b)
{T tmp = a;a = b;b = tmp;
}
int main()
{char a = 'a', b = 'b';Swap(a, b);cout << a << b<<endl;int c = 1, d = 2;Swap(c, d);cout << c << d << endl;
​return 0;
}

结果:

 

接下来我们具体介绍如何使用泛型编程。

二、函数模板

泛型编程思想下得到的函数,就像是过了模具得到的。这些“模具”,被称作函数模板。

函数模板不是一个函数,而是一个模板。

函数模板的参数是一个模板,可以包含多个类型,返回值也是一个模板,可以包含多个类型。

格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}

注:

1.typename是用来定义模板参数关键字,也可以使用class。

2.tyname后面的类型名可以自己定,我们常常取为T(type)、Ty、K、V等,一般是大写字母or单词首字母大写。

运用起来:

template<typename T>
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}

template<class T>
……

原理

❓Q:这俩调用的是同一个Swap函数吗?

char a = 'a', b = 'b';
Swap(a, b);int c = 1, d = 2;
Swap(c, d);

不是的!在函数栈帧里,要给形参开空间。这俩形参的类型不一样,自然不会是同一块空间。

不信我们看看汇编代码:

 

可见,调用的是两个不同的函数。

实际上,编译器会根据传入的实参类型来推演,把函数模板中的 T 换成相应类型,从而生成对应的函数。

如:当用double类型使用函数模板时,编译器会通过推演,将T替换成double,然后产生一份专门处理double类型的代码。

如果是int,那通过推演、替换,生成一份处理int的代码。

所以,在使用函数模板时,编译常常会慢上一点,因为它正在后台默默处理这些工作。

如图:

函数模板的实例化

什么叫”函数模板的实例化“?

是指:在调用函数模板时,根据传递的实参推导出函数模板的具体实现,生成一个特定类型的函数。

模板参数实例化分为:隐式实例化 和 显式实例化。

隐式实例化

隐式实例化,就是不指定类型,让编译器 自己去推演 模板参数的类型。

例:

template<class T>
T Add(const T& a, const T& b)return a + b;
}
int main()
{cout << Add(1, 2) << endl;      cout << Add(1.1, 2.0) << endl;  
​return 0;
}

这里补充一个点(可跳过不看):

当Add的实参是数字时,那一定要加const修饰形参。如果实参是变量,const就不是一定要加。

这样写会报错:

template<class T>
T Add( T& a,  T& b)     //不加const会报错return a + b;
}
int main()
{cout << Add(1, 2) << endl;    //当实参是数字cout << Add(1.1, 2.0) << endl;  
​return 0;
}

这是因为:编译器会做一个强校验,当实参是数字时,它本身就是不能被修改的,此时必须加const才能通过编译。

如果这样写,加const就只是锦上添花,不是必须要的:

template<class T>
T Add( T& a,  T& b)   //不加const也能通过编译
{return a + b;
}
int main()
{int a = 1, b = 2;cout << Add(a, b) << endl;  //当实参是变量
​
​return 0;
}

然而,下面这种情况却编译不通过:

 cout << Add(1.1, 2) << endl;   

 

这是因为:编译器根据实参1.1将 T推为double,根据实参2又将 T推为int,这样T就不知道自己到底是int还是double,矛盾了。

此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化

用强制转换的方式:

template<class T>
T Add(const T& a, const T& b)  
{return a + b;
}
int main()
{cout << Add(1, (int)2.1) << endl;      cout << Add((double)1, 2.1) << endl;
​return 0;
}

用显示实例化的方式:

cout << Add<int>(1, 2.1) << endl;   //指定实例化成int类型
​
cout << Add<double>(1, 2.1) << endl;   //指定实例化成double类型

显式实例化

显示实例化,就是显示地指定函数模板的实参,从而生成一个特定类型的函数。

格式:在函数名后的<>中指定模板参数的实际类型。

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

如:

int main(void)
{int a = 10;double b = 20.1;// 显式实例化Add<int>(a, b);return 0;
}

如果类型不匹配,如b是bouble类型,但实例化类型指定为int,此时 编译器会尝试进行隐式类型转换。

如果转换失败 编译器将会报错。

模板参数的匹配原则

➡️1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。

int Add(int a, int b)
{return a + b;
}
​
template<class T>
T Add(const T& a, const T& b)
{return a + b;
}
int main()
{cout << Add(1,2) << endl;   //调用非模板函数cout << Add<int>(1,2) << endl;   //调用 模板显示实例化出的函数return 0;
}

来看看怎么调用的:

❓为什么两者调用的函数不同呢?

当已经有现成的,专门处理int的函数Add存在时,Add(1,2)会优先调用现成的,这样效率更高,省去了模板实例化的时间。

而下面的Add<int>(1,2),则指定了编译器去显示实例化模板,生成int类型的Add函数。

➡️2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数。

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

例:

int Add(int a, int b)
{return a + b;
}
​
template<class T1,class T2>
T1 Add(const T1& a, const T2& b)
{return a + b;
}
int main()
{cout << Add(1,2) << endl;  cout << Add(1,2.0) << endl;   //模板会隐式实例化,使T1为int,T2为double,更匹配return 0;
}

来看看怎么调用的:

三、类模板

其实相比函数模板,后面用到 类模板的场景要更多。

为什么需要类模板

在没有类模板的时代,我们用typedef。typedef的问题有哪些呢?

typedef int STDateType;
class Stack
{
private:STDateType* _a;int _top;int _capacity;
};
int main()
{Stack s1;  //s1是int类型Stack s2;   //s2是char类型return 0;
}

如上,如果我们想要s1是int而s2是char,咋整?

解决办法:将int重命名为STDateType1,char重命名为STDateType2:

typedef int STDateType1;
typedef char STDateType2;
class Stack
{
private:STDateType1* _a;int _top;int _capacity;
};
class Stack
{
private:STDateType2* _a;  int _top;             int _capacity;
};
int main()
{Stack s1;  //s1是int类型Stack s2;   //s2是char类型return 0;
}

两段Stack代码重复度极高,可见这个办法很多余。这暴露了typedef所不能解决的问题。

这种场景下,就需要用到类模板了。

类模板

格式:

template<class T1, class T2, ..., class Tn> 
class 类模板名
{// 类内成员定义
}; 

实例化:

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可。

 // Vector类名,Vector<int>才是类型Vector<int> s1;Vector<double> s2;

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

例:

Stack<int> s1;  //s1是int类型
Stack<char> s2;   //s2是char类型

这种场景下,编译器会根据<int>、<char>分别生成对应的class Stack{};。

所以说,即使用了模板,T为 int和char时,依旧是两种不同的类,只不过不由我们手动实现,而是交给编译器去做了。

补充说明:

模板不支持分离编译,不能把声明写在.h文件,定义写在.cpp文件中。如果非要分离的话,模板是支持写在同一个文件里的。

可以把声明留在类里,定义写在类外(同一个文件里)。

用下面的例子展示下 声明与定义分离的写法:

class Stack
{
public:void Push(const T& x);  //声明写在类里
private:T* _a;  int _top;             int _capacity;
};
​
//定义在类外
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template<typename T>
void Stack<T>::Push(const T& x)
{if (_top == _capacity){……}_a[_top] = x;_top++;
}

当然,都写在类里面也是可以的。

应用类模板

回到刚刚的那种场景,我们用类模板处理一下:

template<typename T>
class Stack
{
public:Stack(int capacity = 4)  :_a(nullptr), _top(0)  , _capacity(0)   {if (capacity > 0) {_a = new T[capacity];  _capacity = capacity;_top = 0;}}
private:T* _a;          //用模板,不同的类型都可以套int _top;             int _capacity;
};
int main()
{Stack<int> s1;  //s1是int类型Stack<char> s2;   //s2是char类型return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/131537.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在Robosuite中导入自建的物体模型

Robosuite是基于mujoco封装的机器人操作库&#xff0c;而mujoco的碰撞检测是基于凸壳&#xff0c;也就是geom必须是凸的&#xff0c;如果是凹的话&#xff0c;凹的部分会有一个透明的屏障而无法与其他物体有碰撞。但是实际场景很多物体都不是凸的&#xff0c;比如杯子的口是凹进…

六招带你认识炫酷的新的室内工业艺术风格

就在设计师们以为他们已经见识过所有可能的风格和样式组合时&#xff0c;工业风出现了。这种原始与精致的开创性组合从大都市的仓库和工厂改建而来&#xff0c;对现代装饰产生了巨大的影响。 工业风格的起源 工业风格住宅和工业风格公寓的起源 由于大城市缺乏经济适用房&…

模拟实现字符串函数和内存函数

模拟实现字符串函数和内存函数 函数介绍部分模拟实现strlenstrcpy,strcat,strcmpstrncpy,strncat,strncmpstrstr,strtokstrerror 字符分类函数内存函数memcpy,memmove,memset,memcmp 求字符串长度(strlen)长度不受限制的字符串函数(strcpy,strcat,strcmp)长度受限制的字符串函数…

科学中的人工智能:量子、原子和连续体技术概述

人工智能&#xff08;AI&#xff09;的进步正在推动自然科学领域的一种新的发现范式。如今&#xff0c;AI已经开始通过改进、加速和促进我们对各种空间和时间尺度上自然现象的理解来推动自然科学的发展&#xff0c;催生了一个被称为AI for science&#xff08;AI4Science&#…

Vue中的生命周期钩子

生命周期钩子 :::warning 注意 所有生命周期钩子的 this 上下文将自动绑定至实例中&#xff0c;因此你可以利用 this 访问 props、data、computed 和 methods 等选项内的数据/函数。这意味着你不应该使用箭头函数来定义一个生命周期方法&#xff0c;因为箭头函数中没有 this&a…

牛客:小美的01串翻转

小美的01串翻转 #include<iostream> #include<cstring> #include<string> #include<vector>using namespace std; typedef long long ll; const int N 1100; string s; ll res 0;int main() {cin>>s;int n s.size();vector<vector<in…

第三方服务提权

nfs挂载原理 目标机器192.168.17.138 开启2049 nfs端口 查看目标开放的文件夹 showmount -e 192.168.17.138 回显&#xff1a;/home/peter * 说明可挂载/home/peter的所有目录 使用WinSCP链接靶机192.168.17.138 更改名字为 authorized_keys 靶机 赋值权限 攻击机 nfs挂载提…

2023最新UI工作室官网个人主页源码/背景音乐/随机壁纸/一言

2023最新UI工作室官网个人主页源码/支持背景音乐/随机壁纸/一言 功能介绍&#xff1a; 载入动画 站点简介 Hitokoto 一言 日期及时间 实时天气 时光进度条 音乐播放器 移动端适配 打开文件&#xff1b;index.html和setting.json修改替换你的相关信息&a…

Unity的GPUSkinning进一步介绍

大家好&#xff0c;我是阿赵。   在几年前&#xff0c;我曾经写过一篇介绍GPUSkinning的文章&#xff0c;这么多年之后&#xff0c;还是看到不停有朋友在翻看这篇旧文章。今天上去GitHub看了一下&#xff0c;GPUSkinning这个开源的插件已经很久没有更新过了&#xff0c;还是停…

虹科方案 | HK-NEOs系列带来先进的磁带自动化解决方案

一、HK-NEOs 系列自动磁带库 通常只有在昂贵的企业解决方案中才能找到的高级功能&#xff0c;我们的入门级磁带自动化产品就能够具备。使用 HK-NEOs 系列自动化磁带库&#xff0c;可以获得远程管理、可拆卸盒式磁带卷、可升级磁带驱动器、条形码阅读器等更多功能。 但这还不是…

JavaScript逻辑题:输出1000之内的所有完数。所谓完数指的是:如果一个数恰好等于它的所有因子之和,这个数就称为完数。

// 定义函数function judgeNum(){// 定义数组存储完数let arr []// for循环1000以内的所有数for(let i 1;i<1000;i){// 定义sum存储一个数的因子之和let sum 0;// 内层循环一个数的因子for(let j 1;j<i;j){if(i % j 0){sum j;}}// 如果一个数和它的因子之和相等&am…

FPGA-结合协议时序实现UART收发器(六):仿真模块SIM_uart_drive_TB

FPGA-结合协议时序实现UART收发器&#xff08;六&#xff09;&#xff1a;仿真模块SIM_uart_drive_TB 仿真模块SIM_uart_drive_TB&#xff0c;仿真实现。 vivado联合modelsim进行仿真。 文章目录 FPGA-结合协议时序实现UART收发器&#xff08;六&#xff09;&#xff1a;仿真模…

JavaScript逻辑题:牙膏2元 牙刷5元 牙膏盒15元 请问正好花完100元 有多少情况?

// 定义牙膏 牙刷 牙膏盒分别的价格 let toothpaste 0;let toothbrush 0;let toothpastebox 0;// 定义sum用来存储几种情况let sum 0;//第一层循环 循环牙膏买多少for (let i 0; i < 20; i){toothpaste 5 * i;// 二层循环 循环牙刷的数量for (let j 0; j < 50; j…

合伙企业是什么?

合伙企业可能大家听说也较多&#xff0c;但是到底什么是合伙企业&#xff0c;可能就没那么清楚了。看完今日的内容&#xff0c;你就会知道原来这就是合伙企业啊。 一、什么是合伙企业&#xff1f; 根据《中华人民共和国合伙企业法》&#xff0c;合伙企业是由两个或两个以上的自…

黑马JVM总结(六)

&#xff08;1&#xff09;常量池 方法区的组成中都由一个叫做运行时常量池的部分&#xff0c;内部包含一个叫做StringTable的东西 反编译二进制字节码&#xff1a; 类的基本信息&#xff1a; 常量池&#xff1a; 方法定义&#xff1a; 构造方法 main方法 &#xff1a;方法中…

手动开发-实现SpringMVC底层机制--小试牛刀

文章目录 前端控制器Controller注解RequestMapping注解自定义容器LingWebApplicationContext设计handlerList完成分发请求Service注解和AutoWired注解RequestParam注解完整代码 在这里说的底层机制的实现主要是指&#xff1a;前端控制器、Controller、Service注入容器、对象自动…

go并发处理业务

引言 实际上&#xff0c;在服务端程序开发和爬虫程序开发时&#xff0c;我们的大多数业务都是IO密集型业务&#xff0c;什么是IO密集型业务&#xff0c;通俗地说就是CPU运行时间只占整个业务执行时间的一小部分&#xff0c;而剩余的大部分时间都在等待IO操作。 IO操作包括htt…

反编译小程序详细教程,处理各种异常报错

文章目录 一、准备工作 &#xff08;一&#xff09;安装Nodejs &#xff08;二&#xff09;解密和逆向工具 二、小程序缓存文件解密 &#xff08;一&#xff09;定位小程序缓存路径 &#xff08;二&#xff09;源码解密 &#xff08;三&#xff09;源码反编译 三、小结 四、异常…

Go 异常处理

代码在执行的过程中可能因为一些逻辑上的问题而出现错误 func test1(a, b int) int {result : a / breturn result } func main() {resut : test1(10, 0)fmt.Println(resut) }panic: runtime error: integer divide by zero goroutine 1 [running]: …

华为云arm架构的linux系统中通过docker部署python环境

背景 有时候需要在无互联网的环境安装部署python环境,虽然可以在linux系统中直接安装python环境,但是比较复杂乱,特别是环境多的时候,其实可以通过docker打包安装的方式来实现 1、租用华为云arm加载的服务器 https://www.huaweicloud.com/product/ecs.html 2、安装doc…