C++模板(函数模板、类模板)

news2024/11/18 8:22:53

目录

一、泛型编程

二、函数模板

函数模板概念

函数模板格式 

函数模板的原理 

函数模板的实例化 

模板参数的匹配原则

三、类模板 

类模板的定义格式

类模板的实例化 

四、扩展 

函数模板一定是推演?类模板一定是指定?

模板的分离编译 


一、泛型编程

泛型编程:不再针对某种类型,能适应广泛的类型

  • 我们看如下的交换函数:
//交换int类型
void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}
//利用C++支持的函数重载交换double类型
void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}

使用函数重载虽然可以实现不同类型的交换函数,但是也有以下几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数,使得代码重复性高,过渡冗余。
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错。

那我们能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

在C++中也能够存在这样一个模具,通过给这个模板中传递不同类型,来获得不同具体类型的代码。

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

  • 函数模板
  • 类模板

二、函数模板

函数模板概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。


函数模板格式 

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{
    //……
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)

因此,交换函数我们就可以套用以下模板

template<typename T>//或者template<class T>
void Swap(T& left, T& right)
{
	T tmp = left;
	left = right;
	right = tmp;
}

调用情况如下:


函数模板的原理 

问:我们上述的Swap调用的是同一个函数吗?

  • 答:不是,这里我们三次Swap不是调用的同一个函数,当Swap时编译器会根据不同的类型通过模板定制专属的函数,然后再调用,我们可以用反汇编来验证这一结论:

函数模板只是一个蓝图,它本身并不是函数。模板只是将本来我们做的重复的事情交给了编译器。

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

  • 补充:

其实在库里面就有swap函数,因此我们就不需要自己写模板了:

我们直接用swap即可: 

int main()
{
	int a = 1, b = 2;
	double c = 1.1, d = 2.2;
	swap(a, b);
	swap(c, d);
}

函数模板的实例化 

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化显式实例化。

  • 隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2); //编译器推出T是int
	Add(d1, d2); //编译器推出T是double
}

但是我们如下调用就会出错:

int main()
{
    int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, d1); //err 编译器推不出来
	/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
    */
}

编译器无法确定这里的T是int还是double。此时有两种处理方式:

法一:用户自己来强制转化

int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    Add(a1, (int)d1); //强制类型转换。或者Add((double)a1, d1);
}

法二:使用显式实例化

  • 显式实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    //显示实例化
	Add<int>(a1, d1); //double隐式类型转换成int 
	Add<double>(a1, d2); 
}
  • 补充:模板支持多个模板参数。
template<class K, class V> //两个模板参数
void Func(const K& key, const V& value)
{
	cout << key << ":" << value << endl;
}
int main()
{
	Func(1, 1); //K和V均int
	Func(1, 1.1);//K是int,V是double
	Func<int, char>(1, 'A'); //多个模板参数也可指定显示实例化不同类型
}

我们还可以给模板参数附上缺省值,和函数里的缺省参数一样需要从右往左给缺省值

同时我们也可以全缺省。 


模板参数的匹配原则

  • 原则1: 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
//通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
	Add(1, 2); //会调用哪个Add函数?
}

这两个Add可以同时存在,那我们调用Add的时候调的是模板函数Add还是专属的Add?

 

通过反汇编我们知道调用的是专属Add函数。我们得出结论:编译器在调用时,有现成的就调用现成的,没有就调用模板当然,我们也可以强制让编译器走模板函数,如下:

void Test()
{
    Add(1, 2); // 与非模板函数匹配,编译器不需要特化
    Add<int>(1, 2); // 调用编译器特化的Add版本
}
  • 原则2:对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
  • 原则3:模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

三、类模板 

类模板的定义格式

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

我们拿下面的栈为例:

template<class T>
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = new T[capacity];
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		delete[]_a;
		_capacity = _top = 0;
	}
private:
	T* _a;
	int _top;
	int _capacity;
};

类模板的实例化 

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后面跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

int main()
{
	Stack<int>st1; //int类型
	Stack<double>st2;//double类型
}

四、扩展 

函数模板一定是推演?类模板一定是指定?

函数不一定都能推演,但是类模板一定要指定,假设我们有如下的函数模板:

 这里的模板就推不出T的类型,因此我们就要对其显示实例化。

template<class T>
T* func(int n)
{ 
	return new T[n];
}
int main()
{
	//函数模板显示实例化
	int* p1 = func<int>(10);
	double* p2 = func<double>(10);
}

因此如果函数模板不能自动推演,就要显示实例化,指定模板参数。


模板的分离编译 

模板的声明和定义是可以分离的(前提是声明和定义在一个文件中)。比如下面情况:

//函数模板的声明
template<typename T>
void Swap(T& left, T& right);
//类模板的声明
template<class T>
class Vector
{
public:
	Vector(size_t capacity = 10);
private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};
 
