【C++】从零开始认识泛型编程 — 模版

在这里插入图片描述

送给大家一句话:
尽管眼下十分艰难,可日后这段经历说不定就会开花结果。总有一天我们都会成为别人的回忆,所以尽力让它美好吧。 – 岩井俊二

\\\ ⱶ˝୧(๑ ⁼̴̀ᐜ⁼̴́๑)૭兯 ////
\\\ ⱶ˝୧(๑ ⁼̴̀ᐜ⁼̴́๑)૭兯 ////
\\\ ⱶ˝୧(๑ ⁼̴̀ᐜ⁼̴́๑)૭兯 ////


从零开始认识模版

  • 1 前言
  • 2 函数模板
    • 什么是函数模版
    • 如何使用函数模版
    • 调用规则
  • 2 类模板
    • 什么是类模版
    • 如何使用类模版
  • 4 特别注意
    • 4.1 非类型模板参数
    • 4.2 模版缺省值
    • 4.3 编译细节
    • 4.4 模版特化
    • 4.5 模版的分离编译
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 前言

泛型编程是C++中十分关键的一环,泛型编程是C++编程中的一项强大功能,它通过模板提供了类型无关的代码,使得C++程序可以更加灵活和高效,极大的简便了我们编写代码的工作量。

泛型编程作为一种编程范式的主要优点包括:

  • 代码复用:同一个算法或数据结构可以用于不同的数据类型,提高了代码的复用性。
  • 性能:由于在编译时就已经知道具体的数据类型,因此编译器可以生成针对该类型的优化代码。
  • 类型安全:泛型编程仍然可以进行类型检查,从而减少运行时错误。

泛型编程它允许开发者编写独立于数据类型的算法和函数。在C++中,泛型编程主要通过模板(Templates)来实现。模板允许编写代码时使用抽象的数据类型,这些数据类型在编译时会被具体的类型所替换。

我们来看一个简单的例子:假如我想要编写一个求和函数,那么传统的写法是:

//光是简单的这三种常见类型的自身我们都需要写许多代码!
int sum(int a, int b)
{return a + b;
}
float sum(float a, float b)
{return a + b;
}
double sum(double a, double b)
{return a + b;
}

而通过使用模版就可以极大的简便我们的过程:

template<class A >
A sum(A a, A b)
{return a + b;
}

使用一个函数就可以实现多种类型的求和,极大的提高了代码的复用率!下面我们就来学习模版!!!

C++中的模板分为两类:函数模板(Function Templates)和 类模板(Class Templates);

2 函数模板

什么是函数模版

函数模板(Function Templates):允许定义一个函数,它可以接受任何类型的参数。编译器会根据传递给函数的实际参数类型来实例化函数的特定版本。
上面的函数就是使用的函数模版。

template<class A >
A sum(A a, A b)
{return a + b;
}

在这个例子中,sum 函数可以接受任何类型的参数(包括自定义类型),只要该类型支持比较操作。

如何使用函数模版

函数模版的格式是:

//需要几个模版就使用几个
template<typename T1, typename T2,......,typename Tn>
//写入对应函数即可

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

函数模板是C++中的一项强大特性,它本质上并非一个具体的函数实体,而更像是编译器生成具体类型函数的蓝图。当我们定义一个函数模板时,我们实际上是在描述一个能够处理多种数据类型的算法框架。编译器会根据这个框架,在程序中使用模板的具体实例时,自动生成对应的具体类型函数。只有使用了才会生成实例化函数哦!!!!

这样的设计理念,使得模板成为了一种将重复性的工作抽象化、自动化的工具,从而极大地提高了代码的复用性和开发效率。简而言之,函数模板让编译器承担了生成多样化函数实例的职责,让程序员能够专注于逻辑和结构,而不是繁琐的细节。

ps: 函数模版就像是让编译器干苦力,从而减去我们的工作量。

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将A确定为double类型,然
后产生一份专门处理double类型的代码:
在这里插入图片描述
就这样编译器生成一个个函数,将模版实例化,这是一种隐式实例化。
我们在使用过程中可以通过显示实例化与隐式实例化来进行实例化

  1. 显示实例化:在函数名后的<>中指定模板参数的实际类型sum<int>(a,b) ,直接表明想要进行什么数据类型的函数即可。
  2. 隐式实例化:让编译器根据实参推演模板参数的实际类型,也就是正常使用函数,让编译器去处理类型(可能会发生类型转换,存在隐患)。

调用规则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
  2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
  3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

2 类模板

什么是类模版

类模板(Class Templates):允许定义一个类,其成员函数和方法可以操作任何类型的数据。与函数模板类似,编译器会根据使用时指定的类型来实例化类的特定版本。我们之前实现的vector等各种容器都使用到了类模版,通过类模版我们可以适配各种数据类型,省去重复造轮子的过程。

