[C++]: 模板进阶

news2024/9/28 9:31:41

标题:[C++]: 模板进阶

@水墨不写bug



目录

一、非类型模板参数

(1)、非类型模板参数简介 

 (2)、非类型模板参数实例

二、模板的特化

(1)函数模板特化

(2)类模板特化

三、模板的分离编译


正文开始:

一、非类型模板参数


(1)、非类型模板参数简介 

        在模板初阶中,我们讲解了一般我们使用模板的做法:

//函数模板
template<class T1,class T2>
void func(T1 t1,T2,t2)
{
    //......
}

//类模板
template<class T>
class A
{
public:
    //......

private:
    T t;
}

        模板在如下场景中的使用会让你感到更加方便:

        如果我们要实现一个栈,在通常情况下我们可能会选择实现一个静态的栈,它的大小是固定的,比如:

typedef N 100

template<class T>
class stack
{

private:
    T _data[N];
}

         但是我们发现这样实现的栈的局限性很大,因为一旦确定它的大小,就无法改变了。如果我们想要在实例化的时候能够自己手动确定它的大小,就需要用到非类型模板参数;

        模板参数的类型分为:类型模板形参与非类型模板形参。

类型模板形参:出现在模板的参数列表中,跟在class或者typename之后;

非类型模板形参:就是用一个常量作为类(函数)模板的一个参数,在实例化的时候确定,在模板内部可以将该参数作为常量来使用。

在使用非类型模板形参之后,我们可以这样定义模板:


template<class T, int N>
class stack
{
public:

private:
	T st[N];
};

int main()
{
	stack<int, 100> st1;
	stack<int, 10> st2;
	
	return 0;
}

        其中,第二个模板参数 是一个整形常量,这个常量值在类实例化的时候确定。这样一来,就可以在创建栈的时候定义它的大小。

注意: 

        1.浮点数、类对象以及字符串是不允许作为非类型模板参数的。

        2.非类型模板参数必须在编译时就能确定。

        但是,在C++20及以后,浮点数可以作为类的非类型模板参数 。


 (2)、非类型模板参数实例

        STL中有一种容器,array

        在C++11及以后,它就是一种使用非类型模板参数的容器:

         array就是数组,但是它是一种封装后的一种数组;对于一般的数组,越界检查是部分的抽查,是通过编译器内部对比数组边界外的小范围内是否被改变来检测实现的;

        如果我们只读,检测不出来: 


int main()
{
	int a[10] = { 1,2,3,4,5,6,7,8,9,10};
    a[10];
    a[11];
    a[12];
	return 0;
}

         如果我们越界写入一个值,就会被检测出来;甚至编译都无法通过:

        但是当我们在数组外距离边界比较远的地方越界写入时,就可能不会被检测出来:

但是array解决了这个问题,因为array是一个类,它可以通过在类内部实现对  [ ]  的重载来进行严格的越界检查,也就是通过assert()来进行检查。

在使用容器array时越界访问,在越界读时会被检测出来: 

#include<array>
using namespace std;
int main()
{
	array<int, 10> arr = {1,2,3,4,5,6,7,8,9,10};
	arr[10];
	return 0;
}

         

越界写时也可以检测出来:

#include<array>
using namespace std;
int main()
{
	array<int, 10> arr = {1,2,3,4,5,6,7,8,9,10};
	arr[10] = 1;
	return 0;
}

 


二、模板的特化

        我们曾将实现了一个比较大小的函数模板:

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

这个模板在大多数情况下都可以正常使用,不会出错,比如对int(整形家族),Date(重载了比较运算符的自定义类型等)都可以正常使用:

class Date
{
public:

	Date(int year = 0, int month = 0, int day = 0);

	bool operator<(Date d) const;

private:
	int _year = -1;
	int _month = -1;
	int _day = -1;
};

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

int main()
{
	int a = 1;
	int b = 2;
	cout << ::Less<int>(a, b) << endl;
	cout << ::Less<double>(9.2, 8.2) << endl;
	Date d1(2022, 1, 1);
	Date d2(2023, 1, 1);

	cout << ::Less<Date>(d1, d2) << " ";

	return 0;
}

