【C++从0到王者】第二十站:模板进阶

文章目录

  • 前言
  • 一、typename 和 class的一些区别
  • 二、非类型模板参数
    • 1.非类型模板参数介绍
    • 2.array容器
  • 三、模板的特化
    • 1.函数模板的特化
    • 2.类模板的特化
      • 1.全特化
      • 2.偏特化(半特化)
  • 三、模板的分离编译
  • 四、总结


前言

在前面我们使用模板主要是为了解决两类问题。一类是解决类里面某个数据类型,可以使用模板。 第二类就不单单是控制某种数据类型,而是控制某种逻辑,比如我们的适配器模式:传一个正向迭代器,可以适配出反向迭代器。传一个普通的容器,可以适配出栈、队列、优先级队列等。这样的好处就是我们的栈不是死的。并不单单只是一个链式栈、或者顺序栈等等,或者传一个类型过去,这个类型可以仿造函数,即仿函数,一般这个类也就是一个普通的类,只不过其重载了()运算符,导致其生成的对象可以像函数一样进行调用。它可以控制sort的升序或降序,堆的大小堆

一、typename 和 class的一些区别

typename和class在绝大多数场景下都是没有区别的,但是在一些场景下还是存在一些区别的。
如下代码所示:

在我们想要写一个打印vector里面的数据的时候,我们会写出如下代码。

#include<iostream>
#include<vector>
#include<list>
using namespace std;void Print(const vector<int>& v)
{vector<int>::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}cout << endl;
}
int main()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);for (auto e : v){cout << e << " ";}cout << endl;Print(v);return 0;
}

确实上面方法挺好用的,但是我们这个Print是否可以利用模板往泛型去写呢?答案当然是可以的,于是我们可能就会写出这样的代码,结果当我们运行的时候,报错了。

template<class Container>
void Print(const Container& v)
{Container::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}cout << endl;
}

它的报错是这样的,提示说要在Container前加上typename
在这里插入图片描述
于是我们按照它的错误信息进行改成,代码就通过了

template<class Container>
void Print(const Container& v)
{typename Container::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}cout << endl;
}

在这里插入图片描述

那么为什么必须要加上typename呢?

这是因为编译器在编译的时候从上往下,在编译到这里的时候这里还没有Container实例化,那么此时编译器就区分不清楚Container是什么类型,之前是vector<int>的时候它以及被实例化出来了,所以不会报错。vector的话编译器就很清楚在vector里面找到这个迭代器类型即可。而现在Container没有实例化,那么此时就会出问题,就有两种可能性:一种可能就是这里是一个静态成员变量,一种就是类里面进行typedef出来的类型。也就是说,这里到底是类型还是静态成员变量是无法区分的。所以编译器要求加上typename告诉它这里是一个类型,说明这里是合乎语法的。等模板实例化以后再去找

在我们之前优先级队列里面其实也用到了typename
在这里插入图片描述

二、非类型模板参数

1.非类型模板参数介绍

有时候,我们需要一些不是类型的模板参数

比如在我们想要写一个静态栈的时候,我们之前需要将N进行一个宏定义,然后再类里面之间使用,现在我们可以使用非类型模板参数,直接传递一个N过去,从而修改N的容量

template<class T,size_t N>
class Stack
{
private:T _a[N];int _top;
};
int main()
{Stack<int, 10> st1;Stack<int, 100> st2;return 0;
}

要注意这里的N是一个常量,不可以被修改的。否则报错。

非类型模板参数必须满足以下两点

  1. 必须是常量
  2. 必须是整型

2.array容器

在这里插入图片描述

数组是固定大小的序列容器:它们按照严格的线性顺序保存特定数量的元素。

在内部,数组不保留它所包含的元素以外的任何数据(甚至不保留它的大小,这是一个模板参数,在编译时固定)。就存储大小而言,它与使用该语言的括号语法([])声明的普通数组一样有效。这个类只是给它添加了一层成员函数和全局函数,这样数组就可以用作标准容器。

与其他标准容器不同,数组具有固定的大小,并且不通过分配器管理其元素的分配:它们是封装固定大小的元素数组的聚合类型。因此,它们不能动态地展开或收缩(有关可以展开的类似容器,请参阅vector)。