//函数模板的定义
template<typename T> //定义的时候也要给模板参数
void Swap(T& left, T& right)
{
	T tmp = left;
	left = right;
	right = tmp;
}
//类模板的定义
template<class T> //定义的时候也要给模板参数
Vector<T>::Vector(size_t capacity)
	: _pData(new T[capacity])
	, _size(0)
	, _capacity(capacity)
{}

模板不支持声明和定义放在两个文件中(xxx.h和xxx.cpp),会发生链接错误。那这是为什么呢?

 我们都知道源文件在生成可执行程序的过程会经历预处理、编译、汇编、链接四大模块。

template.i在编译后生成对应的.s文件以及后续的.o文件其实都为空,编译器无法下手,因为编译器不知道T是什么,这也就导致了符号表是空的,没有地址。而调用的地方就没有问题,因为main函数里面已经实例化显示出了T的类型。随后就去符号表里找对应函数的地址,但是找不到。就会导致链接出错。

那如果我非要将声明和定义放到两个文件里面,有什么办法呢?

  • 解决方法1:在template.cpp中针对要使用的模板类型显示实例化指定

你调用函数的地方有那些类型,就要指定显示实例化那些类型。加上了显示实例化后此时就能够链接上了。不过这种方法不怎么实用,不推荐使用。

  • 解决方法2:将声明和定义放在一个文件"xxx.hpp"里面或者"xxx.h"里面。比较推荐使用此种方法。

模板的声明和定义一般是要放到一个文件中,有些地方就会命名成xxx.hpp,寓意就是头文件和定义实现内容合并一起,但并不一定是.hpp.h也是可以的。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL高级【行级锁】

1&#xff1a;行级锁1.1&#xff1a;介绍行级锁&#xff0c;每次操作锁住对应的行数据。锁定粒度最小&#xff0c;发生锁冲突的概率最低&#xff0c;并发度最高。应用在 InnoDB存储引擎中。 InnoDB的数据是基于索引组织的&#xff0c;行锁是通过对索引上的索引项加锁来实现的&a…

WPF中Binding数据校验、并捕获异常信息的三种方式

Binding数据校验、并捕获异常信息的三种方式 WPF在使用Binding时&#xff0c;经常需要进行数据校验&#xff0c;如果校验失败需要捕获失败的原因&#xff0c;并加以展示&#xff0c;本文主要介绍数据校验异常并捕获的三种方式。 依赖属性异常捕获 先定义一个依赖属性 publi…

【Nacos】Nacos配置中心的使用与SpringCloud整合

在微服务架构中&#xff0c;当系统从一个单体应用&#xff0c;被拆分成分布式系统上一个个服务节点后&#xff0c;配置文件也必须跟着迁移&#xff08;分割&#xff09;&#xff0c;这样配置就分散了&#xff0c;不仅如此&#xff0c;分散中还包含着冗余。配置中心将配置从各应…

哪儿有微服务开源项目?

随着数字化时代的到来&#xff0c;微服务开源项目的应用价值逐渐凸显。作为提升企业办公协作效率的低代码开发平台项目&#xff0c;其表现出来的灵活性、易操作、简便的特性&#xff0c;成为现代化办公管理中的重要合作伙伴。我们今天一起来了解什么是微服务开源项目。 一、微服…

基于JavaWeb实现蜀南调味品商城物流配货系统

一、项目介绍 本文系统利用JavaWeb技术&#xff0c;设计和实现了连接公司、客户公司、物流运输为桥梁的销售配送管理系统&#xff0c;并以网络技术和信息技术在销售配送中的应用为重点&#xff0c;实现员工登录模块、员工信息管理模块、库存管理模块、订单处理模块、包装管理模…

高通Wi-Fi 7网络芯片方案IPQ9574,IPQ9554,IPQ9514,IPQ9570,IPQ9550,IPQ9510

networking pro 1620&#xff1a;芯片型号IPQ9574&#xff0c;支持4频段16路数据流&#xff0c;峰值速率33Gbps&#xff0c;支持4个2.5G口&#xff0c;1个5G口&#xff0c;1个万兆口&#xff1b;networking pro 1220&#xff1a;芯片型号IPQ9574&#xff0c;支持3频段12路数据流…

三个案例详解不同网段之间如何互通

当然还可以通过三层交换机划分VLAN配置更好。这里主要讲通过普通路由器之间互通一台路由器连接另外一台路由器&#xff0c;这两台路由器分别连接不同的网段&#xff0c;那么如果要这两个网段互通&#xff0c;则必须配置路由&#xff0c;这个就是静态路由。案例一、不同网段之间…

【练习】Day06

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、选择二、编程最小时间差答案1. 选择2. 编程普通小孩也要热爱生活&#xff01; 一、选择 散列技术中的冲突是指&#xff08; &#xff09; A. 两个元素具有相同的序号 B. 两个元素的键值不同&#xff0c;而其他…

Linux权限理解

