C++ 模版进阶

news2024/11/25 2:45:07

目录

前言

1. 非类型模版参数

1.1 概念与讲解

1.2  array容器

2. 模版的特化

2.1 概念

2.2 函数模版特化

2.3 类模版特化

2.3.1 全特化

2.3.2 偏特化

3.模版的编译分离

3.1 什么是分离编译

3.2 模版的分离编译

3.3 解决方法

4. 模版总结

总结


前言

本篇文章主要讲解的是模版进阶的内容,其中有模版更深入的应用,内容丰富,干货多多!


1. 非类型模版参数

1.1 概念与讲解

模版参数分类类型形参非类型形参

类型形参即,出现在模版参数列表中,跟在class或者typename后面的参数类型名称。

非类型形参,就是用一个常量作为类(函数)模版得一个参数,在类(函数)模版中可将该参数当成常量来使用。

我们来看下面的场景。

  • 我们要完成一个静态的栈,一般可以使用宏来定义N,需要存储多少数据,修改宏的大小即可。如果在main函数中我们想让第一个栈存储10个数据,第二个栈存储100数据。
  • 此时,宏的弊端就体现出来。因为N只能表示一个数值,为了满足上面的要求,将N定义为100。可是第一个栈只需要存储10个数据,这样就会造成空间上的浪费。
#define N 100

//静态的栈
template<class T>
class Stack
{
private:
	T a[N];
	int top;
};

int main()
{
	Stack<int> st1;  //10
	Stack<int> st2;  //100

	return 0;
}

为了解决上面出现的问题,我们可以使用非类型模版参数。

//静态的栈
template<class T, size_t N>
class Stack
{
private:
	T a[N];
	int top;
};

int main()
{
	Stack<int, 10> st1;  //10
	Stack<int, 100> st2;  //100

	return 0;
}

  • 非类型模版参数还可以给缺省值,类似于函数参数。
  • 非类型模版参数是个常量,不可以修改。
//静态的栈
template<class T, size_t N = 10>
class Stack
{
public:
	void func()
	{
		N++;//不可以修改N
	}

private:
	T a[N];
	int top;
};

int main()
{
	Stack<int> st1;  //10
	Stack<int, 100> st2;  //100
    
    st1.func();//会报错
	return 0;
}

  • C++20之前的版本只允许整型类型做非类型模版参数。
  • C++20之后的版本支持所有内置类型做非类型模版参数,但是不支持自定义类型参数做非类型模版参数
template<double X, string str>
class Unkonwn
{};

int main()
{
	Unkonwn<1.1, "xxxxx"> un;
	return 0;
}

1.2  array容器

非类型模版参数既然可以解决一些场景下的问题,在STL中的容器有没有使用的呢?array容器就是用非类型模版参数。相当于一个定长数组,进行了封装。

#include <array>
int main()
{
	array<int, 10> aa1;
	cout << sizeof(aa1) << endl;

	return 0;
}

相比于之前的数组,array容器有什么优势呢?

  • 之前的定长数组,检查越界方面使用的是抽查机制,即检查超出数组下标一两位内存空间是否发生改变,如果超出数组下标太多,可能检查不到,并且检查的成本很大。只有你进行写的操作可以检查出来,如果进行读操作,打印出来时,无法检查出来。
  • array容器是自定义类型,下标方括号访问是运算符重载,可以对方括号内的参数进行限制,不管是写还是读操作,都能检查出来。
#include <array>
int main()
{
    //严格的越界检查
	array<int, 10> aa1;
    aa1[10] = 10;
    cout << aa1[11] << endl;

	int aa2[10];
    aa2[10] = 10; //大多数编译器检查的出来
	aa2[14] = 10; //一般的编译器检查不出来,除非是新版的
    cout << aa2[15] << endl; //检查不出来

	return 0;
}

2. 模版的特化

2.1 概念

通常情况下,使用模版可以实现一些与类型无关的代码,但对于一些特殊类型的肯呢个会得到一些错误的结果,需要进行特殊处理。如下面的场景,写一个用来进行小于比较的函数模版。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}

	bool operator<(const Date& d) const
	{
		if (_year < d._year)
			return true;
		else
			if (_year == d._year)
				if (_month < d._month)
					return true;
				else
					if (_month == d._month)
						return _day < d._day;
		return false;
	}
private:
	int _year;
	int _month;
	int _day;
};

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	cout << Less(1, 2) << endl;// 可以比较

	Date d1(2024, 7, 8);
	Date d2(2024, 7, 5);
	cout << Less(d1, d2) << endl; // 可以比较

	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,但是结果错误
	return 0;
}

运行结果如下:

如上述代码所示,Less函数使用模版后对内置类型和自定义类型都可以进行比较操作,但是Date*日期指针类型返回的结果是跟Date类型结果不同,因为p1表示d1日期类变量的地址,p2表示d2日期类变量的地址,只有对p1和p2进行解引用时,才是指向d1和d2两个变量的内容,所以指针相关类型比较特殊。

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

2.2 函数模版特化

函数模版特化要求:

  1. 必须要先有一个基础的函数模版。
  2. 关键字template后面接一对空的尖括号<>。
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
  4. 函数形参必须要和原模版函数的基础参数类型完全相同,需要注意的是,如果不同编译器可能会报一些奇怪的错误

一开始用Less函数举例子,指出对于指针类型无法比较,我们可以使用函数模版来解决。

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()
{
	cout << Less(1, 2) << endl;

	Date d1(2024, 7, 8);
	Date d2(2024, 7, 5);
	cout << Less(d1, d2) << endl; 

	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; //调用特化函数模版,进行比较
	return 0;
}

运行结果如下:

不过使用模版函数会有许多坑。我们一般对函数使用模版时,函数参数会对模版使用引用,如果这个函数不进行修改操作,会在前面加上const修饰,防止该变量被修改。

  • 如下面代码所示,一般人写函数模版的特化,会写成第一种。
  • 这是错误的,因为T此时是Date*指针类型,const T& left中的const修饰的是left,且left本身存储的是变量的地址,所以是指针变量本身不能改变。如果写成第一种const加在Date*前,修饰的是指针指向的内容,跟原函数模版的函数参数类型不同,编译时会报错。
  • 第二种函数模版的特化才是正确的写法,const放在*符号之后,表示修饰left存储变量的地址。
// 函数模板 -- 参数匹配
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

//1.
template<>
bool Less<Date*>(const Date* & left, const Date* & right)
{
	return *left < *right;
}

//2.
template<>
bool Less<Date*>(Date* const & left, Date* const & right)
{
	return *left < *right;
}

2.3 类模版特化

类模版的特化分为全特化偏特化

2.3.1 全特化

全特化即是将模版参数列表中所有的参数都确定化。其中的格式跟函数模版特化类似

template<class T1, class T2>
class Data
{
public:
	Data() 
	{ 
		cout << "Data<T1, T2> -原模版" << endl; 
	}
private:
	T1 _d1;
	T2 _d2;
};

template<>
class Data<int, char>
{
public:
	Data() { 
		cout << "Data<int, char> -全特化" << endl; 
	}
};

void Test1()
{
	Data<int, double> d1; //走第一个构造函数
	Data<int, char> d2; //走特化的构造函数
}

运行结果如下:

2.3.2 偏特化

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

  • 部分特化:将模版参数表中的一部分参数特化。
//特化第二个参数int
template<class T1>
class Data<T1, int>
{
public:
	Data()
	{
		cout << "Data<T1, int> -偏特化" << endl;
	}
};

  • 进一步限制参数类型。偏特化并不仅仅是指特化部分参数,而是针对模版参数更进一步的条件限制所设计出来的一个特化版本。
// 限定模版类型
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:
	Data()
	{
		cout << "Data<T1*, T2*> -偏特化" << endl;
	}
};

void Test2()
{
	Data<int, double> d1;
	Data<int, char> d2;
	Data<int, int> d3;
	Data<int*, double*> d4;
	Data<int**, char*> d5;
}

运行结果如下:

再看下面的代码,先对第二种偏特化的构造函数做一些修改,其中typeid(T1).name( )是获取T1的类型名称,下面一条也是。

// 限定模版类型
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:
	Data()
	{
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
		cout << "Data<T1*, T2*> -偏特化" << endl;
	}
};

void Test3()
{
	Data<int*, double*> d4;
	Data<int**, char*> d5;
}

运行结果如下,T1和T2分表是原来*符号前的类型,而不是原来的指针类型。

3.模版的编译分离

3.1 什么是分离编译

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

3.2 模版的分离编译

下面的代码是模版函数和普通函数分离编译的代码,看看运行的结果如何。建立两个文件,分别是Func.h和Func.cpp。

  • Func.h文件存放函数的声明。
//Func.h

//模版函数
template<class T>
T Add(const T& left, const T& right);

//普通函数
void func();
  • Func.cpp文件存放函数的定义。
//Func.cpp
#include "Func.h"
//模版函数
template<class T>
T Add(const T& left, const T& right)
{
	cout << "T Add(const T& left, const T& right)" << endl;
	return left + right;
}

//普通函数
void func()
{
	cout << "void func()" << endl;
}

写两个测试函数,运行程序看结果。

//Test.cpp
#include "Func.h"
void test1()
{
	Add(1, 2);     
	Add(1.0, 2.0); 
}

void test2()
{
	func();
}

