【C++深入系列】:模版详解(上)

🔥 本文专栏:c++
🌸作者主页:努力努力再努力wz

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

💪 今日博客励志语录你不需要很厉害才能开始,但你需要开始才能很厉害。

★★★ 本文前置知识:

类和对象(上)

类和对象(中)

类和对象(下)


引入

那假设有这么一个场景,那么你现在要编写一个简单的程序,那么该程序实现的内容就是交换两个数,那么此时你会如何实现呢?

那么我们可以定义一个交换的_swap函数,那么这个函数就是用来实现交换两个数,但是这两个数的数据类型可以是内置类型也可以是自定义类型,那么意味着我们得利用函数的重载,编写多个同名但是参数列表不同的 _swap函数来处理多种不同数据类型的两个数的交换

void swap(int& a,int& b)
{int temp=a;a=b;b=temp;
}
void swap(double& a,double&b)
{double temp=a;a=b;b=temp;
}
.....

那么上面的这种实现方式肯定是没问题的,但是不难会发现这里定义了的多个 _swap函数,那么它们的函数逻辑都是一样的,唯一的区别就是处理的数据类型是不同的,那么这样其实会导致代码重复以及逻辑冗余,并且假设我们 _swap函数的代码逻辑写错了,我们还得修改之前编写的所有的 _swap函数,所以还存在不好维护的问题

为了解决这个问题,如果我们可以尝试把这些重载函数的代码逻辑给提取出来,忽略其涉及到的数据的数据类型,而我们需要调用处理特定数据类型的函数的话,那么只需要套用这个代码逻辑,将其中的数据类型给替换为我们想要的目标数据类型,而这种解决方式正是c++的模版解决该问题的核心思想,那么下文我将会从模版的使用以及其相关细节等多个维度带你全面认识模版,那么相信你看完之后,一定会对模版爱不释手

什么是模版以及模版如何使用

那么从模版这个名字,就形象的告诉了它的作用,那么对于函数来说,如果该函数要对不同的数据类型应用同一套函数逻辑的话,那么我们就可以采取模版,那么将该函数的代码逻辑给提取出来,忽略其涉及到的数据的数据类型,那么所谓的忽略其涉及到的数据的数据类型,不是说模版不提供数据类型,而是说它将该函数中涉及到的数据的数据类型都用一个变量来表示,那么这个变量可以代表着任意一个数据类型比如像int,double这样的内置类型,也可以是你自己定义的自定义类型,那么说这么多,我们先来看看模版长什么样子,那么我以上文的 _swap函数为例,来看看如何定义一个 _swap函数模版:

template<typename T>
void _swap(T& a,T&b)
{T temp=a;a=b;b=a;
}

那么其中模版的定义需要用到template关键字,那么template就是模版的英文,而后面跟上的尖括号里面的内容就是数据类型,那么它用一个变量T来表示,那么这个T就代表着任意的数据类型,那么它的专业的术语叫做模版参数,其中我们可以用typename或者class来声明一个模版参数,template下面就是模版函数的定义,那么此时模版函数不关心具体处理的是什么类型的数据,并且将涉及到的数据类型都用模版参数来替代

那么知道了模版如何定义之后,我们再来看看模版是如何使用,那么还是以 _swap函数为例,那么我写了一份简单的调用 _swap函数的c++代码:

#include<iostream>
using namespace std;
template<typename T>
void _swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
int main()
{int a = 10;int b = 20;cout << "befor: a=" << a << " b=" << b << endl;_swap(a, b);cout << "after: a=" << a << " b=" << b << endl;return 0;
}

在这里插入图片描述

那么我们发现调用模版函数就和我们调用普通函数一样,在代码中直接调用即可,而这就是模版的强大之处,当我们调用模版函数时,那么编译器会根据你函数调用处识别到参数的数据类型,然后找到匹配的函数模版,由于模版定义的时候,你已经给出了该函数的完整定义,只不过不涉及到的具体的数据类型,那么这里编译器就会将T给赋予具体的含义,替换为int类型或者double类型等等,也就意味着编译器此识别到模版函数调用处涉及到的参数的数据类型,然后根据模版实例化出一份具体的函数代码实现

所以之所以我们能够像调用普通函数那样调用模版函数,是因为有一个人为我们默默承担了一切,那么这个人就是编译器,那么本来应该由我们自己去编写处理int类型的数据的 _swap函数的代码以及处理double类型的数据的 _swap函数的代码,那么这个工作都交给了编译器来完成,也就是编译器在编译阶段会根据你调用该模版函数传的具体的参数的数据类型,然后它自己根据模版生成一份该函数的代码

