【C++】泛型编程 | 函数模板 | 类模板

news2025/1/22 9:12:33

一、泛型编程

泛型编程是啥?

编写一种一般化的、可通用的算法出来,是代码复用的一种手段。

类似写一个模板出来,不同的情况,我们都可以往这个模板上去套。

举个例子:

void Swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
int main()
{
    int a = 1, b = 2;
    Swap(a, b);
    cout << a << b<<endl;
    return 0;
}

这是一个交换函数。如果很多不同类型的数据需要交换,咋办?

🤔函数重载?

函数重载的确可以解决,但是每多一种数据,都要实现对应的重载函数。实在太麻烦了。

我们想要的是:有一个一般化的模板,不管是什么类型,往这个模板函数上套用就行。这就是泛型编程的思想。

当用上泛型编程:

template<typename T>
void Swap(T& a, T& b)
{
    T tmp = a;
    a = b;
    b = tmp;
}
int main()
{
    char a = 'a', b = 'b';
    Swap(a, b);
    cout << a << b<<endl;
    int c = 1, d = 2;
    Swap(c, d);
    cout << c << d << endl;
​
    return 0;
}

结果:

 

接下来我们具体介绍如何使用泛型编程。

二、函数模板

泛型编程思想下得到的函数,就像是过了模具得到的。这些“模具”,被称作函数模板。

函数模板不是一个函数,而是一个模板。

函数模板的参数是一个模板,可以包含多个类型,返回值也是一个模板,可以包含多个类型。

格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}

注:

1.typename是用来定义模板参数关键字,也可以使用class。

2.tyname后面的类型名可以自己定,我们常常取为T(type)、Ty、K、V等,一般是大写字母or单词首字母大写。

运用起来:

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

template<class T>
……

原理

❓Q:这俩调用的是同一个Swap函数吗?

char a = 'a', b = 'b';
Swap(a, b);
    
int c = 1, d = 2;
Swap(c, d);

不是的!在函数栈帧里,要给形参开空间。这俩形参的类型不一样,自然不会是同一块空间。

不信我们看看汇编代码:

 

可见,调用的是两个不同的函数。

实际上,编译器会根据传入的实参类型来推演,把函数模板中的 T 换成相应类型,从而生成对应的函数。

如:当用double类型使用函数模板时,编译器会通过推演,将T替换成double,然后产生一份专门处理double类型的代码。

如果是int,那通过推演、替换,生成一份处理int的代码。

所以,在使用函数模板时,编译常常会慢上一点,因为它正在后台默默处理这些工作。

如图:

函数模板的实例化

什么叫”函数模板的实例化“?

是指:在调用函数模板时,根据传递的实参推导出函数模板的具体实现,生成一个特定类型的函数。

模板参数实例化分为:隐式实例化 和 显式实例化。

隐式实例化

隐式实例化,就是不指定类型,让编译器 自己去推演 模板参数的类型。

例:

template<class T>
T Add(const T& a, const T& b)
    return a + b;
}
int main()
{
    cout << Add(1, 2) << endl;      
    cout << Add(1.1, 2.0) << endl;  
​
    return 0;
}

这里补充一个点(可跳过不看):

当Add的实参是数字时,那一定要加const修饰形参。如果实参是变量,const就不是一定要加。

这样写会报错:

template<class T>
T Add( T& a,  T& b)     //不加const会报错
    return a + b;
}
int main()
{
    cout << Add(1, 2) << endl;    //当实参是数字
    cout << Add(1.1, 2.0) << endl;  
​
    return 0;
}

这是因为:编译器会做一个强校验,当实参是数字时,它本身就是不能被修改的,此时必须加const才能通过编译。

如果这样写,加const就只是锦上添花,不是必须要的:

template<class T>
T Add( T& a,  T& b)   //不加const也能通过编译
{
    return a + b;
}
int main()
{
    int a = 1, b = 2;
    cout << Add(a, b) << endl;  //当实参是变量
​
​
    return 0;
}

然而,下面这种情况却编译不通过:

 cout << Add(1.1, 2) << endl;   

 

这是因为:编译器根据实参1.1将 T推为double,根据实参2又将 T推为int,这样T就不知道自己到底是int还是double,矛盾了。

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