大小为零的数组是有效的,但它们不应该被解引用(成员front、back和data)。

与标准库中的其他容器不同,交换两个数组容器是一个线性操作,涉及单独交换范围内的所有元素,这通常是一个效率相当低的操作。另一方面,这允许两个容器中的元素的迭代器保持它们原来的容器关联。

数组容器的另一个独特特性是它们可以被视为元组对象:头重载get函数以访问数组的元素,就像它是一个元组一样,以及专门的tuple_size和tuple_element类型。

如上是关于这个容器的介绍,它就是采用了非类型模板参数,它支持的操作有下面这些
在这里插入图片描述

其实本质就是多加了一层函数。

array和普通的数组本质上没有太大区别,要说唯一的区别就是,对于越界的检查更加严格了。对越界读写都有检查,而普通数组不能检查越界读,少部分越界写可以检查。

三、模板的特化

1.函数模板的特化

有时候我们会遇到这样的场景

template<class T>
bool Less(T a, T b)
{return a < b;
}
int main()
{int a = 2;int b = 1;cout << Less(a, b) << endl;cout << Less(&a, &b) << endl;return 0;
}

我们期望说,比较的时候即便是指针,也能比较里面的值,但是此时我们这里比较的是两个指针的大小
在这里插入图片描述

为了达到我们的期望,我们可以有多种方法进行处理
如下面就是使用了模板的特化

当遇到int*类型的时候,就走的是特化

template<class T>
bool Less(T a, T b)
{return a < b;
}
template<>
bool Less<int*>(int* a, int* b)
{return *a < *b;
}
int main()
{int a = 2;int b = 1;cout << Less(a, b) << endl;cout << Less(&a, &b) << endl;return 0;
}

但是实际上,这样写特化不如直接就是一个函数重载更加来的方便
在这里插入图片描述

函数函数调用是有现成的就用现成的,没有现成的才用模板。

但是像下面这种情况就必须使用模板的特化了
在这里插入图片描述

即我们有时候还需要特化其他类型。就必须使用模板的特化来的更加方便

2.类模板的特化

1.全特化

如下所示,就是对Date类的特化。它的步骤也是一样的,需要对某种类型进行特殊处理。于是我们就写一个template<> ,然后比之前的Date多一个类型。这样我们就可以对某一类型特殊处理了

template<class T1, class T2>
class Date
{
public:Date(){cout << "Date<T1, T2>" << endl;}
private:T1 _d1;T2 _d2;
};//对上面的类进行特化
template<>
class Date<int, double>
{
public:Date(){cout << "Date<int, double>" << endl;}
private:int _d1;double _d2;
};
int main()
{Date<int, int> d1;Date<int, double> d2;return 0;
}

运行结果如下所示

在这里插入图片描述

有了类了特化,这样我们之前的优先级队列就可以更加完善了。我们之前优先级队列的时候我们本身期望传入指针的时候而言按照指向的内容去比较。之前我们是直接替换了比较类,现在我们可以使用类的特化对前面进行加以修改

在这里插入图片描述

这样一来我们就可以进行正常比较了。(注意我们这里使用了域作用限定符,不然我们的就命名冲突了,会出事的)
在这里插入图片描述

像以上这些特化必须得有原模板以后才可以进行特化,向上面这种特化,将原来全部的模板参数给特化,这种特化也被称之为全特化

2.偏特化(半特化)

顾名思义,偏特化就是只特化一部分模板参数

//偏特化
template<class T1>
class Date<T1, double>
{
public:Date(){cout << "Date<T1, double>" << endl;}
private:T1 _d1;double _d2;
};

如上代码所示,我们还是对前面的Date类进行特化,这次我们只特化一个参数,那么此时称之为半特化或偏特化
在这里插入图片描述

上面的偏特化的作用就是部分特化。这是偏特化的一种形式

偏特化其实有两种形式:

  1. 对模板参数做类表的一部分参数特化,即部分特化
  2. 参数的更进一步限制,即偏特化不仅仅指特化部分参数,而是针对模板参数的更进一步的条件限制所设计出来的一个特化版本

