C++ 模板保姆级详解——template<class T>(什么是模板?模板分哪几类?模板如何应用?)

目录

一、前言

二、 什么是C++模板

💦泛型编程的思想 

 💦C++模板的分类

 三、函数模板

 💦函数模板概念

 💦函数模板格式

💦函数模板的原理

 💦函数模板的实例化

🍎隐式实例化

 🍉显式实例化

💦模板支持多个模板参数

 💦模板参数的匹配原则

四、类模板 

 💦类模板的概念

 💦类模板格式

 💦类模板的实例化

 💦类模板的分离编译

五、总结

六、共勉 


一、前言

        在我们学习C++时,常会用到函数重载。而函数重载,通常会需要我们编写较为重复的代码,这就显得臃肿,且效率低下重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。

       那么,模板的出现,就让这些问题有了解决方案,所以本次博客将为大家详细的讲解C++的模板!!

二、 什么是C++模板

        程序设计中经常会用到一些程序实体它们的实现和所完成的功能基本相同,不同的仅 仅是所涉及的数据类型不同。而模板正是一种专门处理不同数据类型的机制。


       模板------是泛型程序设计的基础(泛型generic type——通用类型之意)。
 

        函数、类以及类继承为程序的代码复用提供了基本手段,还有一种代码复用途径——类属类型(泛型),利用它可以给一段代码设置一些取值为类型的参数(注意:这些参数 的值是类型,而不是某类型的数据),通过给这些参数提供一些类型来得到针对不同类 型的代码。

💦泛型编程的思想 

         首先我们来看一下下面这三个函数,如果学习过了C++函数重载 和 C++引用 的话,就可以知道下面这三个函数是可以共存的,而且传值会很方便

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;
}

        但是真的很方便吗?这里只有三种类型的数据需要交换,若是我们需要增加交换的数据呢?再CV然后写一个函数吗?
        这肯定是不现实的,所以很明显函数重载虽然可以实现,但是有一下几个不好的地方:

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

       

        那是否能做到这么一点,告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码

总结:
        所以,总结上面的这么一个技术,C++的祖师爷呢就想到了【模版】这个东西,告诉编译器一个模子,然后其余的工作交给它来完成,根据不同的需求生成不同的代码

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

 💦C++模板的分类

1️⃣: 函数模板(function tempalte):使用泛型参数的函数(function with generic parameters)

2️⃣:类模板(class template):使用泛型参数的类(class with generic parameters)

 三、函数模板

 知晓了模版的基本概念后,首先我们要来看的就是【函数模版】

 💦函数模板概念

        函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
        通过函数模板,可以编写一种通用的函数定义,使其能够适用于多种数据类型,从而提高代码的复用性和灵活性。

 💦函数模板格式

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

因此,交换函数就可以这样套用模板:

template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}int main()
{int x = 2, y = 3;cout << x << " " << y << endl << endl;Swap(x, y);cout << x << " " << y << endl << endl;cout << "-------------------------------" << endl;char a = 'a', b = 'b';cout << a << " " << b << endl << endl;Swap(a, b);cout << a << " " << b << endl << endl;cout << "-------------------------------" << endl;double c = 2.1, d = 3.1;cout << c << " " << d << endl << endl;Swap(c, d);cout << c << " " << d << endl << endl;cout << "-------------------------------" << endl;return 0;
}

 

        我们通过这个函数模版,分别传入不同数据类型的参数,通过结果的观察可以发现这个函数模版可以根据不同的类型去做一个自动推导,继而去起到一个交换的功能。

💦函数模板的原理

 💬 那我现在想问一个问题,请问它们调用的真的是同一个函数吗?
        当然不是,这里我们三次Swap不是调用同一个函数,其实Swap的时候根据不同的类型通过模板定制出专属你的类型的函数,然后再调用,这里可以通过反汇编观察到:

 

        可以发现,在进行汇编代码查看的时候,被调用的函数模版生成了两个不同的函数,它们有着不同的函数地址,因此可以回答上一小节所提出的问题了,两次所调用的函数是不一样的,是根据函数模版所生成的


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

  


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

  • 补充:

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


直接套用swap即可:

int main()
{int x = 2, y = 3;cout << x << " " << y << endl << endl;swap(x, y);cout << x << " " << y << endl << endl;cout << "-------------------------------" << endl;char a = 'a', b = 'b';cout << a << " " << b << endl << endl;swap(a, b);cout << a << " " << b << endl << endl;cout << "-------------------------------" << endl;double c = 2.1, d = 3.1;cout << c << " " << d << endl << endl;swap(c, d);cout << c << " " << d << endl << endl;cout << "-------------------------------" << endl;return 0;
}

 💦函数模板的实例化

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