用强制转换的方式:

template<class T>
T Add(const T& a, const T& b)  
{
    return a + b;
}
int main()
{
    cout << Add(1, (int)2.1) << endl;      
    cout << Add((double)1, 2.1) << endl;
​
    return 0;
}

用显示实例化的方式:

cout << Add<int>(1, 2.1) << endl;   //指定实例化成int类型
​
cout << Add<double>(1, 2.1) << endl;   //指定实例化成double类型

显式实例化

显示实例化,就是显示地指定函数模板的实参,从而生成一个特定类型的函数。

格式:在函数名后的<>中指定模板参数的实际类型。

函数名 <类型> (参数列表);

如:

int main(void)
{
 int a = 10;
 double b = 20.1;
 
 // 显式实例化
 Add<int>(a, b);
 return 0;
}

如果类型不匹配,如b是bouble类型,但实例化类型指定为int,此时 编译器会尝试进行隐式类型转换。

如果转换失败 编译器将会报错。

模板参数的匹配原则

➡️1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。

int Add(int a, int b)
{
    return a + b;
}
​
template<class T>
T Add(const T& a, const T& b)
{
    return a + b;
}
int main()
{
    cout << Add(1,2) << endl;   //调用非模板函数
    cout << Add<int>(1,2) << endl;   //调用 模板显示实例化出的函数
    return 0;
}

来看看怎么调用的:

❓为什么两者调用的函数不同呢?

当已经有现成的,专门处理int的函数Add存在时,Add(1,2)会优先调用现成的,这样效率更高,省去了模板实例化的时间。

而下面的Add<int>(1,2),则指定了编译器去显示实例化模板,生成int类型的Add函数。

➡️2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数。

如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。

例:

int Add(int a, int b)
{
    return a + b;
}
​
template<class T1,class T2>
T1 Add(const T1& a, const T2& b)
{
    return a + b;
}
int main()
{
    cout << Add(1,2) << endl;  
    cout << Add(1,2.0) << endl;   //模板会隐式实例化,使T1为int,T2为double,更匹配
    return 0;
}

来看看怎么调用的:

三、类模板

其实相比函数模板,后面用到 类模板的场景要更多。

为什么需要类模板

在没有类模板的时代,我们用typedef。typedef的问题有哪些呢?

typedef int STDateType;
class Stack
{
private:
    STDateType* _a;
    int _top;
    int _capacity;
};
int main()
{
    Stack s1;  //s1是int类型
    Stack s2;   //s2是char类型
    return 0;
}

如上,如果我们想要s1是int而s2是char,咋整?

解决办法:将int重命名为STDateType1,char重命名为STDateType2:

typedef int STDateType1;
typedef char STDateType2;
class Stack
{
private:
    STDateType1* _a;
    int _top;
    int _capacity;
};
class Stack
{
private:
    STDateType2* _a;  
    int _top;             
    int _capacity;
};
int main()
{
    Stack s1;  //s1是int类型
    Stack s2;   //s2是char类型
    return 0;
}

两段Stack代码重复度极高,可见这个办法很多余。这暴露了typedef所不能解决的问题。

这种场景下,就需要用到类模板了。

类模板

格式:

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

实例化:

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可。

 // Vector类名,Vector<int>才是类型
 Vector<int> s1;
 Vector<double> s2;

注意,类模板名字不是真正的类,而实例化的结果才是真正的类。

例:

Stack<int> s1;  //s1是int类型
Stack<char> s2;   //s2是char类型

这种场景下,编译器会根据<int>、<char>分别生成对应的class Stack{};。

所以说,即使用了模板,T为 int和char时,依旧是两种不同的类,只不过不由我们手动实现,而是交给编译器去做了。

补充说明:

模板不支持分离编译,不能把声明写在.h文件,定义写在.cpp文件中。如果非要分离的话,模板是支持写在同一个文件里的。

可以把声明留在类里,定义写在类外(同一个文件里)。

用下面的例子展示下 声明与定义分离的写法:

class Stack
{
public:
    void Push(const T& x);  //声明写在类里
private:
    T* _a;  
    int _top;             
    int _capacity;
};
​
//定义在类外
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template<typename T>
void Stack<T>::Push(const T& x)
{
    if (_top == _capacity)
    {
        ……
    }
    _a[_top] = x;
    _top++;
}

当然,都写在类里面也是可以的。

应用类模板

回到刚刚的那种场景,我们用类模板处理一下:

template<typename T>
class Stack
{
public:
    Stack(int capacity = 4)  
        :_a(nullptr)
        , _top(0)  
        , _capacity(0)   
    {
        if (capacity > 0) 
        {
            _a = new T[capacity];  
            _capacity = capacity;
            _top = 0;
        }
    }
private:
    T* _a;          //用模板,不同的类型都可以套
    int _top;             
    int _capacity;
};
int main()
{
    Stack<int> s1;  //s1是int类型
    Stack<char> s2;   //s2是char类型
    return 0;
}

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

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

相关文章

智能驾驶感知技术的综述与展望

目录 1 智能驾驶环境感知的目的 1.1 智能驾驶感知技术的定义和作用 1.2 基于传感器的智能驾驶感知技术 1.3 基于深度学习的智能驾驶感知技术 2 环境感知的手段与方法 2.1 感知技术在智能驾驶中的应用与发展 2.2 智能驾驶决策系统的设计与优化 2.3 控制技术在智能驾驶中的应…

SpringMVC之JSON返回异常处理机制

json处理统一异常处理 1.json处理 //pom.xml <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"…

加快项目开发进度常用5种方法

项目进度管理是根据进度目标&#xff0c;制定合理的进度计划&#xff0c;全程监控项目进度的执行情况。这样有利于明确项目目标&#xff0c;协调团队行动&#xff0c;提高开发效率&#xff0c;从而最大化项目利益。而加快项目进度&#xff0c;有利于提高项目整体效率&#xff0…

我的创作纪念日——1个普通网安人的漫谈

机缘 大家好&#xff0c;我是zangcc。今天突然收到了一条私信&#xff0c;才发现来csdn已经1024天了&#xff0c;不知不觉都搞安全渗透2年半多了&#x1f414;&#xff0c;真是光阴似箭。 我写博客的初衷只是记录自己的学习历程&#xff0c;比如打打靶场&#xff0c;写一下通关…

高成本获客时代,企业如何通过营销自动化实现突围?

在数字化时代&#xff0c;随着市场竞争的不断升级&#xff0c;企业在获客方面面临了前所未有的挑战。不论是B端或C端的市场和运营部门纷纷寻求可降低获客成本的新运营路径&#xff0c;将有限的预算花在刀刃上。 企业迫切需要寻求更加智能和高效的方式来吸引、转化和留住潜在客…

QGC 参数设置中关于param_union的使用

在QGC中在设置飞控参数中使用到了union,此处关于param_uinon使用的理解进行记录。 参数上传 在参数上传中根据参数类型将参数存储在param_union对应的类型中&#xff0c;但是上传参数时&#xff0c;上传的是param_float类型&#xff0c;所以在飞控端接收时&#xff0c;需要使…

JSON与实体类之间的互相转换!!

一、意义 在我们调用三方平台接口时&#xff0c;经常需要将我们封装的实体类转换为json作为传参&#xff0c;或者是当我们接收报文时接收的为json数据想要转换为我们自己封装的实体类。 1实体类转JSON public static void main(String[] args) throws JsonProcessingExceptio…

“深入理解SpringMVC的JSON数据返回和异常处理机制“

目录 引言1. SpringMVC之JSON数据返回1.1 导入依赖1.2 配置弹簧-MVC.xml1.3 ResponseBody注解使用1.4.Jackson 2. 异常处理机制2.1 为什么要全局异常处理2.2 异常处理思路2.3 SpringMVC异常分类2.4 综合案例 总结 引言 在现代Web开发中&#xff0c;SpringMVC是一个广泛使用的框…

山东EV多域名SSL证书可以保护几个域名

EV多域名SSL证书是一种特殊的SSL证书&#xff0c;可以在一个物理服务器上为多个不同的域名同时部署SSL证书。这种证书特别适合于需要同时保护多个域名的网站&#xff0c;不仅可以提高网站的安全性&#xff0c;还可以提高网站的可信度。今天就随SSL盾小编了解EV多域名SSL证书。 …