✅<1>主页&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;C ☂️<3>开发环境&#xff1a;Visual Studio 2022 &#x1f4ac;<4>前言&#xff1a;linux当中对于权限的理解。 &#x1f490;一.生活中的权限 &#x1f338;二.Linux权限…

一文让你弄懂多租户数据库设计⽅案

文章目录前言一、设计方案二、方案剖析三、方案总结四、方案选型五、引申问题的解决方案六、写在最后前言 多租户是SaaS&#xff08;Software-as-a-Service&#xff09;下的一个概念&#xff0c;意思为软件即服务&#xff0c;即通过网络提供软件服务。 SaaS平台供应商将应用软…

微软的AD登录loginRedirect

我这边技术栈是reactts 如果你是vue&#xff0c;直接将tsx文件改成jsx就可以或者不该也没问题 上篇文章介绍了msal 的弹框登录&#xff0c;先介绍下重定向登录这个相对弹框登录要烦很多。。。中国内网看我查询的资料很少&#xff0c;只有微软系的公司才会有相对应的需求。此处自…

代码随想录算法训练营第二天|977.有序数组的平方 |209.长度最小的子数组 |59.螺旋矩阵II

977 有序数组平方 看完题后的思路 双指针 思路 本题如果使用暴力解法,需要按照绝对值将数组排序0(logn),然后进行平方.(或先平方,再排序,这样可以直接调用排序函数) 可以使用双指针法,定义两个指针,左指针是当前绝对值最小的负数,右指针是当前绝对值最小的整数,每一轮将较小…

03、openscenegraph(简称osg)源代码编译

通过上一节&#xff0c;我们准备下载好了osg源代码和依赖库&#xff0c;并安装了CMake、VS2013开发环境&#xff0c;接下来就可以进入编译工作了。 首先&#xff0c;将下载的openscenegraph源代码和依赖库3rdParty_VS2012.3_v110_x86_x64_V8b_full解压到同一个目录下&#xff…

构建系列之新一代利器Esbuild(下)

前言 本篇文章接上文&#xff0c;通过尝试使用esbuild的能力和业界的落地方案作为切入点继续深入esbuild的原理。 尝试Esbuild ESBuild在API层面上非常简洁, 主要的API只有两个: Transform和Build, 这两个API可以通过CLI, JavaScript, Go的方式调用 Transform主要用于对源代…

致而立之年的自己

&#xff08;点击即可收听&#xff09;时间是连续性的,人越长大,越能体会到,所谓的跨年与过年,其实是没有多大意义的但只要是人,就需要制造一些仪式感,弄出一些特殊的节日,用于安慰自己,对于逝去的曾今做一个告别,制造些记忆点然而,记忆这东西,是很容易健忘的,就像昨天,前天,吃…

泰克新2系示波器在微电网测试上的应用

直流微电网是由直流构成的微电网&#xff0c;是未来智能配用电系统的重要组成部分&#xff0c;对推进节能减排和实现能源可持续发展具有重要意义。相比交流微电网&#xff0c;直流微电网可更高效可靠地接纳风、直流逆变等分布式可再生能源发电系统、储能单元、电动汽车及其他直…

【C语言进阶】一万字教你实现简易通讯录管理

目录一. 通讯录要实现的功能1.项目文件分配2.通讯录基本功能二.test.c的实现1.逻辑代码的实现2.通讯录联系人信息的创建三.contacts.c的实现1.初始化通讯录2.添加联系人信息3.显示通讯录信息3.删除指定联系人信息4.查找指定联系人5.修改指定联系人信息6.以名字年龄排序通讯录7.…

进程收尾,初识线程

PCB中的信息:常见属性(有关于进程调度) 1)PID:(进程id)是进程中的身份标识&#xff0c;一个机器同一时刻&#xff0c;不可能有两个进程的PID相同&#xff0c;同一个系统的身份标识&#xff0c;进程的身份证号 2)一组内存指针&#xff1a;指名了该进程持有的一些重要数据和要执行…

一文讲解Linux虚拟化KVM-Qemu分析之virtio初探

说明&#xff1a; KVM版本&#xff1a;5.9.1QEMU版本&#xff1a;5.0.0工具&#xff1a;Source Insight 3.5&#xff0c; Visio 概述 从本文开始将研究一下virtio&#xff1b;本文会从一个网卡虚拟化的例子来引入virtio&#xff0c;并从大体架构上进行介绍&#xff0c;有个宏…

惊奇地发现:「精通或熟练使用 Navicat 」成为了业内企业招聘的任职要求之一

2023 年疫情逐渐消散&#xff0c;招聘市场也火热了起来。我们在招聘平台惊奇地发现&#xff1a;许多岗位招聘启事&#xff08;如&#xff1a;前端开发工程师、业务系统开发工程师和高级数据分析师等&#xff09;中提到&#xff0c;具备熟练使用 Navicat 软件成为任职要求之一。…