🍎隐式实例化

 隐式实例化 让编译器根据实参推演模板参数的实际类型

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是intAdd(d1, d2); //编译器推出T是doublereturn 0;
}

⚠ 注意:但是我调用的时候如若这样就会出错:

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); return 0;
}

💦模板支持多个模板参数

template<class K, class V> //两个模板参数
void Func(const K& key, const V& value)
{cout << key << ":" << value << endl;
}
int main()
{Func(1, 1); //K和V均intFunc(1, 1.1);//K是int,V是doubleFunc<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函数
}

四、类模板 

 💦类模板的概念

        类模板是对成员数据类型不同的类的抽象,它说明了类的定义规则,一个类模板可以生成多种具体的类。与函数模板的定义形式类似 类模板也是使用template关键字和尖括号“<>”中的模板形参进行说明,类的定义形式与普通类相同。

 💦类模板格式

  • 首先来看到的就是其定义格式,函数模版加在函数上,那对于类模版的话就是加在类上
template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};

我们以下面这个Stack类为例来进行讲解

  • 如果你学习了模版的相关知识后,一定会觉得这个类的限制性太大了只能初始化一个具有整型数据的栈,如果此时我想要放一些浮点型的数据进来的话也做不到,代码如下:
// C++ 正常情况下,如果需要不同类型的类(int char double 等)
typedef int DataType;
class Stack
{
public:// 构造函数Stack(int capacity = 3)   //初始化列表:_array(new DataType[capacity])   // 开辟一个DateType的动态数组,并进行初始化, _capacity(capacity),_size(0){}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){delete[]_array;_array = nullptr;_size = _capacity = 0;}
private:DataType* _array;int _capacity;int _size;
};
int main()
{Stack s1;return 0;
}

💬 如果没有模版技术的话你会如何去解决这个问题呢?很简单那就是定义多个类
这是我们同学最擅长的事,CV一下两个栈就有了,StackInt存放整型数据,StackDouble存放浮点型数据

class StackInt
class StackDouble

但是本文我们重点要讲解的就是【模版技术】,技术界有一句话说得好 “不要重复造轮子”

  • 下面就是使用模版去定义的一个类,简称【模板类】,不限制死数据类型,将所有的DataType都改为【T】,代码如下:
template<class T1>
class Stack
{
public:// 构造函数Stack(int capacity = 4):_a(new T1[capacity]),_capacity(capacity),_size(0){}void Push(T1 data){_a[_size] = data;_size++;}// ...其他方法// 析构函数~Stack(){delete[]_a;_a = nullptr;_capacity = _size = 0;}
private:T1* _a;int _capacity;int _size;
};int main()
{Stack<int> s1;Stack<double> s2;return 0;
}


 

 💦类模板的实例化

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

  • 可以看到因为我们将这个类定义为了类模版,此时便可以去初始化不同数据类型的栈了,上面说到过Stack是类名,但是像Stack<int>Stack<double>这些都是它的类型
int main()
{Stack<int> s1;		// intStack<double> s2;	// doubleStack<char> s3;		// charreturn 0;
}

 💦类模板的分离编译

        上面这样写的栈代码,其实并不是最规范的写法,还记得我们在学习C++类和对象讲到过一个类要声明和定义分离,那对于模板类也同样适用,我们马上来看看

首先进行栈 类的模板声明:

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;template<class T1>
class Stack
{
public:// 构造函数Stack(int capacity = 4);//插入函数void Push(T1 data);// 其他方法....// 析构函数~Stack();
private:T1* _a;int _capacity;int _size;
};

其次进行栈 类的模板定义:先看一下构造函数的定义

// 构造函数
Stack::Stack(int capacity):_a(new T1[capacity]),_capacity(capacity),_size(0)
{}

  • 不过呢可以看到直接像我们之前那样去进行类外定义似乎行不通,说缺少类模版“Stack”的参数列表因为这个成员函数内部也使用到了模版参数T,那么这个函数也要变为函数模版才行
  • 这里要强调一点的是对于普通类来说类名和类型是一样的, 像构造函数,它的函数名就是类名;可是对于模板类来说是不一样,类名和类型不一样,这里Stack只是这个模版类的类名罢了,但我们现在需要的是类型,此处就想到了我们在上面所学的【显式实例化】,这个模板类的类型即为Stack<T>

所以正确的写法为:

#include "Stack.h"template<class T1>
// 构造函数
Stack<T1>::Stack(int capacity):_a(new T1[capacity]),_capacity(capacity),_size(0)
{}
template<class T1>
// 插入函数
void Stack<T1>::Push(T1 data)
{_a[_size] = data;_size++;
}
// 析构函数
template<class T1>
Stack<T1>::~Stack()
{delete[]_a;_a = nullptr;_capacity = _size = 0;
}