但是对于一种特殊情况,Less函数就会出现问题了:

 


int main()
{

	Date d1(2022, 1, 1);
	Date d2(2023, 1, 1);

	cout << ::Less<Date*>(&d1, &d2) << " ";

	return 0;
}

运行结果会随着d1和d2的实例化顺序而不同:

        究其原因,是函数模板实例化出的函数是根据d1和d2的地址大小比较的而不是d1和d2本身。

         通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理 ,这就需要用到  模板的特化  ;


        对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。

(1)函数模板特化

1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同!


        我们就以上面的less函数模板特化出Date类函数为例,进行模板特化:

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

int main()
{
	Date d2(2023, 1, 1);

	Date d1(2022, 1, 1);

	cout << ::Less<Date*>(&d1, &d2) << " ";

	return 0;
}

        这样我们就完成了对函数模板的特化,这时,函数就会根据d1和d2大小来比较,而不是根据d1和d2的地址来比较。这样函数的结果就不会因为d1和d2的实例化顺序不同而产生差异了。


           但是,我们在实现一个函数的时候,传参传自定义类型消耗太大,于是就需要传引用,既然传引用就要防止对象被改变,需要加上const,如下:

template<class T>
bool Less(const T& left,const T& right)
{
	return left < right;
}

当你试图对这个函数模板进行特化的时候,就会发现意想不到的问题:刚开始学习特化,有一个误区:那就是将原模板的类型直接替换到特化后的函数内,比如对上面这个函数进行特化<Date*>,可能你会这样写:


template<>
bool Less<Date*>(const Date*& pleft,const Date* & pright)
{
	return *pleft < *pright;
}

        但是,这是错误的写法;你会发现编译都无法通过,这就是违背了“4. 函数形参表: 必须要和模板函数的基础参数类型完全相同!”的这一条规则。

        仅仅对于语法来说,对于less模板,const修饰的两个变量本身不能修改,特化的Date*版本,两个参数是指针类型,要与模板保持一致,就需要const修饰变量本身,即const修饰指针本身。

        并且const在*之前,修饰指针的内容;const在*之后,修饰指针本身。那么这样写才是正确的:


template<>
bool Less<Date*>(Date* const& pleft, Date* const& pright)
{
	return *pleft < *pright;
}

        这一点需要非常慎重,特别注意!


        同时你可能会发现:

        const修饰指针本身,但是我们还可以通过指针解引用改变其内容,这不是我们希望的,这也就要求:当你在使用函数模板的时候,需要对特化出来的函数一清二楚,并不能说你试着特化一下,看一下特化出来的东西是不是想要的,这个不是概率问题。

(2)类模板特化

        类模板的特化分为全特化和偏特化。

全特化:即是将模板参数列表中所有的参数都确定化:


template<class T1,class T2>
class Data
{
private:
	T1 t1;
	T2 t2;
};
template<>
class Data<char, int>
{
private:
	char t1;
	int t2;
};


偏特化:任何针对模版参数进一步进行条件限制设计的特化:


对参数的进一步限制可以是对参数部分特化:

// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{

private:
    T1 _d1;
    int _d2;
};

也可以是对参数类型的进一步限制:

//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{

private:
    T1 _d1;
    T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{

private:
    const T1 & _d1;
    const T2 & _d2;
};

        对于上述两种偏特化类型,可能会有一个误区:

        我们直接避开这个误区不谈,直接将正确的思想;其实,上述两个类名后面的特化参数只是一个标记,编译器会根据这个标记来匹配特化的类,而不会由于特化参数的写法而改变原本传入的参数类型:

比如:

template<class T1,class T2>
class Data
{
public:
	Data()
	{
		cout << "原模板" << endl;
	}
	
private:
	T1 _d1;
	T2 _d2;
};
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data()
	{
		cout << "Data<T1*, T2*>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};
//引用偏特化
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data(const T1& d1 = 0, const T2& d2 = 0)
		: _d1(d1)
		, _d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
	}
	
private:
	const T1& _d1;
	const T2& _d2;
};
int main()
{
	Data<int, int> d1;
	Data<int*, double*> d2;
	Data<int&, int&> d3;
	
	return 0;
}

        指针偏特化,传入<int*,double*>,参数T1,T2分别就是int*,double*,不会因为类名后面的<T1*, T2*>而将T1,T2改变为int,double。

        引用偏特化也是类似的。