int main()
{
    test1();
    test2();
    return 0;
}

运行test1函数,报连接错误,说有无法解析的外部符号。

运行test2函数,结果如下,没有问题。

普通函数分离编译可以正常运行,模版函数分离编译后运行会报链接错误,这是为什么呢?

  • C/C++程序要运行起来,都要经过这几个步骤:预处理—>编译—>汇编—>链接
  • 在预处理阶段,所有的.h文件都要在包含的位置展开。在编译过程中,将Func.cpp和Test.cpp文件编译成目标文件,分别是Func.o和Test.o。
  • 并且会生成一个符号表,符号表中有函数名经过不同平台规则的变化成新的名称,并且会在函数的定义中,存放函数地址,方便链接。
  • 但是模版函数在定义的部分,不知道要使用什么类型实例化,就不会将Add函数的地址存放在符号表中,那么就会出现上面报的连接错误,无法解析的符号。

 

 

3.3 解决方法

有两种解决方法:

  1. 将声明和定义放在同一个.hpp或者.h文件中
  2. 在模版函数定义的位置显示实例化。

如下面的代码所示,不过这种方法很少用。因为当你使用到这个函数其他类型的话,还需要再添加,十分麻烦。

#include "Func.h"

template<class T>
T Add(const T& left, const T& right)
{
	cout << "T Add(const T& left, const T& right)" << endl;
	return left + right;
}

//显示实例化
template
int Add(const int& left, const int& right);

template
double Add(const double& left, const double& right);

4. 模版总结

模版的优点:

  1. 模版复用代码,节省资源,提高开发效率。
  2. 增强代码的灵活性。

缺点:

  1. 模版会导致代码膨胀问题,使得编译时间变长。
  2. 出现模版编译错误时,错误信息非常凌乱,难以定位错误进行纠正。


总结

通过这篇文章,对于模版的使用有了更深入的了解,如果还有某些地方不够熟悉,可以自己动手敲敲代码。

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!

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

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

相关文章

在mac下 Vue2和Vue3并存 全局Vue2环境创建Vue3新项目(Vue cli2和Vue cli4)

全局安装vue2 npm install vue-cli -g自行在任意位置创建一个文件夹vue3&#xff0c;局部安装vue3,注意不要带-g npm install vue/cli安装完成后&#xff0c;进入目录&#xff0c;修改vue为vue3 找到vue3/node-moudles/.bin/vue&#xff0c;把vue改成vue3。 对环境变量进行配置…

【6】图像分类部署

【6】图像分类部署 文章目录 前言一、将pytorch模型转为ONNX二、本地终端部署2.1. ONNX Runtime部署2.2. pytorch模型部署&#xff08;补充&#xff09; 三、使用flask的web网页部署四、微信小程序部署五、使用pyqt界面化部署总结 前言 包括将训练好的模型部署在本地终端、web…

在Linux操作系统中去修复/etc/fstab文件引起的系统故障。

如果/etcfstab文件中发生错误&#xff0c;有可能导致系统无法正常启动。 比如&#xff1a;系统里的一块磁盘被删除&#xff0c;但是/etc/fstab中关于这块磁盘的信息依然被保存在文件/etc/fstab中。 主要看倒数后两行&#xff0c;系统提示&#xff0c;敲ctrlD或者是直接输入密码…

LeetCode 744, 49, 207

目录 744. 寻找比目标字母大的最小字母题目链接标签思路代码 49. 字母异位词分组题目链接标签思路代码 207. 课程表题目链接标签思路代码 744. 寻找比目标字母大的最小字母 题目链接 744. 寻找比目标字母大的最小字母 标签 数组 二分查找 思路 本题比 基础二分查找 难的一…

redhat7.x 升级openssh至openssh-9.8p1

1.环境准备&#xff1a; OS系统&#xff1a;redhat 7.4 2.备份配置文件&#xff1a; cp -rf /etc/ssh /etc/ssh.bak cp -rf /usr/bin/openssl /usr/bin/openssl.bak cp -rf /etc/pam.d /etc/pam.d.bak cp -rf /usr/lib/systemd/system /usr/lib/systemd/system.bak 3.安装…

阿里云存储应用

如何做好权限控制 小浩在梳理门户网站静态资源时&#xff0c;发现有些资源是仅内部员工可访问&#xff0c;有些资源是特定的注册客户可访问&#xff0c;还有些资源是匿名客户也可以访问。针对不同场景、不同用户&#xff0c;小浩该如何规划企业门户网站静态资源的权限控制呢&a…

解析商场智能导视系统背后的科技:AR导航与大数据如何助力商业运营

