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

news2024/10/3 2:14:26

文章目录

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

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

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

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

相关文章

vs2022+qt6.24+Cef编译

1.QCefView源码下载地址 https://github.com/cefview/qcefview2.目录层级关系如下&#xff1a; 3.下载CefViewCore git pull --regit pull --recurse-submodules上面命令失败直接用下面的命令 git clone gitgithub.com:CefView/CefViewCore.git4.编译QCefView准备工作 a.准…

Java程序猿搬砖笔记(十五)

文章目录 在Java中将类作为参数传递(泛型)IDEA快捷键&#xff1a;查看该方法调用了哪些方法、被哪些方法调用快捷键&#xff1a;ctrlalth IDEA快捷键&#xff1a;快速从controller跳转到serviceImplIDEA快捷键&#xff1a;实现接口的方法IDEA 快捷键&#xff1a;快速包裹代码ID…

剑指offer66.构建乘积数组

我一开始的想法就是&#xff0c;先把所有数的乘积求出来&#xff0c;然后遍历数组&#xff0c;用这个积除以它&#xff0c;就是除了这个数外所有数的乘积。但是题目明确给了不能用除法&#xff0c;所以可不可以用位运算来实现除法呢。 class Solution {public int[] construct…

第R3周 - 天气预测

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 我的环境&#xff1a; 语言环境&#xff1a;Python3.10.7编译器&#xff1a;VScode深度学习环境&#xff1a;TensorFlow 2.13.0 数据集&#xff1a; 一、前期…

系列二、Redis简介

一、概述 # 官网 https://redis.io/ 总结&#xff1a;redis是一个内存型的数据库。 二、特点 Redis是一个高性能key/value内存型数据库。Redis支持丰富的数据类型。Redis支持持久化 。Redis单线程,单进程。

Jmeter 配置环境变量,简明教程专享

通过给 JMeter 配置环境变量&#xff0c;可以快捷的打开 JMeter&#xff1a; 打开终端。执行 jmeter。 配置环境变量的方法如下。 Mac 和 Linux 系统 在 ~/.bashrc 中加如下内容&#xff1a; export JMETER_HOMEJMeter所在目录 export PATH$JAVA_HOME/bin:$PATH:.:$JMETER…

日常BUG —— Java判空注解

&#x1f61c;作 者&#xff1a;是江迪呀✒️本文关键词&#xff1a;日常BUG、BUG、问题分析☀️每日 一言 &#xff1a;存在错误说明你在进步&#xff01; 一. 问题描述 问题一&#xff1a; 在使用Java自带的注解NotNull、NotEmpty、NotBlank时报错&#xff0c;…

ubuntu 安装 cuda

ubuntu 安装 cuda 初环境与设备在官网找安装方式 本篇文章将介绍ubuntu 安装 CUDA Toolkit CUDA Toolkit 是由 NVIDIA&#xff08;英伟达&#xff09;公司开发的一个软件工具包&#xff0c;用于支持并优化 GPU&#xff08;图形处理器&#xff09;上的并行计算和高性能计算。它…

ISC 2023 | 赛宁网安验证评估 重磅发布

​​8月9日-10日&#xff0c;第十一届互联网安全大会&#xff08;简称ISC 2023&#xff09;在北京国家会议中心隆重举办。作为本次大会的战略合作伙伴&#xff08;最高级别&#xff09;&#xff0c;赛宁网安主办 “安全验证评估论坛”&#xff0c;邀请邬江兴院士与业界专家共同…

企业分配给员工的微信号怎么高效管理?

很多很多公司都在发愁这几个问题&#xff1a; 1、拥有多个微信号&#xff0c;不想管理多台手机&#xff0c;想将所有微信号进行统一管理 2、想用软件来代替传统的营销体系&#xff0c;安全性上也要有保障 3、用人成本太大与公司的效益不成正比 4、多个账号发圈不方便&#xff0…

半关闭、端口复用与IO多路复用