三、模板的分离编译

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

        当我们在使用模板时,如果使用分离编译模式,会导致链接错误。

由于模板在编译时不会实例化出对应的类或者函数,自然没有相应的地址。所以在链接时,编译器在找这个类或者函数的地址时,会找不到,所以报错。

解决方法:

       1. 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
        2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。

模板总结
【优点】
        1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
        2. 增强了代码的灵活性
【缺陷】
        1. 模板会导致代码膨胀问题,也会导致编译时间变长
        2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
 


完~

未经作者同意禁止转载 

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

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

相关文章

微信文件太大传不了?学会这些,微信秒变大文件传输神器

在数字化时代&#xff0c;微信已成为我们日常沟通的重要桥梁。然而&#xff0c;当需要在微信上传输大文件时&#xff0c;文件大小的限制往往让人束手无策。 今天&#xff0c;我们将分享一些实用的技巧&#xff0c;帮助你在微信上轻松传输大文件&#xff0c;无论是工作文档还是…

防火墙组网

一、实验拓扑图 二、实验要求 1、DMZ区内的服务器&#xff0c;办公区仅能在办公时间内&#xff08;9&#xff1a;00-18&#xff1a;00&#xff09;可以访问&#xff0c; 生产区的设备全天可以访问。 2、生产区不允许访问互联网&#xff0c;办公区和游客区允许访问互联网。 3、办…

无人机之飞行规划与管理篇

无人机飞行规划与管理是确保无人机安全、高效且符合法规的运行的关键步骤。这一过程包括了对飞行任务的详细安排、航线的设定以及风险的评估和管理。下面简述这一过程的主要环节&#xff1a; 一、飞行目的和任务确定 在规划之初&#xff0c;必须明确无人机的飞行目的&#xf…

ES13的4个改革性新特性

1、类字段声明 在 ES13 之前,类字段只能在构造函数中声明, ES13 消除了这个限制 // 之前 class Car {constructor() {this.color = blue;this.age = 2

表格数据存本地,实时保存

需求&#xff1a;填写表格时&#xff0c;每填写一个就要保存 将表格数据以json对象的格式存在本地&#xff0c;刷新时在created或者mounted将数据取出&#xff0c;点击保存时将存的数据清除&#xff0c;但是如果不点击保存&#xff0c;直接填写下一个数据&#xff0c;之前填写…

【java算法专场】双指针(下)

611. 有效三角形的个数 目录 611. 有效三角形的个数 算法思路 算法代码 LCR 179. 查找总价格为目标值的两个商品 算法思路 算法代码 HashSet 双指针 15. 三数之和 算法思路 算法代码 18. 四数之和 ​编辑算法思路 算法代码 611. 有效三角形的个数 算法思路 算法…

echarts无法加载Map地图的问题

项目场景&#xff1a; echarts无法加载Map地图的问题 详情 查阅相关资料讲&#xff0c;echarts4.9以上版本已经移除了map&#xff0c;那么我们就得重新打包echarts文件了。打包echarts.min.js的链接&#xff1a;https://echarts.apache.org/zh/builder.html 在这个链接页面可…

CTFShow的36D杯

神光 还是想了一下&#xff0c;但那个异或搞不出来&#xff0c;都是对dword_41A038操作&#xff0c;想起开头就给了 key &#xff0c;还有反调试应该是要调试的 输出的应该就是 flag &#xff0c;只是为什么是乱码呢&#xff1f; 放 od 再试试&#xff0c;直接就出 flag 了&am…

对比:9款最佳个人项目管理软件盘点

文章介绍了9款个人项目管理软件&#xff1a;PingCode、Worktile、Flowus、Todoist、Trello、Teambition、有道云笔记、Notion、Microsoft To Do。 在管理个人项目时&#xff0c;是否常感到信息零散、进度难以把控&#xff1f;选择合适的项目管理软件&#xff0c;可以有效解决这…

关于解决双屏幕鼠标移动方向问题