所以总而言之,模版就是一个蓝图,那么编译器就像一个建筑师,那么它手上持有了这份蓝图,那么编译器会根据模版函数调用处传递的参数的类型,来根据蓝图建造出不同的房子,那么这就是模版,那么模版的出现就体现了一个泛型编程的思想,所谓的泛型编程就是编写独立于数据类型的代码

模版的相关细节补充

1.template关键字

那么我们知道了模版函数如何定义以及如何调用之后,那么有的小伙伴可能对于template关键字的使用不是特别熟悉,假设有这么一个场景,那么有个小伙伴想要定义两个模版函数,比如一个是上文所说的用于交换两个数的 _swap函数,另一个则是用于比较两个数大小的 _max函数,那么此时它采取的是这种方式定义模版函数的:

template<typename T>
void _swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
T _max(T&a,T&b)
{return a>=b? a: b;    
}

那么你觉得上面小伙伴这种定义的方式对吗?那么接下来我通过代码来验证一下:

#include<iostream>
using namespace std;
template<typename T>
void _swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
T _max(T& a, T& b)
{return a >= b?a : b;
}
int main()
{int a = 10;int b = 20;cout << "max :" << _max(a, b) << endl;return 0;
}

在这里插入图片描述

那么根据运行结果我们知道这么定义是错的,那么我猜这么定义的小伙伴心里面想的就是我在template关键字下面定义两个模版函数,那么这两个模版函数都会采取用上面template关键字后面的同一个模版参数,那么如果有的小伙伴是怎么想的话,那么我想说,你还是把编译器想的太天真了

根据编译器报的错误,那么我们可以发现template作用或者说修饰的范围只能是它下面接挨着的函数或者我们后文说道的类(类也可以定义为模版),那么此时编译器就把你后面定义的 _max当做一个普通的全局函数的定义来处理了,而这里我们没有所谓的T的内置类型,并且我们在代码中也没有定义所谓的T的类,编译器也无法将其视作自定义类型来处理,所以这里会报一个无法识别T的类型的错误,那么在编译器阶段就不会通过,所以我们还是老老实实的在定义每一个模版函数或者模版类前面加一个template关键字

正确版本:

#include<iostream>
using namespace std;
template<typename T>
void _swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
template<typename T>
T _max(T& a, T& b)
{return a >= b?a : b;
}
int main()
{int a = 10;int b = 20;cout << "max :" << max(a, b) << endl;cout << "a: " << a << " b:" << b << endl;return 0;
}

在这里插入图片描述

2.模版函数的调用机制

那么有的现在有的小伙伴可能会好奇,对于上文提到的 _swap函数,那么如果这里我定义了一个 _swap的普通函数的话,并且也定义了 _swap 的模版函数,那么我能否调用成功该 _swap函数,那么这里我首先写了一个代码来实验一下,那么这里代码的逻辑也很简单,那么就是定义了一个 _swap的模版函数以及一个处理int类型数据交换的普通函数 :

#include<iostream>
using namespace std;
template<typename T>
void _swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
void _swap(double& a, double& b)
{double temp = a;a = b;b = temp;
}
int main()
{int a = 10;int b = 20;cout << "befor: " << "a=" << a << " b=" << b << endl;_swap(a, b);cout <<"after: " << "a: " << a << " b:" << b << endl;return 0;
}

在这里插入图片描述

那么结果没有任何问题

那么我们再来简单的改变一下代码的逻辑,让其处理double类型的数据的交换:

#include<iostream>
using namespace std;
template<typename T>
void _swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
void _swap(double& a, double& b)
{double temp = a;a = b;b = temp;
}
int main()
{double a = 10.7;double b = 20.5;cout << "befor: " << "a=" << a << " b=" << b << endl;_swap(a, b);cout <<"after: " << "a: " << a << " b:" << b << endl;return 0;
}

在这里插入图片描述

发现结果也没有问题,那么我们对于第一个代码,那么我们应该知道,这里我们调用的真正的 _swap函数其实是编译器利用模版生成的 _swap函数因为没有处理int类型的普通函数,而对于第二种情况,我们可能会存有一个疑问,那么就是这里理论上编译器可以调用模版实例化生成的函数,也可以调用我们定义的普通函数,那么这里编译器的最终的行为是什么呢?那么这里我在代码中打了断点,通过调试来看看编译器此时的选择:

在这里插入图片描述