针对第二点,如下就是第二种特化形式

template<class T1, class T2>
class Date<T1*, T2*>
{
public:Date(){cout << "Date<T1*, T2*>" << endl;}
};

在这里插入图片描述

有了偏特化的第二种形式的思想的,我们可以将前面优先级队列中的仿函数再次修改,只要是指针类型的,都进行特化

	template<class T>class less{public:bool operator()(const T& x, const T& y){return x < y;}};template<class T>class less<T*>{public:bool operator()(const T* x, const T* y){return *x < *y;}};template<class T>class greater{public:bool operator()(const T& x, const T& y){return x > y;}};template<class T>class greater<T*>{public:bool operator()(const T* x, const T* y){return *x > *y;}};

在这里插入图片描述

除了对指针类型的限制,还可以是对引用的限制,引用和指针混在一起的特化,以下是演示

template<class T1, class T2>
class Date<T1&, T2&>
{
public:Date(){cout << "Date<T1&, T2&>" << endl;}
};
template<class T1, class T2>
class Date<T1&, T2*>
{
public:Date(){cout << "Date<T1&, T2*>" << endl;}
};
template<class T1, class T2>
class Date<T1*&, T2*>
{
public:Date(){cout << "Date<T1*&, T2*>" << endl;}
};

在这里插入图片描述

三、模板的分离编译

我们之前在C语言的时候特别喜欢声明和定义分类。在C++中,当我们试着分离的时候
在这里插入图片描述
编译器报错了报的是一个链接错误
在这里插入图片描述

但是如果调用size等接口的话,模板又正常了。不报错误。

可而得知,是类成员函数的声明和定义分离时出现的链接错误。即没有找到这个函数的地址。

这种错误就类似于我们定义了一个类,这个类是如下进行定义的,一个类声明了两个函数,但是只实现了一个函数。另外一个函数没有被实现。
在这里插入图片描述

于是此时我们的func2函数在调用的时候就会报错,且错误类型还是一样的。链接错误,即找不到地址。
在这里插入图片描述

这里其实就涉及到我们的编译链接过程了。在test.c文件中,对于stack类,它的其他成员函数在编译的时候就已经找到地址了。而push和pop都只有声明,在编译阶段都是没有地址的。

在编译阶段虽然他们没有地址,但是由于有声明,相当于一种承诺。所以自然不会报错

编译阶段只看声明, 声明是一种承诺,所以编译检查声明函数参数返回可以对上,等着链接的时候,拿着修饰后的函数去其他文件符号表查找

到了链接阶段我们此时的现象是

  1. func1链接查到了
  2. func2链接没有查到。因为func2没有定义
  3. push链接查不到,但是我们的push定义了

那么为什么会出现第三中情况的,我们究其原因,是因为他们是分别编译的。stack.o文件就没有生成地址,因为压根就不知道这个T是什么类型的,就没办法去生成地址。没法实例化
在这里插入图片描述

那么如何解决呢?其实我们可以显式实例化,即我们直接在函数是实现中,写一个template,注意不要带尖括号,然后class stack<int>即可
在这里插入图片描述

但是这里还是存在一些问题的,因为治标不治本,如果我们在主函数中又用一个double类型的,那么又要添加一个显式实例化。

namespace Sim 
{template<class T ,class Container>void stack<T,Container>::push(const T& val){_con.push_back(val);}template<class T, class Container>void stack<T,Container>::pop(){_con.pop_back();}templateclass stack<int>;templateclass stack<double>;
}

模板的声明和定义如果通过分文件的方式,显然是不太合适的。我们如果要将其分类,可以在同一个文件内进行分离。

这样是由于test文件是知道模板要实例化为什么类型的,所以就不用进行显式实例化了

