C++(模板进阶)

news2025/1/16 16:16:26


目录

前言:

本章学习目标:

1.非类型模版参数 

1.1使用方法 

1.2注意事项

1.3 实际引用

2.模版特化 

 2.1概念

2.2函数模板特化

 2.3类模板特化

2.3.1全特化

2.3.2偏特化 

 3.模版分离编译

​编辑 3.1失败原因

​编辑 3.2解决方案

4 总结 



前言:

本章节是在学习完STL之后,对高阶模版进行的总结,模板给泛型编程注入了灵魂,模板提高了程序的灵活性,模板包括:非类型模版参数、全特化、偏特化等,同时本文还会对模板声明和定义不能分离的问题做出介绍。

本章学习目标:

1. 非类型模板参数
2. 类模板的特化
3. 模板的分离编译

1.非类型模版参数 

模板参数分类 :类型形参与非类型形参。
类型形参即:    出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:    就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

1.1使用方法 

非类型模版参数 既然是使用常量作为参数,那么浮点数、类对象以及字符串是不允许作为非类型模版参数的,   非类型模版参数必须在编译期就能确认结果。

利用非类型模版参数创建一个可以自由调整大小的整形数组代码如下:

#include <assert.h>
using namespace std;


template <size_t N>
class arr
{
public:
	int& operator[](size_t pos)
	{
		assert(pos >= 0 && pos < N);
		return _arr[pos];
	}

	size_t size() const
	{
		return N;
	}

private:
	int _arr[N];
};
int main()
{
	arr<5> a1;
	arr<10> a2;
	arr<15> a3;

	cout <<a1.size() << endl;
	cout << a2.size() << endl;
	cout << a3.size() << endl;
	return 0;
}

定义一个模板类型的静态数组

template <class T ,size_t N>
class arr
{
public:
	T& operator[](size_t pos)
	{
		assert(pos >= 0 && pos < N);
		return _arr[pos];
	}

	size_t size() const
	{
		return N;
	}

private:
	int _arr[N];
};
int main()
{
	arr<int,5> a1;
	arr<double,10> a2;
	arr<char,15> a3;

	cout <<a1.size()<< typeid(a1).name() << endl;
	cout << a2.size() << typeid(a2).name()<< endl;
	cout << a3.size() << typeid(a3).name()<< endl;
	return 0;
}

1.2注意事项

 非类型模板参数要求类型为 整型家族,其他类型是不被编译器通过的,如果我们使用非整形的家族成员就会报错,代码如下:

//浮点型,非标准
template<class T, double N>
class arr 
{
 /*……*/ 
};

 整形家族:char     short      int      bool         long           longlong

1.3 实际引用

 在 C++11 标准中,引入了一个新容器 array,它就使用了 非类型模板参数,为一个真正意义上的 泛型数组,这个数组是用来对标传统数组的。

注意: 部分老编译器可能不支持使用此容器

新引入arry非类型模版参数的代码 如下:

#include <iostream>
#include <cassert>
#include <array>  //引入头文件

using namespace std;

int main()
{
	int arrOld[10] = { 0 };	//传统数组
	array<int, 10> arrNew;	//新标准中的数组

	//与传统数组一样,新数组并没有进行初始化
	
	//新数组对于越界读、写检查更为严格 优点

	arrOld[15];	//老数组越界读,未报错
	arrNew[15];	//新数组则会报错

	arrOld[12] = 0;	//老数组越界写,不报错,出现严重的内存问题
	arrNew[12] = 10;	//新数组严格检查
	return 0;
}

array 是泛型编程思想中的产物,支持了许多 STL 容器的功能,比如 迭代器 和 运算符重载 等实用功能,最主要的改进是 严格检查越界行为。

需要提醒的是:arry能做到严格的越界检查 得益于 []的重载,对下标进行了严格检查。

2.模版特化 

 2.1概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结,需要特殊处理,比如:使用 日期类对象指针 构建优先级队列后,若不编写对应的仿函数,则比较结果会变为未定义 代码如下:

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误
	return 0;
}

造成每次结果不一样是因为 我们每次都日期类对象比较的时候 系统每次分配的地址都是随机的,我们对地址进行比较是不符合实际情况的。 

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

2.2函数模板特化

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