那么根据调试的结果,我们可以发现,当我们运行到 _swap函数调用处的时候,那么此时编译器调用的是我们自己定义的 _swap函数,那么这里我们就可以认识到编译器调用模版函数的机制了,那么当我们调用一个函数的时候,那么编译器此时会收集候选函数,而这里的候选函数就包括我们自己定义匹配的普通函数以及模版函数,那么这时候编译器调用的优先级肯定是优先调用参数列表最为匹配的普通函数,如果没有普通函数,但是有模版函数的话,那么此时编译器才会根据模版实例化函数,然后调用实例化后的函数

3.在调用处显示实例化

那么在上文的 _max函数的例子中,那么该函数只能处理一种数据类型,也就是 _swap函数只能处理两个相同数据类型的数的交换,但是不能处理两个数数据类型不同的情况,比如一个int类型的数与double类型的数的交换:

#include<iostream>
using namespace std;
template<typename T>
T _max(T& a, T& b)
{return a>=b ?a :b;
}
int main()
{int a=10;double b=20.8;cout<<"max: "<<_max(a,b)<<endl;return 0;
}

在这里插入图片描述

那么之所以会出现这种情况,就是因为我们定义的模版参数只有一个,那么这里编译器识别到模版函数的调用处,那么其识别到了两种不同的类型,但是按照模版函数的定义,该模版函数只能处理一种数据类型,所以它无法根据模版来实例化出函数,所以为了解决这个问题,那么第一种做法可以是定义两个模版参数,那么这样编译器就可以根据模版生成函数了

#include<iostream>
using namespace std;
template<typename T1,typename T2>
T2 _max(T1& a, T2& b)
{return a >= b ? a : b;
}
int main()
{int a = 10;double b= 20.8;cout << "max: " << _max(a, b) << endl;return 0;
}

在这里插入图片描述

那么第二种方式就是可以利用类型转换,那么为了匹配模版参数,那么这里我们可以将int类型转换为double类型,但是这里由于是传值而不是传引用,那么类型转换中间会生成临时变量,而临时变量具有常性,所以这里我们得用const引用来接收:

#include<iostream>
using namespace std;
template<typename T>
T _max(const T& a, const T& b)
{return a >= b ? a : b;
}
int main()
{int a = 10;double b= 20.8;cout << "max: " << _max((double)a, b) << endl;return 0;
}

那么还有一种方式则是在调用处显示实例化,那么所谓显示实例化,就是在调用处给编译器指定你要将该位置的调用的模版函数的模版参数给实例化成什么类型,那么编译器此时就不会关心你传递的参数的类型,而是先生成一份函数,然后再去识别类型,调用相应的匹配的函数

#include<iostream>
using namespace std;
template<typename T>
T _max(const T& a, const T& b)
{return a >= b ? a : b;
}
int main()
{int a = 10;double b= 20.8;cout << "max: " <<  _max<double>(a, b) << endl;return 0;
}

4.类模版

那么刚才讲的例子都是函数模版,那么我们类也可以支持所谓的泛型编程的思想,也就是不关心特定的数据类型,那么比如拿我们的栈来作为例子,那么我们知道我们可以将栈定义为一个类,然后采取动态数组的方式来实现,那么此时这个数组里面的元素可以是存储的是int类型,也可以存储的是double类型,那么如果没有模版的话,那么我们采取的方式就是定义两个类,那么一个类名叫stackInt对应的是存储int类型的数据的栈,还得定义一个类名为stackDouble的类对应存储double类型的数据的栈,那么这里面临的问题和上文的函数面临的问题一样,那么就是会导致代码重复并且不好维护

所以这里我们可以将栈定义成一个模版类,那么这里不关心该类中涉及到各种成员变量的数据类型以及成员函数中定义的变量的数据类型:

template<typename T>
class stack
{private:T* ptr;T top;T max_size;public:stack(int _deafult):ptr(new T[_deafult]),top(-1),max_size(_deafult){}void push(T x){if(top+1==max_size){ptr=new T[2*max_size];max_size=2*max_size;}ptr[++top]=x;}void pop(){if(top==-1){return;}top--;}bool empty(){return top==-1;}
};

那么注意我们实例化一个模版类的对象的时候,就一定要注意,对于我们自定义的类来,那么它的类型就是类名,而对于模板类来说,那么类型不在等于类名,而是类名加模版参数才是类型,那么一定要注意这点

类名<type> a;

那么编译器会根据调用的类模版以及模版参数实例化出一份类:

#include<iostream>
using namespace std;
template<typename T>
class stack
{
private:T* ptr;T top;T max_size;
public:stack(int _deafult=4):ptr(new T[_deafult]), top(-1), max_size(_deafult){}void push(T x){if (top + 1 == max_size){ptr = new T[2 * max_size];max_size = 2 * max_size;}ptr[++top] = x;}void pop(){if (top == -1){return;}top--;}bool empty(){return top == -1;}void print(){for (int i = 0;i <= top;i++){cout << "stack[i]: " << ptr[i] << " ";}cout << endl;}
};
int main()
{stack<int> a;a.push(1);a.push(2);a.push(4);a.push(5);a.pop();a.print();return 0;
}

在这里插入图片描述

结语

那么这就是本期关于模版所讲解的全部内容,那么这部分内容只是模版的一部分知识点,那么剩余知识点我打算在介绍完STL之后的模版下篇讲解,那么这部分知识就足够让我们顺利进入STL的学习,那么注意模版应用的场景一定还是类或者函数的逻辑相同,处理的数据类型不同的情况,如果函数的逻辑不同的话,那么还是得老老实实利用函数重载自己去手写几份代码

那么下一期我将讲解string,那么我会持续更新,希望你多多关注,如果本文有帮组到你的话,还请多多支持,你的支持就是我创作的最大的动力!
在这里插入图片描述

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

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

相关文章

java 设计模式之策略模式

简介 策略模式&#xff1a;策略模式可以定制目标对象的行为&#xff0c;它通过传入不同的策略实现&#xff0c;来配置目标对象的行为。使用策略模式&#xff0c;就是为了定制目标对象在某个关键点的行为。 策略模式中的角色&#xff1a; 上下文类&#xff1a;持有一个策略类…

Perf学习

重要的能解决的问题是这些&#xff1a; perf_events is an event-oriented observability tool, which can help you solve advanced performance and troubleshooting functions. Questions that can be answered include: Why is the kernel on-CPU so much? What code-pa…

「仓颉编程语言」Demo

仓颉编程语言」Demo python 1)# 仓颉语言写字楼管理系统示例&#xff08;虚构语法&#xff09;# 语法规则&#xff1a;中文关键词 类Python逻辑定义 写字楼管理系统属性:租户库 列表.新建()报修队列 列表.新建()费用单价 5 # 元/平方米方法 添加租户(名称, 楼层, 面积):…

锁(Mutex)、信号量(Semaphore)与条件量(Condition Variable)

一、同步机制的核心意义 在多线程/多进程编程中&#xff0c;当多个执行流共享资源&#xff08;如变量、内存、文件&#xff09;时&#xff0c;可能因操作顺序不确定导致数据竞争&#xff08;Data Race&#xff09;。同步机制的作用是&#xff1a; 保证原子性&#xff1a;确保…

前端基础之《Vue(6)—组件基础(2)》