template <typename T>
class Stack {
public:void push(T value);T pop();bool isEmpty();
private:std::vector<T> elems;
};

在这个例子中,Stack 类可以被用来创建任何类型数据的堆栈。

如何使用类模版

与函数模版类似,我们在类声明的前面加上:

//需要几个模版就使用几个
template<typename T1, typename T2,......,typename Tn>
class ClassName
{};

然后在类声明里面就可以直接使用我们的模版类型。

对于类模版的实例化是很关键的:

vector<int> num;
stack<string> st;
queue<char> q;
//在迭代器中更是好用
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;

我们加入对应的模版参数即可!!!

C++标准模板库(Standard Template Library,STL)是泛型编程在C++中的一个典型应用,它提供了一系列模板化的数据结构和算法,如向量(vector)、列表(list)、队列(queue)、栈(stack)、排序算法等,这些都可以用于任何符合特定要求的类型。

4 特别注意

通过上述的介绍,就可以进行使用模版来进行代码的编写了。但是仍然有一些注意事项!!!

4.1 非类型模板参数

模板参数分类类型形参与非类型形参
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用

就比如STL 中有一个这样的容器array(很鸡肋,一般不使用,而且由于是静态数组,直接开在栈区,容易造成栈溢出),如果我们想要一个静态数组,就可以通过它来创建:

// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:T& operator[](size_t index) { return _array[index]; }const T& operator[](size_t index)const { return _array[index]; }size_t size()const { return _size; }bool empty()const { return 0 == _size; }
private:T _array[N];//可以当做常量来使用size_t _size;
};int main()
{array<int, 10> arr1;array<int, 1000> arr2;return 0;
}

通过传入的参数,编译器会生成两个不同的类,类的模版参数就是给定参数。
在C++20之前,只支持整型作为非类型模版参数(char , short , int , long long… )

4.2 模版缺省值

像函数参数一样,模版参数也支持使用缺省值!!!
使用缺省值就可以方便我们传入参数了:

//这里就是使用了缺省值
template<class T, size_t N = 10>
class array
{};

另外再优先队列里也有很重要的使用:

//    默认底层容器是vector<T> , 默认用来比较的仿函数是 less<T>
template<class T , class Container = vector<T> , class compare = less<T> >
class priority_queue
{
};

4.3 编译细节

注意看下面的代码,我们在[ ] 重载中加入了一个size(1),明显不和语法规范,但是我们来看编译会出现什么现象:

template<class T, size_t N = 10>
class array
{
public:T& operator[](size_t index) { assert(index < N);//明显错误size(1);return _array[index]; }const T& operator[](size_t index)const { return _array[index]; }size_t size()const { return _size; }bool empty()const { return 0 == _size; }
private:T _array[N];//可以当做常量来使用size_t _size;
};int main()
{array<int, 10> arr1;return 0;
}

来看:
在这里插入图片描述
我们的代码居然可以正常运行!!!这是怎么回事儿???

因为编译器在遇到模版时会进行下面操作:

  1. 根据模版的实例化形成模版半成品
  2. 实例化成具体的类/函数
  3. 进行语法编译

但是这里又增加了一个新的概念:按需实例化!!!没有实例化之前只会进行简单的框架检查。
也就是只有使用对应函数才会进行函数的实例化,才会进行语法编译,才会报错!!!
没有调用operator[ ],所以operator[ ] 有调用参数不匹配,就没有检查出来。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/8789d512205f4ae1b2336702d071f9aa.png
所以只有我们使用[ ] 重载函数时,才会进行检查!!!

4.4 模版特化

模版特化就是指把模版的参数进行确定,就进行了特殊化:
来看一段代码:

#include<iostream>using namespace std;template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};template<>
class Data<int, char>
{
public:Data() { cout << "特化:Data<int, char>" << endl; }
private:int _d1;char _d2;
};void TestVector()
{Data<int, int> d1;Data<int, char> d2;
}int main()
{TestVector();return 0;
}

我们运行看看:
在这里插入图片描述

得到的是这样的结果。因为如果类的模版参数与模版特化一致,那么就会进行特化的模版来进行实例化。

比较复杂一点点的用法是指针特化

class Data
{
public:Data(int a, char b) {  _d1 = a;_d2 = b;}bool operator<(Data d){return _d1 < d._d1;}
private:int _d1;char _d2;
};template<class T>
bool Less(T left, T right)
{cout << "Less(T left, T right)" << endl;return left < right;}template<>
bool Less<Data*>(Data* left, Data* right)
{cout << "Less<Data*>(Data* left, Data* right)" << endl;return *left < *right;
}void TestVector()
{Data* d1 = new Data(1,'c');Data* d2 = new Data(2,'c');Less(d1,  d2);
}int main()
{TestVector();return 0;
}

这样就是对指针进行特化
在这里插入图片描述
如果加上这个:


template<class T>
bool Less(T* left, T* right)
{cout << " Less(T* left, T* right)" << endl;return *left < *right;
}

那么就会优先执行这个指针模版。
总的来说,函数模版真不如直接使用函数重载!!!

特化分为:全特化与偏特化