上述日期进行比较的函数模版进行特化之后,代码如下:

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}
int main()
{
	cout << Less(1, 2) << endl;
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl;
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了
	return 0;
}

 2.3类模板特化

 模板特化主要用在类模板中,它可以在泛型思想之上解决大部分特殊问题,并且类模板特化还可以分为:全特化和偏特化,适用于不同场景

后面用的日期类举例比较多,先把日期类放出来

class Date
{
public:
	Date(int year = 1970, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}
	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

private:
	int _year;
	int _month;
	int _day;
};

2.3.1全特化

 全特化指 将所有的模板参数特化为具体类型,将模板全特化后,调用时,会优先选择更为匹配的模板类

//原模板
template<class T1, class T2>
class Test
{
public:
	Test(const T1& t1, const T2& t2)
		:_t1(t1)
		,_t2(t2)
	{
		cout << "template<class T1, class T2>" << endl;
	}

private:
	T1 _t1;
	T2 _t2;
};

//全特化后的模板
template<>
class Test<int, char>
{
public:
	Test(const int& t1, const char& t2)
		:_t1(t1)
		, _t2(t2)
	{
		cout << "template<>" << endl;
	}

private:
	int _t1;
	char _t2;
};

int main()
{
	Test<int, int> T1(1, 2);
	Test<int, char> T2(20, 'c');
	return 0;
}

对模板进行全特化处理后,实际调用时,会优先选择已经特化并且类型符合的模板。

2.3.2偏特化 

 偏特化,指 将泛型范围进一步限制,可以限制为某种类型的指针,也可以限制为具体类型

//原模板---两个模板参数
template<class T1, class T2>
class Test
{
public:
	Test()
	{
		cout << "class Test" << endl;
	}
};

//偏特化之一:限制为某种类型
template<class T1>
class Test<T1, int>
{
public:
	Test()
	{
		cout << "class Test<T, int>" << endl;
	}
};

//偏特化之二:限制为不同的具体类型
template<class T>
class Test<T*, T*>
{
public:
	Test()
	{
		cout << "class Test<T*, T*>" << endl;
	}
};

int main()
{
	Test<double, double> t1;
	Test<char, int> t2;
	Test<Date*, Date*> t3;
	return 0;
}

 

偏特化(尤其是限制为某种类型)在 泛型思想 和 特殊情况 之间做了折中处理,使得 限制范围式的偏特化 也可以实现 泛型

  • 比如偏特化为 T*,那么传 int*char*Date* 都是可行的

应用实例:

 有如下专门用来按照小于比较的类模板 :Less

#include<vector>
#include <algorithm>
template<class T>
struct Less
{
 bool operator()(const T& x, const T& y) const
 {
 return x < y;
 }
};
int main()
{
 Date d1(2022, 7, 7);
 Date d2(2022, 7, 6);
 Date d3(2022, 7, 8);
 vector<Date> v1;
 v1.push_back(d1);
v1.push_back(d2);
 v1.push_back(d3);
 // 可以直接排序,结果是日期升序
 sort(v1.begin(), v1.end(), Less<Date>());
 vector<Date*> v2;
 v2.push_back(&d1);
 v2.push_back(&d2);
 v2.push_back(&d3);
 
 // 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
 // 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
 // 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
 sort(v2.begin(), v2.end(), Less<Date*>());
 return 0;
}
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指 针,结果就不一定正确。因为: sort 最终按照 Less模板中方式比较,所以只会比较指针,而不是比较指针指 向空间中内容,此时可以使用类版本特化来处理上述问题
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
 bool operator()(Date* x, Date* y) const
 {
 return *x < *y;
 }
};

 3.模版分离编译

 

早在 模板初阶 中,我们就已经知道了 模板不能进行分离编译,会引发链接问题

 3.1失败原因

 声明与定义分离后,在进行链接时,无法在符号表中找到目标地址进行跳转,因此链接错误

下面是 模板声明与定义写在同一个文件中时,具体的汇编代码执行步骤

test.h 

#pragma once

//声明
template<class T>
T add(const T x, const T y);

//定义
template<class T>
T add(const T x, const T y)
{
	return x + y;
}

 main.cpp

#include <iostream>
#include "Test.h"

using namespace std;

int main()
{
	add(1, 2);
	return 0;
}

 

 声明与定义在同一个文件中时,可以直接找到函数的地址