接上一篇。 七、v-model深入学习 <html> <head><title>组件基础-4</title><style>.score {display: inline-block;}.score>span {display: inline-block;width: 25px;height: 25px;background: url(./assets/star.png) center center / 25p…

SQL:聚合函数(Aggregate Functions)

目录 第一性原理出发思考 ——我们为什么需要聚合函数&#xff1f; 什么是聚合函数&#xff1f; 常见聚合函数 实例讲解 &#x1f538; 1. COUNT() —— 计数 &#x1f538; 2. MAX() / MIN() —— 最大 / 最小值 &#x1f538; 3. SUM() —— 求和 &#x1f538; 4. …

海关总署广东:广东外贸一季度进出口2.14万亿元 同期增长4.2%

大湾区经济网湾区财经报道&#xff0c;据海关总署广东分署统计&#xff0c;今年一季度&#xff0c;广东外贸进出口2.14万亿元&#xff0c;较去年同期&#xff08;下同&#xff09;增长4.2%&#xff0c;增速高于全国2.9个百分点。其中&#xff0c;出口1.34万亿元&#xff0c;增长…

MySQL中高级语法

Mysql高级语法 持续更新中… 1、EXISTS语法 一、基本语法结构 SELECT [列名] FROM [主表] WHERE [条件]AND EXISTS (SELECT 1 -- 子查询内容无关&#xff0c;仅需占位符&#xff08;如1、*、X等&#xff09;FROM [子查询表]WHERE [关联条件] -- 必须与外层查询关联&#xf…

SpringBoot 调用deepseek

个人学习心得&#xff0c;仅供参考 软件环境&#xff1a; JDK 17 你用JDK 11 无法支持SpringBoot 3SpringBoot 3 版本以上才支持spring aimavan 3.6.11.获取Deepseek官网的API-key 官网&#xff1a;https://platform.deepseek.com/api_keys 2.创建项目 这样创建 添加依赖…

性能测试面试题的详细解答

以下是性能测试面试题的详细解答&#xff1a; 1. 性能测试的流程是怎样的&#xff1f; 性能测试流程通常包括以下几个步骤&#xff1a; - **需求分析**&#xff1a;明确测试目标、性能指标&#xff08;如响应时间、吞吐量等&#xff09;。 - **环境搭建**&#xff1a;搭建测试环…

C++程序设计基础实验:C++对C的扩展特性与应用

C程序设计基础实验&#xff1a;C对C的扩展特性与应用 &#x1f525; 本文详细讲解C基础实验&#xff0c;包含C对C语言的扩充与增强特性&#xff0c;从零开始掌握函数重载、引用、指针等核心概念&#xff0c;附详细代码分析与运行结果。适合C初学者和有C语言基础想学习C的同学&a…

量子神经网络编译器开发指南:从理论突破到产业落地全景解析

本文深度剖析IBM Qiskit 5.0量子经典混合编译器的技术架构&#xff0c;详解如何基于含噪量子处理器实现MNIST手写数字分类任务&#xff08;准确率达89%&#xff09;。结合本源量子云、百度量子等国内平台免费配额政策&#xff0c;系统性阐述量子神经网络开发的技术路线与资源获…

ESP32之本地HTTP服务器OTA固件升级流程,基于VSCode环境下的ESP-IDF开发(附源码)

背景知识&#xff1a; 本实验利用编译链内Python内置的 HTTP 服务器&#xff0c;将升级包通过http发送给设备&#xff0c;实现OTA固件升级。 目录 背景知识&#xff1a; 1.创建工程 1.1 创建OTA基础工程 3.编写、修改代码 3.1 修改menuconfig配置文件 3.1.1 配置WiFi账…

BootStrap:进阶使用(其一)

今天我要讲述的是在BootStrap中进一步使用的方法与代码举例; 导航条 作为在应用或网站中作为导航页头的响应式基础组件。导航条在移动设备上可以折叠&#xff08;且可开可关&#xff09;&#xff0c;在视口&#xff08;viewport&#xff09;宽度增加时逐渐变为水平展开模式 …

ffmpeg无损转格式的命令行

将ffmpeg.exe拖入命令行窗口 c:\users\zhangsan>D:\ffmpeg-2025-03-11\bin\ffmpeg.exe -i happy.mp4 -c:v copy -c:a copy 格式转换后.mkv -c:v copy 仅做拷贝视频,不重新编码 -c:a copy 仅做拷贝音频 ,不重新编码

【Linux】深入理解Linux文件系统:从C接口到内核设计哲学

文章目录 前言一、C语言中的文件接口1. 文件指针&#xff08;句柄&#xff09;FILE*以写方式打开文件&#xff0c;若文件不存在会新建一个文件W写入方式&#xff0c;在打开文件之前都会将文件内容全部清空追加写方式&#xff0c;其用法与写方法一致&#xff0c;不同在于a方法可…

国产品牌芯洲科技100V降压芯片系列

SCT2A25采用带集成环路补偿的恒导通时间(COT)模式控制&#xff0c;大大简化了转换器的片外配置。SCT2A25具有典型的140uA低静态电流&#xff0c;采用脉冲频率调制(PFM)模式&#xff0c;它使转换器在轻载或空载条件下实现高转换效率。 芯洲科技100V降压芯片系列提供丰富的48V系…

ctfshow-大赛原题-web702

因为该题没有理解到位&#xff0c;导致看wp也一直出错&#xff0c;特此反思一下。 参考yu22x师傅的文章 &#xff1a;CTFSHOW大赛原题篇(web696-web710)_ctfshow 大赛原题-CSDN博客 首先拿到题目&#xff1a; // www.zip 下载源码 我们的思路就是包含一个css文件&#xff0c;…

LabVIEW技巧——获取文件版本信息

获取可执行文件&#xff08;exe&#xff09;版本信息的几种方法 方法1. LabVIEW自带函数 labview自带了获取文件版本号的VI&#xff0c;但是没有开放到程序框图的函数选板中&#xff0c;在该目录下可以找到&#xff1a;...\LabVIEW 20xx\vi.lib\Platform\fileVersionInfo.llb…

三格电子——CAN 转光纤(点对点)布线常见问题

1、CAN 布线 &#xff08;1&#xff09;H 接 H ,L 接 L &#xff08;2&#xff09;两端设备挂 120 欧姆电阻 2、假如用点对点的 CAN 转光纤现实远程传输 &#xff08;1&#xff09;H 接 H ,L 接 L &#xff08;2&#xff09;光端机都挂 120 欧姆电阻 每个光端机挂的设备有一个加…