C++ - 模板(一) #泛型编程 #函数模板 #类模板

article/2025/7/5 8:29:47

文章目录

前言

一、泛型编程

二、函数模板

1、函数模板的概念

2、函数模板的格式

3、函数模板的原理

4、函数模板的实例化

1、隐式实例化:

2、显式实例化:

5、模板参数的匹配原则

三、类模板

1、类模板的定义格式

2、类模板的实例化

总结


前言

路漫漫其修远兮,吾将上下而求索;


一、泛型编程

何为泛型编程

  • 针对广泛的类型去写代码;即编写与类型无关的通用代码,是代码复用的一种手段;

Q:如何实现一个通用的交换函数呢?

代码如下:

void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}void Swap(double& left, double& right)
{double temp = left;left = right;right = temp;
}void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}//还可以继续重载其他参数类型的函数...

上述代码是用函数重载实现的,这些函数的执行逻辑相同,仅要使用的数据的类型不同,就需要额外再写函数

用函数重载实现的几个缺点:

  • 1、重载的函数仅仅是参数的类型不同,代码复用率比较低,只有新类型出现的时,就需要用户自己增加对应的函数;
  • 2、代码的可维护性比较低,一个出错可能所有的重载均会出错;

那么此时我们就会想,能否直接给编译器一个“模板”,然后编译器会通过不同的类型利用该“模板”生成函数呢;就像做月饼一样,我们可以用月饼模具做出许多不同馅料的月饼出来;

于是乎,C++就提供了一种机制:模板

模板是泛型编程的基础;

二、函数模板

1、函数模板的概念

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

2、函数模板的格式

template<typename T1,typename T2,......,typename Tn>

返回类型   函数名(参数列表){} 

新增加了两个关键字:template (模板)、 typename(类型名称);

注意: typename 是用来定义模板参数关键字,也可以使用class(切记:不能用struct 来代替class);即函数模板的格式还可以这么写:

template<classT1,classT2,......,classTn>

返回类型   函数名(参数列表){} 

将上面用函数重载实现的交换的代码写成函数模板,代码如下:

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

注:模板参数可以类比我们之前学习的函数参数;

  • 对于模板参数来说,<> 中定义的是类型,如果有多个类型,其间需要用 , 进行分隔;模板参数:关键字template<typename 类型名称,...>
  • 而对于函数参数,() 中定义的是参数对象、参数变量;可能会有多个参数,参数之间用 , 进行分隔。函数参数: 函数返回值类型 函数名(类型 变量名, ... )

使用该函数模板,代码如下:

//函数模板 --> 泛型
template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}int main()
{int x = 1, y = 2;Swap(x, y);cout << "x:" << x << " " << "y:" << y << endl;double m = 1.1, n = 2.2;Swap(m, n);cout << "m:" << m << " " << "n:" << n << endl;return 0;
}

运行结果如下:

Q:模板的原理是什么?“模具”

调用的是编译器根据这个模板参数的匹配取去推演出生成T为int、double的Swap 函数(这是两个函数);也就是意味着,写成了模板,将工作交给了编译器,便利了我们去写代码;此处的底层实际上还是两个函数;

我们可以观察一下反汇编:

模板相当于是活字印刷的模具,而干活的人是编译器;模板的原理:给一个模板,编译器帮你去生成对应的函数;

3、函数模板的原理

编译器根据模板来生成对应的函数:

  • 步骤一:推演:根据所传的实参来推演形参的类型
  • 步骤二:模板实例化:编译器生成对应的函数(模板实例化是通过函数模板去实例化生成对应具体的函数)

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生具体类型函数的模具,所以其实模板就是将本来应该我们做的重复的事情交给了编译器

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

注意: typename 是用来定义模板参数关键字,也可以使用class(切记:不能用struct 来代替class);

4、函数模板的实例化

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

1、隐式实例化:

编译器根据实参推演模板参数的实际类型

代码如下:

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);Add(d1, d2);return 0;
}

Add(a1,d1);是否可以调用成功?

该语句不能通过编译,因为在编译期间,当编译器要实例化的时候,需要推演其实参类型;通过实参a1 会将T推演为int ,但是通过实参 d1 又将T推演为 double 类型,但是模板参数列表中只有一个T,编译器无法确定此处到底应该将T确定为int 还是 double,于是就报错了