在布局复杂的大型商场中&#xff0c;顾客常常面临寻找特定店铺的挑战。商场的规模庞大&#xff0c;店铺众多&#xff0c;使得顾客在享受购物乐趣的同时&#xff0c;也不得不面对寻路的难题。维小帮商场智能导航导视系统的电子地图、AR导航营销能为顾客提供更加便捷的购物体验。…

Linux—网络设置

目录 一、ifconfig——查看网络配置 1、查看网络接口信息 1.1、查看所有网络接口 1.2、查看具体的网络接口 2、修改网络配置 3、添加网络接口 4、禁用/激活网卡 二、hostname——查看主机名称 1、查看主机名称 2、临时修改主机名称 3、永久修改主机名称 4、查看本…

2023年了,还在手动px转rem吗?

px-to-rem 使用amfe-flexible和postcss-pxtorem在webpack中配置px转rem npm i amfe-flexible -Snpm i postcss-pxtorem -D在main.js中 import flexible from amfe-flexible Vue.use(flexible);index.html中 <meta name"viewport" content"widthdevice-w…

用 Echarts 画折线图

https://andi.cn/page/621503.html

Floyd判圈算法——环形链表(C++)

Floyd判圈算法(Floyd Cycle Detection Algorithm)&#xff0c;又称龟兔赛跑算法(Tortoise and Hare Algorithm)&#xff0c;是一个可以在有限状态机、迭代函数或者链表上判断是否存在环&#xff0c;求出该环的起点与长度的算法。 …

Java语言程序设计篇一

Java语言概述 Java语言起源编程语言最新排名名字起源Java语言发展历程Java语言的特点Java虚拟机垃圾回收Java语言规范Java技术简介Java程序的结构Java程序注意事项&#xff1a;注释编程风格练习 Java语言起源 1990年Sun公司提出一项绿色计划。1992年语言开发成功最初取名为Oak…

vue3使用方式汇总

1、引入iconfont阿里图库图标&#xff1a; 1.1 进入阿里图标网站&#xff1a; iconfont阿里&#xff1a;https://www.iconfont.cn/ 1.2 添加图标&#xff1a; 1.3 下载代码&#xff1a; 1.4 在vue3中配置代码&#xff1a; 将其代码复制到src/assets/fonts/目录下&#xff1…

大众点评2024年全球必吃榜清单

大众点评2024年全球必吃榜清单共2797家&#xff0c;奇怪的是官方并没有发布详细清单&#xff0c;只发布了新闻通稿介绍大概情况。这里做一些整理。 按城市分布情况&#xff0c;数量如下 上海 144 北京 137 成都 96 重庆 93 广州 81 深圳 79 武汉 69 苏州 67 杭州 61 …

应急响应--网站(web)入侵篡改指南

免责声明:本文... 目录 被入侵常见现象: 首要任务&#xff1a; 分析思路&#xff1a; 演示案例: IIS&.NET-注入-基于时间配合日志分析 Apache&PHP-漏洞-基于漏洞配合日志分析 Tomcat&JSP-弱口令-基于后门配合日志分析 (推荐) Webshell 查杀-常规后门&…

17_VGG深度学习图像分类算法

1.1 简介 VGG网络&#xff0c;全称为Visual Geometry Group网络&#xff0c;是由牛津大学的Visual Geometry Group和谷歌DeepMind的研究人员共同提出的深度卷积神经网络模型。这一模型因在2014年ILSVRC&#xff08;ImageNet大规模视觉识别挑战赛&#xff09;中取得图像分类任务…

昇思25天学习打卡营第4天|MindSpore数据集和数据变换

# 打卡 目录 # 打卡 Dateset&#xff1a;Pipeline 的起始 具体步骤 数据处理 Pipeline 代码例子 内置数据集的情况 自定义数据集的情况 可迭代的数据集 生成器 Transforms&#xff1a;数据预处理 代码例子 通用变换Compose 文本变换 Text Lambda变换 Dateset&…

STM32芯片系列与产品后缀解读

一. 产品系列 STM32单片机是一系列基于ARM Cortex-M内核的32位微控制器&#xff0c;广泛应用于嵌入式系统中。 STM32系列由STMicroelectronics&#xff08;意法半导体&#xff09;开发和生产&#xff0c;并凭借其灵活的设计、丰富的外设和强大的生态系统&#xff0c;成为嵌入式…

JVM专题之G1垃圾收集器下

索引(记录)的源码的工作流程图如下: CSet(Collection Set 回收集合) 收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机…

catia数控加工仿真铣平面粗加工

1&#xff0c;零件建模&#xff0c;毛坯建模 2 在毛坯上建立坐标系 3 添加资料刀具 4&#xff0c;双击对相关加工信息做设置 5 Roughing 加工设置 高亮红色区域是必选的&#xff0c;其他可以默认 6 完成加工仿真 7 加工余量