1.点开设置》系统》屏幕 2.分清屏幕标识&#xff0c;一般笔记本为1 3.点击要移动的屏幕&#xff0c;然后按住鼠标左键不方进行移动 感谢您的浏览&#xff0c;希望可以帮到您&#xff01;

【探索Linux】P.38(传输层 —— TCP协议通信连接管理机制简介 | TCP连接状态转换)

阅读导航 引言一、TCP协议通信连接管理机制二、连接状态转换1. TCP状态转换图2. 状态转换过程3. 理解TIME_WAIT状态&#xff08;1&#xff09;目的和作用&#xff08;2&#xff09;状态转换&#xff08;3&#xff09;特殊情况&#xff08;4&#xff09;影响和优化 4. 理解 CLOS…

医疗器械的售后与服务探讨

彩虹医械维修培训 8月长期班报名啦 学技术 考证书 技术支持 大型医疗设备的宕机不仅会造成医疗机构直接的巨额经济损失&#xff0c;宕机期间的损失甚至可以超过维修费用&#xff0c;而且还可能会耽误患者的病情检查&#xff0c;因此对医疗设备的定期保养已经成为各级医院的…

学生选课管理系统(Java+MySQL)

技术栈 Java: 用于实现系统的核心业务逻辑。MySQL: 作为关系型数据库&#xff0c;用于存储系统中的数据。JDBC: 用于Java程序与MySQL数据库之间的连接和交互。Swing GUI: 用于创建图形用户界面&#xff0c;提升用户体验。 系统功能 我们的学生选课管理系统主要针对学生和管理…

SQL中on和where的区别

SQL中on和where的区别   前言&#xff0c;在工作写SQL使用中&#xff0c;在涉及到多个表的关联时&#xff0c;既可以通过on进行数据过滤&#xff0c;又可以使用where进行数据过滤&#xff0c; 确实有点不太了解这两个关键字在left join后的区别&#xff0c;所以就去查了些资…

从产业链视角审视工作

从产业链视角审视工作&#xff1a;定位、价值与成长 作为一名技术博客博主&#xff0c;我经常收到各种关于职业发展、技术成长和学习路径的问题。最近&#xff0c;我看了一份学习报告&#xff0c;其中提到了一种非常有趣且实用的视角——从产业链的角度去审视自己的工作。这种视…

【实战指南】揭秘Pandas:从零开始掌握数据处理与分析的超级工具箱

目录 Series 创建 Series 访问数据 DataFrame 创建 DataFrame 读取 CSV 文件 写入 CSV 文件 基本操作 查看前几行: 查看后几行: 描述性统计: 选择列: 选择行: 条件筛选: 排序: 合并数据: Pandas 是一个强大的 Python 库&#xff0c;主要用于数据分析和数据处理…

【INTEL(ALTERA)】为什么我使用 PIO 边缘捕获中断的 Nios® II 设计不能正常工作?

目录 说明 解决方法 说明 当用户选择了不正确的边缘捕获设置&#xff0c;从而阻止触发中断时&#xff0c;可能会出现此问题。 在 PIO&#xff08;并行 I/O&#xff09;英特尔 FPGA IP内核中&#xff0c;如果“启用单个位设置/清除”选项被关闭&#xff0c;则将任何值写入边…

智驭未来:人工智能与目标检测的深度交融

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;如同一股不可阻挡的浪潮&#xff0c;正以前所未有的速度重塑着我们的世界。在众多AI应用领域中&#xff0c;目标检测以其独特的魅力和广泛的应用前景&#xff0c;成为了连接现实与智能世界的桥梁。本文旨在…

精讲:java之多维数组的使用

一、多维数组简介 1.为什么需要二维数组 我们看下面这个例子&#xff1f;“ 某公司2022年全年各个月份的销售额进行登记。按月份存储&#xff0c;可以使用一维数组。如果改写为按季度为单位存储怎么办呢&#xff1f; 或许现在学习了一维数组的你只能申请四个一维数组去存储每…

探索Kotlin:从K1到K2

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 嘿&#xff0c;小伙伴们&#xff01;今天我们来聊聊Kotlin&#xff0c;这个在安卓开发圈里越来越火的编程语言。…