需要注意的是,在模板中,编译器一般不会进行类型转换操作,因为一旦转换出了问题,编译器就需要背黑锅;

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

强制转换:Add((double)a1, d1);

2、显式实例化:

在函数名后面的<> 中指定模板参数的实际类型

使用代码如下:

int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;//Add((double)a1, d1);//强制类型转换Add<double>(a1, d1);//显式实例化return 0;
}

如果实参的类型不匹配,我们可以在函数名后的<> 中指定模板参数的实际类型,这就是显式实例化;由我们直接决定模板所要实例化这个函数的形参类型;

所以上述问题有两种解决方案:

解决方案一:强制类型转换

解决方案二:显式实例化

在方案一中,强制类型转换实参的类型,然后编译器根据实参的类型进行推演本质:推演实例化;

显式实例化需要在调用函数名和实参之间添加一个 <> ,并在其中指定类型;

Q:显式实例化还有没有其他的应用场景?

  • 有些特殊的函数(eg. 该函数形参不使用模板类型),编译器无法通过其参数推演的时候就要使用显式实例化;

代码如下:

template<class T>
T* func(size_t n)
{return new T[n];
}int main()
{func(10);//编译器无法通过实参推演并实例化return 0;
}

运行结果如下:

不能这样用的原因:实参传给形参,编译器会根据实参来推演模板类型,但是此处函数func 的形参是size_t 类型的,并没有使用模板类型,所以编译器无法进行推演而隐式实例化但是我们可以直接告诉编译器此处的模板类型是什么,即让编译器显式实例化该函数;

显式实例化:

template<class T>
T* func(size_t n)
{return new T[n];
}int main()
{func<int>(10);//显式实例化return 0;
}

5、模板参数的匹配原则

1、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以实例化为这个非模板函数;并且”有现成吃现成“

代码如下:

//专门处理int 类型数据的加法函数
int Add(int left, int right)
{return left + right;
}//通用的加法函数
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.2;//推演实例化cout << Add(a1, a2) << endl;cout << Add(d1, d2) << endl;return 0;
}

Q:为什么同名的函数模板可以与普通函数同时存在?

  • 因为C++中支持函数重载

编译器的原则:有“现成”就是用“现成”的,没有“现成”就自己使用函数模板去实例化;当存在同名的非模板函数与模板函数,调用的时候如果参数类型与非模板函数向匹配就会优先使用非模板函数,不匹配的时候编译器才会根据函数模板进行实例化;

模板的本质就是为了方便程序员,传不同类型的参数就可以实现相应的逻辑,而底层实际上还是在调用不同的函数;

而如果我们使用显式实例化,就意味着强制性地让编译器去使用函数模板实例化,代码如下:

//专门处理int 类型数据的加法函数
int Add(int left, int right)
{return left + right;
}//通用的加法函数
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.2;//推演实例化cout << Add(a1, a2) << endl;//显式实例化,强制编译器去使用函数模板去实例化cout << Add<int>(a1, a2) << endl;cout << Add(d1, d2) << endl;return 0;
}

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;
}int main()
{//与非模板函数类型完全匹配Add(1, 2);//模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数Add(1, 2.0);return 0;
}

但是倘若此时只有一个不太匹配的“现成”,编译器也会选择隐式类型转换强制性地走;

3、模板函数不允许自动类型转换,但是普通函数可以进行自动类型转换;

小结:

在参数匹配的情况下,编译器会“有现成吃现成”;但如果参数不匹配并且此时有现成的不匹配(不匹配的非函数模板),以及匹配的“自己做”(匹配的函数模板),编译器会去调用更加匹配的;

三、类模板

之前用C++实现的Stack 之中,有typedef int STDateType;不能将typedef 理解为泛型,因为typedef 存在巨大的缺陷,无法让实例化出来的对象一个存放int类型的数据,一个存放double 类型的数据...即typedef 只能允许一个类型存在,不能同时多个类型的数据同时使用;

如果我们非要利用typedef 实现也是可以的,这样的话,我们就需要写成多个类来实现不同类型数据的存放,eg.StackInt、StackDouble...... 如果还有其他类型,需要拷贝主逻辑针对该类型重新实现一个类出来;这样做的话,单单就只有数据的类型不同,这些类的主逻辑大体都是相似,而又大幅度地增加了代码量,不利于维护;基于这样的问题,C++中还提供了类模板