打家劫舍 III

题目链接 打家劫舍 III 题目描述 注意点 如果 两个直接相连的房子在同一天晚上被打劫 &#xff0c;房屋将自动报警返回 在不触动警报的情况下 &#xff0c;小偷能够盗取的最高金额 解答思路 记忆化 解决重复子问题解决本题&#xff0c;在任意一个位置&#xff0c;小偷可以…

Unity 轮播图

核心脚本 using UnityEngine; using UnityEngine.UI; using System.IO; using DG.Tweening;public class ImageSlider : MonoBehaviour {public RawImage mainImage; // 中间显示的主要图片public RawImage leftImage; // 左侧辅助图片public RawImage rightImage; // 右侧辅助…

【工艺盘点】新能源行业废水除钴的工艺盘点

钴是一种稀有金属&#xff0c;也是非常重要的过渡金属材料&#xff0c;因其优异的物理、化学性质&#xff0c;以化学品和金属的形式&#xff0c;广泛应用于锂电池、硬质合金、超耐热合金、绝缘材料和磁性材料、工业催化剂、染料及氧化钴的生产过程中。 钴可以提高锂离子电池的稳…

【Flink实战】新老用户分析:按照操作系统维度进行新老用户的分析

&#x1f680; 作者 &#xff1a;“大数据小禅” &#x1f680; 文章简介 &#xff1a;新老用户分析&#xff1a;按照操作系统维度进行新老用户的分析 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 目录导航 同类产品参考日志的数据格式需求&…

vue 检查指定路由是否存在

今天路由跳转报错了 RangeError: Maximum call stack size exceeded 但显然 我的代码只有一个简单的路由跳转 并没有很大的的堆栈数据操作 所以 我就联想到了 会不会是因为路由不存在 我们可以通过 console.log(this.$router.options.routes)输出整个路由对象类看一下 或者…

HttpServletRequest是什么,有什么作用

HttpServletRequest是Java Servlet规范中定义的一个接口&#xff0c;它提供了与HTTP请求相关的方法和属性。在Java Web应用程序中&#xff0c;当客户端发送HTTP请求时&#xff0c;容器&#xff08;例如Tomcat&#xff09;会创建一个HttpServletRequest对象&#xff0c;该对象包…

华为云云耀云服务器L实例评测|搭建WordPress网站

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 在当今数字化时代&#…

云养殖模式:让养殖业走向智慧化、高效化、绿色化

养殖业是我国农业的重要组成部分&#xff0c;也是农民增收的重要来源。然而&#xff0c;传统的养殖方式存在着许多问题&#xff0c;如水环境污染、病害频发、市场风险高、管理落后等&#xff0c;导致养殖效益低下&#xff0c;难以适应现代消费者的需求。如何改变这种局面&#…

90%测试人不知道的快速入门秘籍——接口自动化神器 apin(一)

一、apin 介绍及安装 1、什么是 apin apin是柠檬班——木森老师开发的一个无需写代码、就可以进行接口自动化测试的框架。 只需要通过json或yaml文件编写非常简洁的用例数据&#xff0c;即可完成接口自动化测试&#xff0c;并生成自动化测试报告。 apin旨在帮助更多不会写代…

IPIDEA代理-如何解决使用代理IP后网速变慢的问题

代理IP是一种常见的网络技术手段&#xff0c;它可以被广泛应用于数据采集与分析、网络营销、舆情监测、SEO等诸多场景&#xff0c;帮助用户获取更多的网络资源&#xff0c;提高采集效率。但是&#xff0c;使用代理IP也会带来一些问题&#xff0c;其中最常见的就是网速变慢。在本…

IDEA(2023)修改默认缓存目录

&#x1f607;作者介绍&#xff1a;一个有梦想、有理想、有目标的&#xff0c;且渴望能够学有所成的追梦人。 &#x1f386;学习格言&#xff1a;不读书的人,思想就会停止。——狄德罗 ⛪️个人主页&#xff1a;进入博主主页 &#x1f5fc;专栏系列&#xff1a;无 &#x1f33c…