编译器 生成可执行文件的四个步骤:

  1. 预处理:头文件展开、宏替换、条件编译、删除注释,生成纯净的C代码
  2. 编译:语法 / 词法 / 语义 分析、符号汇总,生成汇编代码
  3. 汇编:生成符号表,生成二进制指令
  4. 链接:合并段表,将符号表进行合并和重定位,生成可执行程序

当模板的 声明 与 定义 分离时,因为是 【泛型】,所以编译器无法确定函数原型,即 无法生成函数,也就无法获得函数地址,在符号表中进行函数链接时,必然失败 

 3.2解决方案

 解决方法有两种:

  1. 在函数定义时进行模板特化,编译时生成地址以进行链接
  2. 模板的声明和定义不要分离,直接写在同一个文件中
//定义
//解决方法一:模板特化(不推荐,如果类型多的话,需要特化很多份)
template<>
int add(const int x, const int y)
{
	return x + y;
}

 

//定义
//解决方法二:声明和定义写在同一个文件中
template<class T>
T add(const T x, const T y)
{
	return x + y;
}

这也就解释了为什么涉及 模板 的类,其中的函数声明和定义会写在同一个文件中 (.h),著名的 STL 库中的代码的声明和定义都是在一个 .h 文件中

为了让别人一眼就看出来头文件中包含了 声明 与 定义,可以将头文件后缀改为 .hpp,著名的 Boost 库中就有这样的命名方式 

 

4 总结 

 

模板是 STL 的基础支撑,假若没有模板、没有泛型编程思想,那么恐怕 "STL" 会变得非常大

模板的优点:

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

模板会导致代码膨胀问题,也会导致编译时间变长
出现模板编译错误时,错误信息非常凌乱,不易定位错误位置
总之,模板 是一把双刃剑,既有优点,也有缺点,只有把它用好了,才能使代码 更灵活、更优雅

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

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

相关文章

乐划锁屏插画大赏热度持续,进一步促进价值内容的创造与传播

锁屏,原本只是为了防止手机在口袋里“误触”而打造的功能,现如今逐渐成为文化传播领域的热门入口。乐划锁屏不断丰富锁屏内容和场景玩法,通过打造“乐划锁屏插画大赏”系列活动为广大内容创作者提供了更多展示自我的机会,丰富平台内容。 从2020年到2023年,乐划锁屏插画大赏已连…

[点云分割] 基于颜色的区域增长分割

效果&#xff1a; 代码&#xff1a; #include <iostream> #include <thread> #include <vector>#include <pcl/point_types.h> #include <pcl/io/pcd_io.h> #include <pcl/search/search.h> #include <pcl/search/kdtree.h> #inclu…

Node.js入门指南(一)