注:也正是因为C语言的语法无法支持“模板”这一概念以同时满足不同类型数据的需求,所以C语言库中不实现数据结构;

类模板与函数模板的区别:

  • 函数模板定义的参数只能给该函数使用,而类模板定义的参数是给整个类使用的;

1、类模板的定义格式

template<class T1,class T2,... ,class Tn>

class 类模板名

{

//类模板成员
};

也可以是:

template<typename T1,typename T2,... ,typename Tn>

class 类模板名

{

//类模板成员
};

使用代码如下:

//类模板
template<typename T>
class Stack
{
public:Stack(int n = 4){_a = (T*)malloc(sizeof(T) * n);if (nullptr == _a){perror("malloc 申请空间失败");return;}_capacity = n;_top = 0;}
private:T* _a;size_t _capacity;size_t _top;
};int main()
{//必须显式实例化Stack<int> st1;Stack<double> st2;return 0;
}

需要注意的是,由于构造函数的形参不一定涉及T ,但构造函数之中又会使用T ,所以类模板的特点是必须显式实例化

上例中的st1 与 st2 使用的并不是同一个类,是编译器根据同一个类模板实例化出来的不同的类;

2、类模板的实例化

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

	//Stack 是类名,Stack<int>、Stack<double> 才是类型Stack<int> st1;//intStack<double> st2;//double

接下来,我们还可以与根据类模板来优化之前我们所写的栈;(优化其中的小细节:引用传参、const 修饰、引用返回)

//类模板
template<typename T>
class Stack
{
public:Stack(int n = 4){//_a = (T*)malloc(sizeof(T) * n);//if (nullptr == _a)//{//	perror("malloc 申请空间失败");//	return;//}//使用new 开辟空间_a = new T[n];_capacity = n;_top = 0;}void Push(const T& x)//引用传参、const修饰{//扩容判断if (_top == _capacity){//走realloc 的逻辑int newcapacity = _capacity * 2;//异地扩容- 开辟新空间T* tmp = new T[newcapacity];memcpy(tmp, _a, sizeof(T) * _top);//拷贝数据delete[] _a;//释放旧空间_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}void Pop(){assert(_top);_a[_top - 1] = -1;_top--;}const T& Top()//获取栈顶的数据,也需要用const 修饰{return _a[_top - 1];}~Stack(){delete[] _a;}private:T* _a;size_t _capacity;size_t _top;
};

需要注意的是,在C++之中并没有renew ,所以扩容不能使用realloc ,需要使用new 、memcpy、delete 去模拟realloc 异地扩容的功能来满足扩容需求;在外部捕获异常即可

对于获取栈顶的数据,初衷并不期望修改栈中的数据,所以Top的返回值为const 引用;

模板类中的成员函数声明与定义分离,分离的定义需要加上类模板:

需要注意的是,指定类域需要加上模板参数 T,而T的来源需要说清楚,所以要加上类模板;

如果写成:

如果存在多个类型的类,就会分不清楚这个Push 究竟是给谁用的了,并且在函数参数、函数体中也有可能会使用到模板参数T;所以在定义该函数的时候,需要声明其模板参数,其次是指定类域需要加上模板参数;修改如下:

其中还需要注意的是,类模板中成员函数不建议声明与定义分离到两个文件之中,否则会出现链接错误(与模板原理相冲突),原因此处暂不讲后续会有所提及;所以,类模板中成员函数的声明与定义一般是放在一个文件之中

接下来,我们看一些stl_list.h 中的源码:

之所以stl源码中只有.h 文件而没有 .cpp 文件,就是因为类模板中成员函数的声明与定义不能分离到两个文件之中;短小的成员函数直接就在类中实现了,在类中实现的函数默认为内联函数,较长的成员函数便会在类中声明,类外定义;


总结

1、typename 是用来定义模板参数关键字,也可以使用class(切记:不能用struct 来代替class);

template<typename T1,typename T2,......,typename Tn>

返回类型   函数名(参数列表){} 

template<classT1,classT2,......,classTn>

返回类型   函数名(参数列表){} 

2、编译器根据模板来生成对应的函数:

  • 步骤一:推演:根据所传的实参来推演形参的类型
  • 步骤二:模板实例化:编译器生成对应的函数(模板实例化是通过函数模板去实例化生成对应具体的函数)

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

4、类模板定义格式:

template<class T1,class T2,... ,class Tn>

class 类模板名

{

//类模板成员
};

也可以是:

template<typename T1,typename T2,... ,typename Tn>

class 类模板名

{

//类模板成员
};


http://www.hkcw.cn/article/PHkPxvptcD.shtml

相关文章

智能制造全场景数字化解决方案

制造企业数字化转型面临的挑战 数智化转型已成为中国制造业高质量发展的关键战略。面对全球制造业格局调整&#xff0c;如何快速构建覆盖全业务流程的可视化应用&#xff0c;通过数据驱动的方式为企业经营管理、预警监测、质量管控、决策支持提供全面支撑&#xff0c;是企业面…

Vue-收集表单信息

收集表单信息 Input label for 和 input id 关联, 点击账号标签 也能聚焦 input 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><title>表单数据</title><!-- 引入Vue --><scrip…

篮球分组问题讨论

1 问题概述 问题&#xff1a;有5支球队在同一块场地上进行单循环赛,共要进行10场比赛。下表是一个赛程安排&#xff0c;有些队觉得不公平。研究以下问题 A B C D E 每两场比赛间相隔场次数 A X 1 9 3 6 1, 2, 2 B 1 X 2 5 8 0, 2, 2 C 9 2 X 7 10 4…

成都鼎讯--通信干扰设备功能全解析

在现代电子战与通信对抗领域&#xff0c;一款高性能的通信干扰设备是掌握电磁频谱主动权的关键。本文将深入解析一款先进的通信干扰设备&#xff0c;其凭借多频段覆盖、多通道并行、多样化调制方式及灵活供电等特性&#xff0c;成为部队、科研院所等机构在电磁对抗训练与研究中…

vscode中让文件夹一直保持展开不折叠

vscode中让文件夹一直保持展开不折叠 问题 很多小伙伴使用vscode发现空文件夹会折叠显示, 让人看起来非常难受, 如下图 解决办法 首先打开设置->setting, 搜索compact Folders, 去掉勾选即可, 如下图所示 效果如下 看起来非常爽 ! ! !

中国城市间地理距离矩阵(2024)

1825 中国城市间地理距离矩阵(2024) 数据简介 中国城市间地理距离矩阵数据集&#xff0c;通过审图号GS(2024)0650的中国城市地图在Albers投影坐标系中进行计算得出矩阵表格&#xff0c;单位为KM&#xff0c;方便大家研究使用。 中国城市地理距离矩阵数据通过计算城市中心距离…

Linux中的shell脚本

什么是shell脚本 shell脚本是文本的一种shell脚本是可以运行的文本shell脚本的内容是由逻辑和数据组成shell脚本是解释型语言 用file命令可以查看文件是否是一个脚本文件 file filename 脚本书写规范 注释 单行注释 使用#号来进行单行注释 多行注释 使用 : " 注释内容…

20250530-C#知识:抽象类、抽象方法、接口

C#知识&#xff1a;抽象类、抽象方法、接口 在开发过程中接口一般用得较多&#xff0c;程序框架往往定义一堆接口规范&#xff0c;然后程序员自己写逻辑来实现接口功能。掌握接口的知识还是很有必要的。 1、抽象类 用abstract关键字修饰的类不能用来实例化对象可以包含抽象方法…

韩国首尔一地铁车厢内遭纵火 乘客被紧急疏散

当地时间5月31日8时47分左右,韩国首尔地铁5号线一辆列车车厢内起火,乘客随后被紧急疏散。据初步调查,火灾原因为有人纵火,嫌疑人已被抓获。目前暂无人员伤亡报告。受火灾事件影响,该地铁线路部分区段一度暂停运行,首尔市交通部门10时13分通报,事故处理已经完毕,暂停运行…

跨平台浏览器集成库JxBrowser 支持 Chrome 扩展程序,高效赋能 Java 桌面应用

JxBrowser 是 TeamDev 开发的跨平台库&#xff0c;用于在 Java 应用程序中集成 Chromium 浏览器。它支持 HTML5、CSS3、JavaScript 等&#xff0c;具备硬件加速渲染、双向 Java 与 JavaScript 连接、丰富的事件监听等功能&#xff0c;能处理网页保存、打印等操作&#xff0c;助…

聊聊网络变压器的浪涌等级标准是怎样划分的呢?

Hqst盈盛&#xff08;华强盛&#xff09;电子导读&#xff1a;聊聊网络变压器的浪涌等级标准是怎样划分的呢&#xff1f; 在和做防雷产品的客户的深度沟通网络变压器产品选型中发现&#xff1a;客户对网络变压器的浪涌等级划分也很希望有更深的了解&#xff0c;今天就这个问题和…

探索Air780EPM:N种GPIO控制LED的创新应用!

通过创新思维与实用技巧&#xff0c;本文将带你了解Air780EPM如何通过GPIO实现LED控制的N种可能&#xff0c;从简单到复杂&#xff0c;激发项目灵感。 一、GPIO直接驱动LED 1.1 适用场景 低功耗场景&#xff1a;LED电流 ≤ 5mA&#xff08;普通GPIO的驱动能力限制&#xff09;…

JS 事件循环详解

JS 事件循环详解 文章目录 JS 事件循环详解一、JS 的单线程模型与异步机制二、事件循环的核心组件1. 执行栈&#xff08;Call Stack&#xff09;2. 任务队列&#xff08;Task Queue&#xff09;3. Web APIs 三、事件循环的执行流程四、任务类型详解1. 宏任务&#xff08;Macrot…

堆遇到的stl与理论基础

目录 二叉完全搜索树是堆吗:并不是,堆比两孩子都大 1. 二叉完全搜索树的特点 2. 堆的特点 3. 两者的主要区别 4. 结论 c有swap吗 堆的向上调整和向下调整是什么 1. 堆的定义 2. 向上调整&#xff08;Heapify Up&#xff09; 操作步骤 示例&#xff08;最大堆&#x…

年度工作汇报工作总结PPT模版分享

年度工作汇报工作总结PPT模版分享&#xff1a;工作总结汇报类PPT模版https://pan.quark.cn/s/774660cc70e8

一文学会c++中的内存管理知识点

文章目录 c/c内存管理c语言动态内存管理c动态内存管理new/delete自定义类型妙用operator new和operator delete malloc/new&#xff0c;free/delete区别 c/c内存管理 int globalVar 1;static int staticGlobalVar 1;void Test(){static int staticVar 1;int localVar 1;in…

ZC-OFDM雷达通信一体化减小PAPR——直接限幅法

文章目录 前言一、直接限幅法技术1、简介2、原理 二、MATLAB 仿真1、核心代码2、仿真结果 三、资源自取 前言 在 OFDM 雷达通信一体化系统中&#xff0c;信号的传输由多个子载波协同完成&#xff0c;多个载波信号相互叠加形成最终的发射信号。此叠加过程可能导致信号峰值显著高…

【二维数组】

二维数组 需要掌握的知识二维数组与内存二维数组语法Arrays类的常用方法介绍如何实现冒泡排序 需要掌握的知识 二维数组与内存 二维数组语法 //数据类型【】【】数组; //或者 //数据类型 数组名【】【】&#xff1b; //二维数组初始化操作 int [][] scorenew int[][]{{90,85,92…

小黑大语言模型通过设计demo进行应用探索:langchain中chain的简单理解demo

chain简介 LangChain 中的 Chain 模块‌在开发大型语言模型&#xff08;LLM&#xff09;驱动的应用程序中起着至关重要的作用。Chain是串联LLM能力与实际业务的关键桥梁&#xff0c;通过将多个工具和模块按逻辑串联起来&#xff0c;实现复杂任务的多步骤流程编排。 案例 通过…

职坐标精选嵌入式AI物联网开源项目

随着嵌入式、AI与物联网技术的深度融合&#xff0c;开源生态已成为开发者构建智能硬件解决方案的核心驱动力。本文将从嵌入式实时操作系统、多模态AI数据集及物联网接入平台三大维度切入&#xff0c;系统性梳理技术选型要点与实践路径。在嵌入式领域&#xff0c;重点解析低功耗…