namespace Sim
{template<class T, class Container = deque<T>>class stack{public:void push(const T& val);void pop();const T& top(){return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};template<class T, class Container>void stack<T, Container>::push(const T& val){_con.push_back(val);}template<class T, class Container>void stack<T, Container>::pop(){_con.pop_back();}};

即便是stl库里面,也是这样做的,小函数定义在类里面,大函数定义在类外面,但是声明和定义分离是放在同一个文件的。

有时候我们会看见这些模板的库的后缀是.hpp,意思就是声明和定义放在一个文件中,这只是一个名字的暗示。

四、总结

【优点】

  1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
  2. 增强了代码的灵活性

【缺陷】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

好了本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!
有任何关于文章的问题,可以直接私信我或者评论区留言哦!!!

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

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

相关文章

Kubernetes(K8s)入门

一、Kubernetes是什么 Kubernetes是什么? 首先&#xff0c;它是一个全新的基于容器技术的分布式架构领先方案。这个方案虽然还很新&#xff0c;但它是谷歌十几年以来大规模应用容器技术的经验积累和升华的一个重要成果。确切地说&#xff0c;Kubernetes是谷歌严格保密十几年的…

湘大 XTU OJ 1097 排序 题解:c++ 函数库的使用 快速排序 归并排序 冒泡排序

一、链接 1097 排序 二、题目 Description N个整数&#xff0c;将其排序输出。 输入 第一行是一个整数K&#xff08;1<K<20&#xff09;&#xff0c;表示有多少个样例&#xff0c;每个样例的第一行是一个整数N&#xff08;1<N<1,000&#xff09;和一个字符X&…

【Nginx】Nginx的优化和防盗链

nginx版本迭代比较快 *工作中&#xff0c;在发版期&#xff0c;通常先备份文件并备注时间&#xff0c;方便后期故障后回档 例&#xff1a; cp nginx.conf nginx.conf.bak.2023.0805 隐藏版本号的两种方法*** 1.修改配置文件 vim /usr/local/nginx/conf/nginx.conf 在http模…

孤立随机森林(Isolation Forest)(Python实现)

目录 1 简介 2 孤立随机森林算法 2.1 算法概述 2.2 原理介绍 2.3 算法步骤 3 参数讲解 4 Python代码实现 5 结果 1 简介 孤立森林&#xff08;isolation Forest&#xff09;是一种高效的异常检测算法&#xff0c;它和随机森林类似&#xff0c;但每次选择划分属性和划…

【JavaEE】Spring Boot - 日志文件

【JavaEE】Spring Boot 开发要点总结&#xff08;3&#xff09; 文章目录 【JavaEE】Spring Boot 开发要点总结&#xff08;3&#xff09;1. 日志有什么作用2. 日志格式2.1 日志框架原理 3. 日志的打印3.1 System.out.println3.2 使用日志框架3.3 日志级别3.3.1 设置默认日志显…

一文盘点 Zebec 生态的几个利好预期

Zebec Protocol 是目前商业进展最快的流支付体系&#xff0c;也是推动流支付向 Web2 世界发展的主要生态。目前&#xff0c;其已经与包括 Visa、Master 等支付巨头展开了合作&#xff0c;以推出银行卡的方式进一步向金融发达地区推出 Zebec Card 以拓展业务&#xff0c;前不久其…

Flutter父宽度自适应子控件的宽度

需求&#xff1a; 控件随着金币进行自适应宽度 image.png 步骤&#xff1a; 1、Container不设置宽度&#xff0c;需要设置约束padding&#xff1b; 2、文本使用Flexible形式&#xff1b; Container(height: 24.dp,padding: EdgeInsetsDirectional.only(start: 8.dp, end: 5.d…

【算法学习】高级班九

这种互为旋变串&#xff1a; 给定两个字符串&#xff0c;判断是否互为旋变串 代码&#xff1a; 打表法&#xff1a; 每一层内的数字不互相依赖&#xff0c;只依赖它下面的层但实际上size会约束L1和L2的值&#xff0c;即L1和L2<N-size 思路&#xff1a;设置一个窗口…

【论文阅读】NoDoze:使用自动来源分类对抗威胁警报疲劳(NDSS-2019)

NODOZE: Combatting Threat Alert Fatigue with Automated Provenance Triage 伊利诺伊大学芝加哥分校 Hassan W U, Guo S, Li D, et al. Nodoze: Combatting threat alert fatigue with automated provenance triage[C]//network and distributed systems security symposium.…

面试热题(二叉树的锯齿形层次遍历)

给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行下一层遍历&#xff0c;以此类推&#xff0c;层与层之间交替进行&#xff09; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3…

前端面试自我介绍

前端面试自我介绍精选篇1 各位面试官大家好&#xff0c;我叫__&#xff0c;就读于__大学__学&#xff0c;大学本科学历&#xff0c;我的求职意向是与金融专业相关的职位&#xff0c;本人拥有较强的学习能力&#xff0c;能快速适应工作环境&#xff0c;兴趣爱好广泛&#xff0c…

基于k8s job设计与实现CI/CD系统

方案一&#xff1a;Jenkinsk8sCICD 方案二&#xff1a;kanikok8s jobCICD CICD 基于K8s Job设计流水线 CI方案 工具镜像 云原生镜像打包工具 kaniko的使用 与Jenkins对比 可用性与易用性

20230811导出Redmi Note12Pro 5G手机的录音机APP的录音

20230811导出Redmi Note12Pro 5G手机的录音机APP的录音 2023/8/11 10:54 redmi note12 pro 录音文件 位置 貌似必须导出录音&#xff0c;录音的源文件不知道存储到哪里了&#xff01; 参考资料&#xff1a; https://jingyan.baidu.com/article/b87fe19e9aa79b1319356842.html 红…

cesium学习记录07-实体(Entity)

在学习记录05中&#xff0c;我们将了如何在 Cesium 中加载各种数据&#xff0c;包括矢量数据、影像图层、地形和 3D 模型。这些数据为我们构建了一个基础的场景和背景。特别是在加载 3D 模型时&#xff0c;我们采用了 viewer.scene.primitives.add 方法将模型作为一个原始对象添…

Chapter 12: Regular expressions | Python for Everybody 讲义笔记_En

文章目录 Python for Everybody课程简介Regular ExpressionsRegular ExpressionsCharacter matching in regular expressionsExtracting data using regular expressionsCombining searching and extractingEscape characterSummaryBonus section for Unix / Linux usersDebugg…

智安网络|恶意软件在网络安全中的危害与应对策略

恶意软件是指一类具有恶意目的的软件程序&#xff0c;恶意软件是网络安全领域中的一个严重威胁&#xff0c;给个人用户、企业和整个网络生态带来巨大的危害。通过潜伏于合法软件、邮件附件、下载链接等途径传播&#xff0c;破坏用户计算机系统、窃取敏感信息、进行勒索等不法行…

商城-学习整理-基础-库存系统(八)

一、整合ware服务 1、配置注册中心 2、配置配置中心 3、配置网关&#xff0c;重启网关 二、仓库维护 http://localhost:8001/#/ware-wareinfo 在前端项目module中创建ware文件夹保存仓库系统的代码。 将生成的wareinfo.vue文件拷贝到项目中。 根据功能&#xff0c;修改后台接…

PHP最简单自定义自己的框架view使用引入smarty(8)--自定义的框架完成

1、实现效果。引入smarty&#xff0c; 实现assign和 display 2、下载smarty&#xff0c;创建缓存目录cache和扩展extend 点击下面查看具体下载使用&#xff0c;下载改名后放到extend PHP之Smarty使用以及框架display和assign原理_PHP隔壁老王邻居的博客-CSDN博客 3、当前控…

Mysql - 配置Mysql主从复制-keepalived高可用-读写分离集群

目录 高可用&#xff1a; 为什么需要高可用呢&#xff1f; 高可用的主要作用&#xff1a; keepalived是什么&#xff1f;它用在哪里&#xff1f; 什么是VRRP协议&#xff0c;它的作用是什么&#xff1f; 搭建一个基于keepalived的高可用Mysql主从复制读写分离集群 一、项…

html实现iphone同款开关

一、背景 想实现一个开关的按钮&#xff0c;来触发一些操作&#xff0c;网上找了总感觉看着别扭&#xff0c;忽然想到iphone的开关挺好&#xff0c;搞一个 二、代码实现 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8&qu…