目录 Node.js入门 什么是Node.js Node.js的作用 Node.js安装 Node.js编码注意事项 Buffer(缓冲器&#xff09; 定义 使用 fs模块 概念 文件写入 文件读取 文件移动与重命名 文件删除 文件夹操作 查看资源状态 路径问题 path模块 Node.js入门 什么是Node.js …

基于单片机声光控智能路灯系统仿真设计

**单片机设计介绍&#xff0c; 基于单片机声光控智能路灯系统仿真设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的声光控智能路灯系统是一种利用单片机技术实现智能控制的路灯系统。它通过感知环境音量和光照强度…

vue中为什么data属性是一个函数而不是一个对象

面试官&#xff1a;为什么data属性是一个函数而不是一个对象&#xff1f; 一、实例和组件定义data的区别 vue实例的时候定义data属性既可以是一个对象&#xff0c;也可以是一个函数 const app new Vue({el:"#app",// 对象格式data:{foo:"foo"},// 函数格…

c语言编程(模考2)

简答题1 从键盘输入10个数&#xff0c;统计非正数的个数&#xff0c;并且计算非正数的和 #include<stdio.h> int main() {int i,n0,sum0;int a[10];printf("请输入10个数&#xff1a;");for(i0;i<10;i){scanf("%d",&a[i]);}for(i0;i<10…

【Python】数据类型和切片的零碎知识点

1. 数据类型 pow(a, b, c) # a^b % c print("happy {}".format(name))数字类型包括整数&#xff0c;浮点数&#xff0c;复数 0x9a表示十六进制数&#xff08;0x&#xff0c;0X开头表示十六进制&#xff09; 0b1010&#xff0c;-0B101表示二进制数&#xff08;0…

【endnote】如何将参考文献放到想放的位置

1. 方式 直接将生成的文献全选拖到想放的位置 注意&#xff1a;不要使用ctrlx这种操作。 2.具体操作 2.1 新建测试文档 如下图&#xff1a; 2.2 引用两篇文献】 如下图&#xff1a; 2.3 测试 如下图&#xff0c;选中所有已经引用的文献。 拖拽到想要防止的位置。 新…

产品经理的具体工作职责有什么?

产品经理是现代企业中非常重要的一种职位&#xff0c;其工作职责也非常广泛和复杂。产品经理需要在市场、用户、技术等多个方面进行综合考虑&#xff0c;为企业开发出具有竞争力的产品&#xff0c;从而推动企业的发展。下面我们将详细介绍产品经理的具体工作职责。 一、市场调研…

深入理解Java注解的实现原理以及前世今生

深入理解Java注解的实现原理以及前世今生 小雪初寒&#xff0c;请添衣&#xff0c;冬棋如意&#xff0c;待良人&#xff0c;望归期。 1.Java注解的前世今生 Java注解是一种元数据标记&#xff0c;它提供了一种在Java代码中添加元数据&#xff08;注释&#xff09;的方式。注解…

【IEEE独立出版 | 往届均完成检索】2024年第四届消费电子与计算机工程国际学术会议(ICCECE 2024)

#国际学术会议# 推荐 #广州# 【IEEE独立出版 | 往届均完成检索】2024年第四届消费电子与计算机工程国际学术会议&#xff08;ICCECE 2024&#xff09; 2024 4th International Conference on Consumer Electronics and Computer Engineering 2024年1月12-14日 | 中国广州 会…

力扣.面试题 04.06. 后继者(java 树的中序遍历)

Problem: 面试题 04.06. 后继者 文章目录 题目描述思路解题方法复杂度Code 题目描述 设计一个算法&#xff0c;找出二叉搜索树中指定节点的“下一个”节点&#xff08;也即中序后继&#xff09;。 如果指定节点没有对应的“下一个”节点&#xff0c;则返回null。 思路 由于题…

双11再创新高!家电行业如何通过矩阵管理,赋能品牌增长?

双11大促已落下帷幕&#xff0c;虽然今年不再战报满天飞&#xff0c;但从公布的数据来看&#xff0c;家电行业整体表现不俗。 根据抖音电商品牌业务发布的收官战报&#xff0c;家电行业创造了成交新纪录&#xff0c;整体同比增长125%。快手官方数据显示&#xff0c;消电家居行业…

7.HTML中列表标签

7.列表标签 7.1无序列表&#xff08;重点&#xff09; 表格是用来显示数据的&#xff0c;那么列表就是用来布局的。 列表最大的特点就是整齐&#xff0c;整洁&#xff0c;有序&#xff0c;他作为布局会更加自由和方便&#xff0c; 根据使用的情景不同&#xff0c;列表可分为三…

Go 语言函数、参数和返回值详解

函数是一组语句&#xff0c;可以在程序中重复使用。函数不会在页面加载时自动执行。函数将通过调用函数来执行。 创建函数 要创建&#xff08;通常称为声明&#xff09;一个函数&#xff0c;请执行以下操作&#xff1a; 使用 func 关键字。指定函数的名称&#xff0c;后跟括…

【Java 进阶篇】Redis 数据结构:轻松驾驭多样性

引言 Redis是一款强大的键值对存储系统&#xff0c;其数据结构的多样性是其引以为傲的特点之一。在这篇博客中&#xff0c;我们将深入探讨Redis的主要数据结构&#xff0c;包括字符串、哈希表、列表、集合和有序集合&#xff0c;并通过实例代码演示它们的用法。 1. 字符串&am…

基于跳蛛算法优化概率神经网络PNN的分类预测 - 附代码

基于跳蛛算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于跳蛛算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于跳蛛优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

linux部署jar 常见问题

1.java -jar xxx.jar no main manifest attribute, in xxx.jar 一.no main manifest attribute, in xxx.jar 在pom.xml文件中加入&#xff1a; <plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifac…

SpringBoot监听器解析

监听器模式介绍 监听器模式的要素 事件监听器广播器触发机制 SpringBoot监听器实现 系统事件 事件发送顺序 监听器注册 监听器注册和初始化器注册流程类似 监听器触发机制 获取监听器列表核心流程: 通用触发条件: 自定义监听器实现 实现方式1 实现监听器接口: Order(1) …