⚠ 注意:类模板,是不支持,声明,定义,测试分开写的,会出现链接编译错误,如下:



解决方法:将测试和定义写在一起

这样就不会报错啦!

五、总结

  •  首先呢我们介绍了什么是【函数模版】,新学习了一个关键字叫做template,用它再配合模版参数就可以去定义出一个函数模版,有了它,我们在写一些相同类型函数的时候就无需去进行重复的CV操作了,在通过汇编观察函数模版的原理后,清楚了我们只需要传入不同的类型,此时模版参数就会去进行一个自动类型推导,从而产生不同的函数。函数模版定义好后还要对其实例化才能继续使用,但此时要注意的一点是如果传递进去的类型个数与模版参数的个数不匹配的话,其就无法完成自动类型推导,因为这会产生一个歧义。所以想要真正学好模版,这点是一定要搞清楚的!!!
  • 接下去呢我们又学习了【类模版】,没想到吧,类也可以变成一个模版,以Stack类为例,对于类模版而言,其类名和类型与普通类是不一样的,这点要注意了,尤其体现在类的成员函数放在类外进行定义的时候,也要将其定义为函数模版,函数名前面指明其类型,这才不会出问题。有了类模版之后,我们去显式实例化不同的数据类型后也可以让模版参数去做一个自动类型推导从而得到不同数据类型的栈

六、共勉 

     以下就是我对C++ 模板的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对C++STL库的理解,请持续关注我哦!!!   

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

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

相关文章

【STM32】TIM2的PWM:脉冲宽度调制--标准库

注意点&#xff1a; TIM_Period---->指要进行比较的值Compare TIM_Prescaler----> 指要进行分频的值【分频值/原始时钟值】 PWM是一种周期固定&#xff0c;脉宽可调整的输出波形。 https://www.cnblogs.com/brianblog/p/7117896.html 0.通用寄存器输出 1.捕获/比较通道…

面向萌新的技术博客入门指南

Python之禅 在Python的解释器中隐藏一个彩蛋&#xff0c;输入import this就会返回19条Python之禅&#xff0c;具体如下&#xff1a; import this The Zen of Python, by Tim Peters Python之禅 &#xff0c;by Tim Peters Beautiful is better than ugly. 优美好于丑陋&…

clouldcompare工具使用

文章目录 1.界面1.1 布局1.3 视觉显示方向1.4 放大镜1.5 建立旋转中心2.快速入门2.1 剪裁2.2 多点云拼接 1.界面 1.1 布局 参考&#xff1a;https://blog.csdn.net/lovely_yoshino/article/details/129595201 1.3 视觉显示方向 1.4 放大镜 1.5 建立旋转中心 2.快速入门 2.1 …

开源:特殊的垄断

免责声明&#xff1a;本博客旨在分享我对开源策略的理解和体会&#xff0c;不代表任何组织或机构的立场或观点&#xff0c;也不构成任何商业或投资的建议或担保。本博客的内容可能存在错误或遗漏&#xff0c;也可能随着时间的推移而变得过时或不适用。请在使用或依赖本博客的内…

open clip论文阅读摘要

看下open clip论文 Learning Transferable Visual Models From Natural Language Supervision These results suggest that the aggregate supervision accessible to modern pre-training methods within web-scale collections of text surpasses that of high-quality crowd…

记录--vue3 setup 中国省市区三级联动options最简洁写法,无需任何库

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 在写页面的时候&#xff0c;发现表单里面有一个省市区的 options 组件要写&#xff0c;因为表单很多地方都会用到这个地址选择&#xff0c;我便以为很简单嘛。 虽然很简单的一个功能&#xff0c;但是网…

如何使用ArcGIS Pro制作个性三维地形图

制作三维地图制作的多了&#xff0c;想着能不能换个“口味”&#xff0c;恰好看见制作六边形蜂窝图&#xff0c;灵光一闪&#xff0c;想着将二者结合&#xff0c;将平滑的三维地形图改成柱状图&#xff0c;从结果来看还可以&#xff0c;这里将制作方法分享给大家&#xff0c;希…

Leetcode2833. 距离原点最远的点

Every day a Leetcode 题目来源&#xff1a;2833. 距离原点最远的点 解法1&#xff1a;贪心 要使得到达的距离原点最远的点&#xff0c;就看 left 和 right 谁大&#xff0c;将 left 和 right 作为矢量相加&#xff0c;再往同方向加上 underline。 答案即为 abs(left - rig…

Windows 安装 Maven

