【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();
	}

	template
	class stack<int>;

	template
	class 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. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

安装docker和案例复现

安装环境 1.安装docker #输入命令 yum install -y yum-utils 安装下载docker的工具包 yum install -y yum-utils # 设置阿里docker镜像仓库地址 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo yum install -y docker-ce d…

大数据-玩转数据-Flink-Transform

一、Transform 转换算子可以把一个或多个DataStream转成一个新的DataStream.程序可以把多个复杂的转换组合成复杂的数据流拓扑. 二、基本转换算子 2.1、map&#xff08;映射&#xff09; 将数据流中的数据进行转换, 形成新的数据流&#xff0c;消费一个元素并产出一个元素…

阻塞队列_线程安全版本_生产消费者模型

前言 在前面PriorityQueue优先级队列_Y君的进化史的博客-CSDN博客&#xff0c;我们学习了优先级队列&#xff0c;但是发现&#xff0c;当一个线程将优先级队列使用完之后&#xff0c;会自动退出程序&#xff0c;如果此时我们想使其一直等待到下一个任务的录入&#xff0c;就需…

面试热题(LRU缓存)

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中&#xff0c;则返回关键字的值&#xff0c;否则返回 -1 …

信息论基础知识

注意&#xff1a;本文只针对离散随机变量做出探讨&#xff0c;连续随机变量的情况不适用于本文探讨的内容&#xff01; &#xff08;一&#xff09;自信息 1. 自信息 I ( x ) − l o g n P ( x ) \color{blue}I(x) - log_{n}{P(x)} I(x)−logn​P(x) 注意&#xff1a; 若n …

亚马逊 EC2服务器下部署java环境

1. jdk 1.8 安装 1.1 下载jdk包 官网 Java Downloads | Oracle tar.gz 包 下载下来 1.2 本地连接 服务器 我用的是亚马逊的ec2 系统是 ubuntu 的 ssh工具是 Mobaxterm , 公有dns 创建实例时的秘钥 链接 Mobaxterm 因为使用的 ubuntu 所以登录的 名称 就是 ubuntu 然后 …

Linux centos 常用命令 【持续更新】

一、查看文件信息 indoe和目录项 # df命令查看每个硬盘分区的inode总数和已经使用的数量 df -i# 查看inode的大学 xfs_growfs /dev/sda1|grep "isize"# 查看文件的indoe号码 ls -istat查看文件信息 # 文件的详细信息 stat anaconda-ks.cfg # -t参数是在一行内输出…

Linux 的基本指令(3)

指令1&#xff1a;date 作用&#xff1a;用来获取时间的指令。 1. 获取当下的时间&#xff1a; date %Y-%m-%d_%H:%M:%S 其中&#xff1a;%Y 表示年&#xff0c;%m 表示月&#xff0c;%d 表示日&#xff0c;%H 表示 小时&#xff0c;%M 表示分&#xff0c;%S 表示秒。 上面代…

用 oneAPI 实现 AI 欺诈检测:一款智能图像识别工具

简介 虚假图像和视频日益成为社交媒体、新闻报道以及在线内容中的一大隐患。在这个信息爆炸的时代&#xff0c;如何准确地识别和应对这些虚假内容已经成为一个迫切的问题。为了帮助用户更好地辨别虚假内容&#xff0c;我开发了一款基于 oneAPI、TensorFlow 和 Neural Compress…

springBoot集成caffeine,自定义缓存配置 CacheManager

目录 springboot集成caffeine Maven依赖 配置信息&#xff1a;properties文件 config配置 使用案例 Caffeine定制化配置多个cachemanager springboot集成redis并且定制化配置cachemanager springboot集成caffeine Caffeine是一种基于服务器内存的缓存库。它将数据存储在…

进销存管理系统(小杨国贸)springboot采购仓库财务java jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 进销存管理系统&#xff08;小杨国贸&#xff09;spri…

k8s之StorageClass(NFS)

一、前言 1、环境 k8s v1.23.5 &#xff0c;服务器是centos7.9 192.168.164.20 k8s-master1 192.168.164.30 k8s-node1 192.168.164.40 k8s-node2 2、貌似storageClass在kubernetes v1.20就被砍了。 因为它比较慢&#xff0c;而且耗资源&#xff0c;但可以通过不同的实现镜…

玩机搞机--【开机出现您的设备内部出现了问题,请联系你的制造商了解详情】故障解决思路

很多友友在玩机过程中经常会遇到下图所示故障。大多数都是刷了第三方系统或者内核或者面具导致的。正常来说。这个提示可以无视的&#xff0c;不影响正常的手机使用。但强迫症例外。究其原因。一般是内核校验原因。解决方法也分为多种。今天就为大家解析下这个提示的解决思路 &…

基于docker部署的Selenium Grid分布式自动化测试

01、什么是Selenium Grid Selenium Grid是Selenium套件的一部分&#xff0c;它专门用于并行运行多个测试用例在不同的浏览器、操作系统和机器上。 Selenium Grid有两个版本——老版本Grid 1和新版本Grid 2。我们只对新版本做介绍&#xff0c;因为Selenium团队已经逐渐遗弃老版…

yum 安装本地包 rpm

有时直接yum install 有几个包死活下不下来 根据网址&#xff0c;手动下载&#xff0c;下载后上传至 centos 然后运行 sudo yum localinstall xxx.rpm 即可安装 参考 https://blog.csdn.net/weiguang1017/article/details/52293244

微服务01-SpringCloud

1、简介 SpringCloud集成了各种微服务功能组件&#xff0c;并基于SpringBoot实现了这些组件的自动装配&#xff0c;从而提供了良好的开箱即用体验。 其中常见的组件包括&#xff1a; 2、服务拆分和远程调用 2.1 服务拆分 这里总结了微服务拆分时的几个原则&#xff1a; …

JAVA Android 正则表达式

正则表达式 正则表达式是对字符串执行模式匹配的技术。 正则表达式匹配流程 private void RegTheory() {// 正则表达式String content "1998年12月8日&#xff0c;第二代Java平台的企业版J2EE发布。1999年6月&#xff0c;Sun公司发布了第二代Java平台(简称为Java2) &qu…

HTML+JavaScript构建一个将C/C++定义的ANSI字符串转换为MASM32定义的DWUniCode字符串的工具

公文一键排版系统基本完成&#xff0c;准备继续完善SysInfo&#xff0c;增加用户帐户信息&#xff0c;其中涉及到Win32_Account结构&#xff0c;其C定义如下&#xff1a; [Dynamic, Provider("CIMWin32"), UUID("{8502C4CC-5FBB-11D2-AAC1-006008C78BC7}"…

【Linux】进程间通信——System V信号量

目录 写在前面的话 一些概念的理解 信号量的引入 信号量的概念及使用 写在前面的话 System V信号量是一种较低级的IPC机制&#xff0c;使用的时候需要手动进行操作和同步。在现代操作系统中&#xff0c;更常用的是POSIX信号量&#xff08;通过sem_*系列的函数进行操作&…

【雕爷学编程】Arduino动手做(24)---水位传感器模块3

37款传感器与模块的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&#x…