文章目录 半关闭端口复用IO多路复用&#xff08;IO多路转接&#xff09;模型解决措施 sellect缺点 poll应用缺点 epoll应用工作模式 半关闭 使用close(fd);所对应的文件描述符写和读都关闭了。 端口复用 可以解决绑定失败的问题。 IO多路复用&#xff08;IO多路转接&#…

网工内推 | 云计算工程师专场,六险一金,IE认证优先

01 铠源科技 招聘岗位&#xff1a;云计算工程师 职责描述&#xff1a; 1.具备虚拟化、桌面云、存储、服务器、数据中心、大数据、相关产品的工程项目交付或协助项目交付能力&#xff1b; 2.具备与客户有效沟通技术方案、项目计划和进度等&#xff0c;获得客户支持和认可&#…

开启想象翅膀:轻松实现文本生成模型的创作应用,支持LLaMA、ChatGLM、UDA、GPT2、Seq2Seq、BART、T5、SongNet等模型,开箱即用

开启想象翅膀&#xff1a;轻松实现文本生成模型的创作应用&#xff0c;支持LLaMA、ChatGLM、UDA、GPT2、Seq2Seq、BART、T5、SongNet等模型&#xff0c;开箱即用 TextGen: Implementation of Text Generation models 1.介绍 TextGen实现了多种文本生成模型&#xff0c;包括&a…

深度解析:使用Postman调试微信支付接口的完美指南

前期准备 在使用 Postman 调试微信支付接口之前&#xff0c;你需要做好以下准备&#xff1a; 安装 Postman 客户端应用&#xff0c;或使用网页版&#xff1b;成为 微信支付商户&#xff1b;已申请 商户API私钥。 当你已经具备这三个条件&#xff0c;就可以进入微信支付接口调…

【视频】使用OBS将MP4推流至腾讯云直播

1、下载OBS OBS官网:https://obsproject.com/ OBS支持Win、Mac、Linux,如果下载速度很慢,建议使用迅雷下载 2、OBS推流设置 2.1 添加场景 默认会有一个“场景”,如果想继续添加可以点击“+”按钮 2.2 添加媒体源 1)点击“来源”窗口中“+”按钮 2)支持的媒体源如…

mysql高级(尚硅谷-夏磊)

目录 内容介绍 Linux下MySQL的安装与使用 Mysql逻辑架构 Mysql存储引擎 Sql预热 索引简介 内容介绍 1、Linux下MySQL的安装与使用 2、逻辑架构 3、sql预热 Linux下MySQL的安装与使用 1、docker安装docker run -d \-p 3309:3306 \-v /atguigu/mysql/mysql8/conf:/etc/my…

IP网络广播系统草坪音箱景区系统防水石头,草坪音箱的应用

IP网络广播系统草坪音箱景区系统防水石头,草坪音箱的应用 SV-7045V是深圳锐科达电子有限公司的一款防水网络草坪音箱&#xff0c;具有10/100M以太网接口&#xff0c;可将网络音源通过自带的功放和喇叭输出播放&#xff0c;可达到功率20W。常用场景&#xff1a;公园ip草坪音箱&…

2009年上半年 软件设计师 下午试卷

博主介绍&#xff1a;✌全网粉丝3W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

PHP最简单自定义自己的框架数据库封装调用(五)

1、实现效果调用实现数据增删改查封装 2、index.php 入口定义数据库账号密码 <?php//定义当前请求模块 define("MODULE",index);//定义数据库 define(DB_HOST,localhost);//数据库地址 define(DB_DATABASE,aaa);//数据库 define(DB_USER,root);//数据库账号 def…

解读百威亚太2023上半年财报:啤酒大年百威如何重塑高端化之路?

随着消费者的需求提升&#xff0c;啤酒行业向高端化发展&#xff0c;其中知名度较高的百威亚太、华润啤酒、青岛啤酒、燕京啤酒、嘉士伯等品牌在高端市场持续鏖战&#xff0c;实际成果如何也可以从业绩一探究竟。 以百威亚太为例。8月3日&#xff0c;百威亚太发布2023年上半年…