目录 安装 JDK下载 Maven配置阿里云镜像配置环境变量 安装 JDK Windows 安装 JDK 下载 Maven 下载地址&#xff1a;https://maven.apache.org/download.cgi 下载 apache-maven-3.9.5-bin.zip 到本地解压到 D:\Software\apache-maven-3.9.5 配置阿里云镜像 配置阿里云远程仓…

docker更改存储目录原因及方案

为什么一定要将docker的存储目录挂载到其他目录 docker在安装时默认存储目录在/var/lib/docker&#xff0c;而该目录是在系统盘下的。docker安装后&#xff0c;会使用各种各样的镜像&#xff0c;动辄几个G&#xff0c;那么如此多的镜像文件&#xff0c;装着装着系统盘就撑爆了…

Halcon WPF 开发学习笔记(4):Halcon 锚点坐标打印

文章目录 专栏前言锚点二次开发添加回调函数辅助Model类 下集预告 专栏 Halcon开发 博客专栏 WPF/HALCON机器视觉合集 前言 Halcon控件C#开发是我们必须掌握的&#xff0c;因为只是单纯的引用脚本灵活性过低&#xff0c;我们要拥有Halcon辅助开发的能力 锚点开发是我们常用的…

【笔记】结合P02项目——maven继承与聚合

maven的继承关系 P02项目大概是这个样子&#xff0c;下图展示的是其父工程 父工程配置了parent依赖springb-boot-starter-parent&#xff0c;子工程配置其parant为父工程 子工程引用common子工程 maven的版本锁定 管理子工程的版本号问题 父工程添加dependencyManageMent…

P6入门:项目初始化3-项目详情之记事本Notebook

前言 使用项目详细信息查看和编辑有关所选项目的详细信息&#xff0c;在项目创建完成后&#xff0c;初始化项目是一项非常重要的工作&#xff0c;涉及需要设置的内容包括项目名&#xff0c;ID,责任人&#xff0c;日历&#xff0c;预算&#xff0c;资金&#xff0c;分类码等等&…

机器学习——实践

目录 一、数据集划分 1、交叉验证 2、不平衡数据的处理 代价敏感学习 二、评价指标 三、正则化、偏差和方差 为什么要标准化/归一化&#xff1f; 过拟合的处理——Dropout 过拟合的处理——Early stopping 过拟合的处理——数据增强 偏差和方差 ​编辑 一、数据集划分…

ida81输入密码验证算法分析以及破解思路

本文分析了ida81对输入密码的验证流程&#xff0c;分别对输入密码到生成解密密钥、密码素材的生成过程以及文件数据的加密过程这三个流程进行分析&#xff0c;并尝试找一些可利用的破绽。很遗憾&#xff0c;由于水平有限&#xff0c;目前也只是有个思路未能完全实现&#xff0c…

【C++】单例模式【两种实现方式】

目录 一、了解单例模式前的基础题 1、设计一个类&#xff0c;不能被拷贝 2、设计一个类&#xff0c;只能在堆上创建对象 3、设计一个类&#xff0c;只能在栈上创建对象 4、设计一个类&#xff0c;不能被继承 二、单例模式 1、单例模式的概念 2、单例模式的两种实现方式 …

20231112_DNS详解

DNS是实现域名与IP地址的映射。 1.映射图2.DNS查找顺序图3.DNS分类和地址4.如何清除缓存 1.映射图 图片来源于http://egonlin.com/。林海峰老师课件 2.DNS查找顺序图 3.DNS分类和地址 4.如何清除缓存

工业摄像机参数计算

在工业相机选型的时候有点懵&#xff0c;有一些参数都不知道咋计算的。有些概念也没有区分清楚。‘’ 靶面尺寸 CMOS 或者是 CCD 使用几分之几英寸来标注的时候&#xff0c;这个几分之几英寸计算的是什么尺寸&#xff1f; 一开始我以为这个计算的就是靶面的实际对角线的尺寸…

ASP.NETWeb开发(C#版)-day1-C#基础+实操

目录 .NET实操&#xff1a;创建项目执行 C#基础语法数据类型变量实操001_变量如何在一个解决方案 中创建另一个项目实操002结构实操003-if else实操004-多分支多行注释按钮实操&#xff1a;循环 面向对象基础如何在同一个项目下创建新的.cs文件实操-类的定义与访问实操-练习实操…

Qt 自定义按钮 区分点按与长按信号,适配触摸事件

Qt 自定义按钮 区分点按与长按信号 适配触摸事件 效果 使用示例 // 点按connect(ui.btnLeft, &JogButton::stepclicked, this, &MainWindow::btnLeft_clicked);// 长按开始connect(ui.btnLeft, &JogButton::continueOn, this, &MainWindow::slotJogLeftOn);//…