  1. 全特化即是将模板参数列表中所有的参数都确定化
  2. 偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
    • 部分特化将模板参数类表中的一部分参数特化,如上面的例子。
    • 参数更进一步的限制,偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。如上面的例子

类特化的是使用场景主要是在仿函数中进行使用,比如我们之前实现优先队列,在里面我们直接使用:

template<class T>
bool Less(T* left, T* right)
{cout << " Less(T* left, T* right)" << endl;return *left < *right;
}

可以适配更多的类型指针。

4.5 模版的分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式

模版不支持分离编译,如果声明与定义写到两个文件里,就会报错。

// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}// main.cpp
#include"a.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}

这样就会发生报错!!!链接错误
链接错误:是在语法没问题情况下,链接的时候,一个函数声明去其他文件寻找函数定义,找不到就会发生链接错误。

那为什么寻找不到呢???明明我们写了函数定义。
因为 a.cpp下的函数定义没有实例化,调用函数时仅仅是声明知道了使用什么模版类型,而函数定义不知道使用什么模版参数,那自然无法实例化!!!

解决方法很简单:

  1. 将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h就很好的。推荐使用这种。因为.h文件预处理展开后,实例化模版时,既有声明又有定义,直接就实例化了,就有函数地址了。不需要链接时再去找。
  2. 模板定义的位置显式实例化。这种方法真不实用,真不推荐使用。这样模版还有什么意义!?
// a.cpp//template<class T>
//T Add(const T& left, const T& right)
//{
//	return left + right;
//}
template<>
int Add(const int& left, const int& right)
{return left + right;
}
template<>
double Add(const double& left, const double& right)
{return left + right;
}

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

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

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

相关文章

AI大模型探索之路-训练篇3:大语言模型全景解读

文章目录 前言一、语言模型发展历程1. 第一阶段&#xff1a;统计语言模型&#xff08;Statistical Language Model, SLM&#xff09;2. 第二阶段&#xff1a;神经语言模型&#xff08;Neural Language Model, NLM&#xff09;3. 第三阶段&#xff1a;预训练语言模型&#xff08…

Python基础知识(二)

&#x1f3ac; 秋野酱&#xff1a;《个人主页》 &#x1f525; 个人专栏:《Java专栏》 《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 1.输入和输出函数1.1输出函数1.2输入函数 2.常见运算符2.1赋值运算符2.2比较运算符2.3逻辑运算符2.4and逻辑与2.5or逻辑或2.6not逻…

c++二叉树的进阶--二叉搜索树

1. 二叉搜索树的概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树&#xff0c;或者是具有以下性质的二叉树: 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值 它的左…

Swift-27-类的初始化与销毁

Swift的初始化是一个有大量规则的固定过程。初始化是设置类型实例的操作&#xff0c;包括给每个存储属性初始值&#xff0c;以及一些其他准备工作。完成这个过程后&#xff0c;实例就可以使用了。 简单来讲就是类的构造函数&#xff0c;基本语法如下&#xff1a; 注意&#xff…

C语言扫雷游戏完整实现(上)

文章目录 前言一、新建好头文件和源文件二、实现游戏菜单选择功能三、定义游戏函数四、初始化棋盘五、 打印棋盘函数六、布置雷函数七、玩家排雷菜单八、标记功能的菜单九、标记功能菜单的实现总结 前言 C语言从新建文件到游戏菜单&#xff0c;游戏函数&#xff0c;初始化棋盘…

JavaScript-4.正则表达式、BOM

正则表达式 正则表达式包含在"/"&#xff0c;"/"中 开始与结束 ^ 字符串的开始 $ 字符串的结束 例&#xff1a; "^The"&#xff1a;表示所有以"The"开始的字符串&#xff08;"There"、"The cat"等&#x…

数据结构(八)——排序

八、排序 8.1 排序的基本概念 排序(Sort)&#xff0c;就是重新排列表中的元素&#xff0c;使表少的元素满足按关键字有序的过程。 输入∶n个记录R1,R2...., Rn&#xff0c;对应的关键字为k1, k2,... , kn 输出:输入序列的一个重排R1,R2....,Rn&#xff0c;使得有k1≤k2≤...≤…

综合大实验

题目&#xff1a; 1、R4为ISP&#xff0c;其上只配置IP地址&#xff1b;R4与其他所直连设备间均使用公有IP&#xff1b; 2、R3-R5、R6、R7为MGRE环境&#xff0c;R3为中心站点&#xff1b; 3、整个OSPF环境IP基于172.16.0.0/16划分&#xff1b;除了R12有两个环回&#xff0c;其…

VUE父组件向子组件传递值

创作灵感 最近在写一个项目时&#xff0c;遇到了这样的一个需求。我封装了一个组件&#xff0c;这个组件需要被以下两个地方使用&#xff0c;一个是搜索用户时用到&#xff0c;一个是修改用户信息时需要用到。其中&#xff0c;在搜索用户时&#xff0c;可以根据姓名或者账号进…

[前端]NVM管理器安装、nodejs、npm、yarn配置

NVM管理器安装、nodejs、npm、yarn配置 NVM管理器安装 nvm(Node.js version manager) 是一个命令行应用&#xff0c;可以协助您快速地 更新、安装、使用、卸载 本机的全局 node.js 版本。 nvm下载地址&#xff1a;https://github.com/coreybutler/nvm-windows/releases 1.全部…

Unity面向切面编程

一直说面向AOP&#xff08;切面&#xff09;编程&#xff0c;好久直接专门扒出理论、代码学习过。最近因为某些原因&#x1f62d;还得再学学造火箭的技术。 废话不多说&#xff0c;啥是AOP呢&#xff1f;这里我就不班门弄斧了&#xff0c;网上资料一大堆&#xff0c;解释的肯定…

mybatis中<if>条件判断带数字的字符串失效问题

文章目录 一、项目背景二、真实错误原因说明三、解决方案3.1针对纯数字的字符串值场景3.2针对单个字符的字符串值场景 四、参考文献 一、项目背景 MySQL数据库使用Mybatis查询拼接select语句中进行<if>条件拼接的时候&#xff0c;发现带数字的或者带单个字母的字符串失效…

CPU资源控制

一、CPU资源控制定义 cgroups&#xff08;control groups&#xff09;是一个非常强大的linux内核工具&#xff0c;他不仅可以限制被namespace隔离起来的资源&#xff0c; 还可以为资源设置权重、计算使用量、操控进程启停等等。 所以cgroups&#xff08;control groups&#xf…

Netty学习——实战篇5 Netty 心跳监测/WebSocket长连接编程 备份

1 心跳监测 MyServer.java public class MyServer {public static void main(String[] args) {NioEventLoopGroup bossGroup new NioEventLoopGroup(1);NioEventLoopGroup workerGroup new NioEventLoopGroup();try {ServerBootstrap serverBootstrap new ServerBootstrap…

学习Docker笔记

在23号刚刚学完新版本的docker还想说回去继续学习老版本的springcloud课程里面的docker 结果一看黑马首页新版本课程出了&#xff0c;绷不住了。以下是我学习新版本docker的笔记&#xff0c;记录了我学习过程遇到的各种bug和解决&#xff0c;也参考了黑马老师的笔记&#xff1a…

TDengine高可用探讨

提到数据库&#xff0c;不可避免的要考虑高可用HA&#xff08;High Availability&#xff09;。但是很多人对高可用的理解并不是很透彻。 要搞清高可用需要回答以下几个问题&#xff1a; 什么是高可用&#xff1f;为什么需要高可用&#xff1f;高可用需要达到什么样的目标&am…

Unity射线实现碰撞检测(不需要rigbody组件)

使用physic.CapsulCast&#xff08;&#xff09;&#xff1b; 前面3个参数生成一个胶囊体&#xff0c; 向着发射方向&#xff0c;发射出一串的胶囊&#xff08;没有最大距离&#xff09; 有最大距离&#xff0c;可以节约性能开销。 physic.CapsulCast&#xff08;&#xff0…

easypoi 导出增加自增序列

要求&#xff1a;使用easypoi导出Excel的时候&#xff0c;要求增加”序号“列&#xff0c;从1开始增加。这列与业务数据无关&#xff0c;只是展示用&#xff0c;便于定位。如下图所示 实现方式&#xff1a;Java对象新增一列&#xff0c;注意name "序号", format &…

Linux-缓冲区(简单理解)

1. 缓冲区是什么 缓冲区就是一段内存空间。 2. 为什么要有缓冲区 IO写入有两种&#xff1a; 写透模式&#xff08;WT&#xff09; 成本高&#xff0c;效率低写回模式&#xff08;WB&#xff09; 成本低&#xff0c;效率高 写透模式&#xff1a;每次的文件写入都要立即刷新…

使用ClickHouse和Terraform进行CI/CD

本文字数&#xff1a;11047&#xff1b;估计阅读时间&#xff1a;28 分钟 审校&#xff1a;庄晓东&#xff08;魏庄&#xff09; 本文在公众号【ClickHouseInc】首发 简介 在 ClickHouse&#xff0c;我们致力于以 API 为先的开发方式来构建 ClickHouse Cloud。用户通过用户界面…