C++---模板进阶(非类型模板参数,模板的特化,模板分离编译)

我们都学习和使用过模板,而这篇文章我们来将一些更深入的知识。在此之前,我们在使用C++编程时可以看到模板是随处可见的,它能支持泛型编程。模板包括函数模板和类模板,我们有的人可能会说是模板函数和模板类,但严格讲这样说是错误的。而在我们实际使用中,类模板用的场景是比函数模板多的,如STL中vector,list等都是类模板,而算法中sort,find等是函数模板。

非类型模板参数

模板参数分为类型模板形参,非类型形参。

类型形参:出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。(我们之前一直使用的)

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

举个例子:

我们想要创建一个大小固定,可以存储不同类型元素的数组类,我们可以这样写:

#include<iostream>
using namespace std;
#define N 10

template<class T>
class Array
{
private:
	int _a[N];	
};
int main()
{
	Array<int> arr1;
	Array<int> arr2;
	return 0;
}

这时实例化出来的arr1,arr2都是大小为10的定长数组。但我们假如说想改变一个数组的长度呢?例如我们想将数组长度改为20,怎么办呢?有同学说我们可以将N define为20呀。但是假如我们想要一个数组长为10,另一个长为20呢?

这时候我们的非类型模板参数就有作用了,我们可以将Array定义为下面的形式:

#include<iostream>
using namespace std;

//这里的N叫做非类型模板参数,它是一个常量 
template<class T, size_t N>
class Array
{
private:
	int _a[N];
};
int main()
{
	Array<int, 10> arr1;
	Array<int, 20> arr2;
	int n = 10;
	Array<int, n> arr3;//error 这里非类型模板参数是常量,这里会报错
	return 0;
}

对于非类型模板参数的使用场景,STL中的deque和array容器中都用了。deque中的非类型模板参数用来传一个一个常量来控制buff的大小。

我们下面介绍一下array容器:

C++11支持array,它的结构类似于vector,只不过vector是动态数组,而array是静态的。它不提供头插,头插,尾插,尾删,任意位置插入删除这种操作,因为它根本不存在这种说法,而且它可以直接使用operator[ ] 访问修改任意位置的数据。

array的缺陷:

我们不推荐使用array,array底层是在栈上开辟空间的,而栈空间又是很有限的,例如:在32位的linux下栈空间只有8M,所以一般开大空间时极不推荐使用array,相比之下vector就很有优势。而且在知道要开多大空间的情况下,vector 也可以通过reserve一次性开好空间,在后续的使用中还可以自动增容,而array空间是固定的,不灵活。所以我们一般不使用array。

注意:

  • 非类型模板参数只允许使用整形家族(int,short,long,long long,char),而浮点型,类对象,字符串是不允许作为非类型模板参数的
  • 非类型模板参数需要在编译的时候就确认结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数

模板的特化

我们先举个例子,理解一下模板的特化,看下面的代码:

template<class T>
bool IsEqual(const T& left, const T& right)
{	
	return left == right;
}
int main()
{
	cout << IsEqual(1, 2) << endl;
	char p1[] = "hello";
	char p2[] = "world";
	cout << IsEqual(p1, p2) << endl;
	return 0; 
}

我们实现了IsEqual函数用来判断两个参数是否相等。我们总共调用了两次,第一次比较的是两个整数,是没问题的;而第二次实际上是有问题的,我们本意是想比较两个字符串是否相等,可是我们仔细看看:我们实际上比较的p1,p2两个指针。我们可能会想先判断一下传过来的参数的类型是什么,再进行不同的操作,就像下面这样:

template<class T>
bool IsEqual(const T& left, const T& right)
{
	if(T == char*)//error,C++中不支持类型比较
	{
        return *left==*right;
    }
	else
	{...}
	
	return left == right;
}
int main()
{
	cout << IsEqual(1, 2) << endl;//ok
	char* p1 = "hello";
	char* p2 = "world";
	cout << IsEqual(p1, p2) << endl;//err
	return 0; 
}

但是很可惜,C++中不支持类型比较,所以上面写的是错误的。

那么这里就需要我们的模板特化出手了,模板特化的目的就是在原模板类的基础上,针对某些类型进行特殊化处理。模板特化又分为函数模板特化和类模板特化。

函数模板的特化

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

我们上面的代码就可以改为:

#include<iostream>
using namespace std;

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

template<>
bool IsEqual<const char *&>(const char*& left,const char*& right)
{
	return strcmp(left, right)==0;
}
int main()
{
	cout << IsEqual(1, 2) << endl;
	const char* p1 = "hello";
	const char* p2 = "hello";
	cout << IsEqual(p1, p2) << endl;
	return 0;
}

说明:其实一般情况下,如果函数模板遇到不能处理或者处理有误的类型,可以直接将该函数直接给出,这叫做模板的匹配原则:有现成的完全匹配的,就直接调用,没有现成调用的,实例化模板生成。上面的代码就可以这样写:

#include<iostream>
using namespace std;

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

bool IsEqual(const char*& left,const char*& right)
{
	return strcmp(left, right)==0;
}
int main()
{
	cout << IsEqual(1, 2) << endl;
	const char* p1 = "hello";
	const char* p2 = "hello";
	cout << IsEqual(p1, p2) << endl;
	return 0;
}

对于模板匹配原则和函数模板特化,两者底层没有任何差别,如果能使用函数模板特化的时候更推荐使用函数模板特化。

类模板特化

1.全特化

就是将模板参数列表中所有参数确定化,例如:

#include<iostream>
using namespace std;

template<class T1,class T2>
class A
{
public:
	A()
	{
		cout << "T" << endl;
	}
private:
	T1 a1;
	T2 a2;
};

template<>
class A<int,int>
{
public:
	A()
	{
		cout << "int" << endl;
	}
private:
};

int main()
{
	A<double, double>a1;
	A<int, int>a2;
	return 0;
}
2.偏特化

也叫半特化:即对模板参数进行一定的确定化,例如:

template<class T1>//第二个参数是int类型就会调用这个
class A<T1, int>
{
public:
	A()
	{
		cout << "T1,int" << endl;
	}
private:
};

template<class T2>//第一个参数是int类型就会调用这个
class A<int, T2>
{
public:
	A()
	{
		cout << "int,T2" << endl;
	}
private:
};

template<class T1,class T2>//两个参数都是指针类型就会调用这个
class A<T1*, T2*>
{
public:
	A()
	{
		cout << "T1*,T2*" << endl;
	}
private:
};

template<class T1, class T2>//两个参数都是引用类型就会调用这个
class A<T1&, T2&>
{
public:
	A()
	{
		cout << "T1&,T2&" << endl;
	}
private:
};

template<class T1, class T2>//第一个参数是指针类型,第二个参数是引用类型就会调用这个
class A<T1*, T2&>
{
public:
	A()
	{
		cout << "T1*,T2&" << endl;
	}
private:
};

int main()
{
	A<double, int>a;
	A<int, char>b;
	A<double*, int*>c;
	A<char&, int&>d;
	A<double*, char&>e;

	return 0;
}

上面这几种特化版本都是偏特化。

偏特化并不仅仅是指特化,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本,例如我们可以限制他的类型是引用,指针之类的。

关于特化的场景我们等到哈希表的时候会给大家介绍。

模板分离编译

我们在工作的时候写的项目都会由若干个源文件共同实现,每个源文件独立编译生成目标文件,最后将所有目标我呢见链接起来形成单一的可执行文件的过程称为分离编译模式

模板的分离编译

在前面我们模拟实现string,vector,list等STL容器的时候,都没有将声明和定义分开。我们实际上是习惯将声明放在头文件中,将定义放在.cpp文件中的。我们没有分离的原因就是C++的模板不支持分离编译。

我们以这段代码为例:

//Func.h代码:
#pragma once
#include<iostream>
using namespace std;

void Print();

template<class T>
void F(T a);

//Func.cpp代码
#include"Func.h"

void Print()
{
	cout << "print" << endl;
}

template<class T>
void F(T a)
{
	cout << "F(T a)" << endl;
}

//test.cpp代码
#include"Func.h"

int main()
{
	Print();
	F(10);//这里编不过
	return 0;
}

不支持分离编译的原因:
我们都知道:程序的编译过程分为:预处理,编译,汇编,链接四个步骤

其中预处理会做的事情就是:头文件展开,宏替换,条件编译,去掉注释,在linux环境下就生成了.i文件

编译:检查语法错误后,生成汇编代码,在linux环境下生成.s文件

汇编:将汇编代码转成二进制机器码,在linux环境下生成.o文件

链接:会把.o文件里F或Print这样没有地址的地方,用被修饰过的函数名去Func.o中的符号表找对应的地址,然后填上地址就像上图中的地址一样。然后再把目标文件合并成可执行程序。

而问题就出现在链接的时候,编译器拿着我们的函数名去符号表中找时,能找到Print()函数,但是却找不到F()函数,这是因为在编译阶段,Print()函数有定义,可以生成。但是F()函数是一个函数模板,他不能生成,因为我们不知道T是什么类型。模板其实是调用时生成的。

如何解决

一、

这种方法实际上是不可行的,就是让编译器在编译的时候去各个地方查找实例化,例如我们在Func.i中看到一个模板,我们就去Test.i中找实例化,但是这样的话,如果是一个大项目的化,有几十几百个文件,对于编译器的实现就复杂了。所以实际在链接前,各文件间是不会交互的

二、

显示指定实例化,编译器看到后就知道要把这个模板参数T实例化成什么类型。但是这样做的问题也很大:就是我换个类型就又链接不上了,那我们就只能使用一种类型就实例化一次,这样就很麻烦。

三、

最后这种方法就很粗暴,STL中用的也是这种方法,就是不分离编译,声明和定义都放在一个.h文件里。这样的话,.h文件中就包含了模板的定义,就不需要链接的时候去查找了,在编译的阶段就能直接填地址了。而我们在项目中一般把这种文件命名为后缀为.hpp的文件,也就是说它即是.h文件,又是.cpp文件。

同样我们的类模板也不支持分离编译,最好的办法也是不分离,写在同一个文件中。

我在这里还要在解释我上文中的一句话:模板其实是调用时实例化生成的。我们在一个类模板或者函数模板中,假如写出了语法错误,假如我们没有实例化运行的话,编译器是检查不出错误的,这就是因为模板是调用时实例化的,没有实例化的时候,编译器不会去检查函数模板或类模板内部的语法错误。

总结

模板优点:

1.模板复用了代码,节约资源,更快的迭代开发,有了模板才有了C++的标准模板库STL的诞生

2.增强了代码的灵活性

模板缺点:

1.模板会导致代码膨胀问题,也会导致编译时间变长

2.假如我们使用模板时出现错误,编译器提示的错误信息会非常凌乱,而且准确度不高,我们不能盲目地相信模板的报错。可能只是一点小错误,最后却报出了一大堆错误,这时候我们要优先看第一个错误。

以上就是本章的全部内容,谢谢大家!

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

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

相关文章

【话题】评价GPT-4o:从革命性技术到未来挑战

大家好&#xff0c;我是全栈小5&#xff0c;欢迎阅读小5的系列文章&#xff0c;这是《话题》系列文章 目录 引言技术原理应用领域实际案例优势挑战局限性未来展望文章推荐 引言 在人工智能领域&#xff0c;自然语言处理&#xff08;NLP&#xff09;技术的进步一直是推动技术革…

Allegro蛇形等长

Allegro蛇形等长 一、蛇形等长规则创建方法 一般有点对点&#xff0c;串组两种形式 点对点一般直接在规则中建立等长组 串组可以用模型等长和pin to pin等长 下面提供pin to pin的方法&#xff0c;个人感觉最好用 二、创建规则 打开规则管理器&#xff0c;展开Electrical–…

Studio One安装教程+软件安装包下载

Studio One6全新版本上线 记录、生产、混合、掌握和执行所有操作。从工作室到舞台&#xff0c;Studio One6以易用为核心&#xff0c;是您的创意合作伙伴。 当你准备好登上舞台时&#xff0c;Studio One就在那里。只有Studio One从最初的灵感到完整的制作&#xff0c;最终混音…

【STM32】基于I2C协议的OLED显示(利用U82G库)

【STM32】基于I2C协议的OLED显示(利用U82G库) 文章目录 【STM32】基于I2C协议的OLED显示(利用U82G库)一、实验背景二、U8g2介绍&#xff08;一&#xff09;获取&#xff08;二&#xff09;简介 三、实践&#xff08;一&#xff09;CubexMX配置&#xff08;二&#xff09;U8g2配…

官方正版 | Mailbird - 2024 年最佳电子邮件客户端

Mailbird&#xff1a;个性化的电子邮件客户端 Mailbird 是一款专为 Windows 用户设计的桌面电子邮件客户端&#xff0c;以其用户友好的界面和强大的功能获得了广泛好评。以下是 Mailbird 的主要特点&#xff1a; 个性化背景&#xff1a;Mailbird 提供了可定制的背景选项&#…

【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt窗口 | 标准对话框 | 消息对话框QMessageBox 文章编号&#xff1a;Q…

linux pxe和无人值守

一 PXE和无人值守 pxe c/s模式 允许客户端通过网络从远程服务器&#xff08;服务端&#xff09;下载引导镜像 加载安装文件 实现自动化安装操作系统 无人值守 就是安装选项不需要认为干预 可以自动化实现 pxe的优点 1 规模化 同时装配多台服务器 20多 30台 2 自动化 …

Rust学习06:使用CSDN的AI工具“C知道”分析代码错误

朋友们&#xff0c;我最近真的是在绝望的边缘了&#xff01; Rust咋这么蓝涅&#xff01; 资料咋这们少涅&#xff01; 记得学Python的时候&#xff0c;基本上你遇到的所有问题都可以在书上或者网上找到答案&#xff0c;中文世界找不到那么在英文世界一定能找到答案。 我猜&…

Redis】Redis主从复制(二)————主从结构/流程

目录 回顾slaveof 命令断开主从复制关系切换主从复制关系只读网络延迟问题应对措施补充 主从结构一主一从结构问题改进 一主多从结构树形主从主从切换结构 主从复制流程简单来记关于数据同步两个参数replicationidoffset. psync 运行流程全量复制和部分复制全量复制流程&#x…

使用使用rundll32 调用指定dll的方法

使用使用rundll32 调用指定dll的方法 //顾名思义&#xff0c;"执行32位的DLL文件"。它的作用是执行DLL文件中的内部函数&#xff0c;这样在进程当中&#xff0c; 只会有Rundll32.exe&#xff0c;而不会有DLL后门的进程&#xff0c;这样&#xff0c;就实现了进程上的隐…

自定义组件——ABManager(AB包管理器)

需求描述 在Unity3D引擎中&#xff0c;AB包作为常用的游戏资源存储格式之一。而对于资源管理我们就不得不谈到集中管理的优势了&#xff0c;通过统一的接口加载和卸载AB包及其中的资源将进一步提升我们的编程效率。本文将围绕这个需求进行尝试。 功能描述 1. AB包的加载包括同…

项目:双人五子棋对战-对战模块(6)

完整代码见: 邹锦辉个人所有代码: 测试仓库 - Gitee.com 当玩家进入到游戏房间后, 就要开始一局紧张而又刺激的五子棋对战了, 本文将就前端后端的落子与判断胜负的部分作详细讲解. 模块详细讲解 约定前后端交互的接口 首先是建立连接后, 服务器需要生成一些游戏的初始信息(可…

java程序在运行过程各个内部结构的作用

一&#xff1a;内部结构 一个进程对应一个jvm实例&#xff0c;一个运行时数据区&#xff0c;又包含多个线程&#xff0c;这些线程共享了方法区和堆&#xff0c;每个线程包含了程序计数器、本地方法栈和虚拟机栈接下来我们通过一个示意图介绍一下这个空间。 如图所示,当一个hell…

63-目录操作(QDir类)及展示系统文件实战

一、目录操作(QDir 类) #include <QCoreApplication>#include <QDir> #include <QStringList> #include <QtDebug>// 自定义函数实现获取目录下大小qint64 GetDirFileInfoSizeFunc(const QString &qpath) {// QDir类专门用来操作路径名称或底层文…

catia零件装配时预览零件的形状

这样的显示方式看不到 选择大或中图标就可预览零件形状

Qt | openSSL将TCP数据进行不对称(RSA)加密传输-windows平台实操(可行)

01、windows平台工具准备 QtQt5.14.2openSSL下载(选择适合自己的版本即可)https://slproweb.com/products/Win32OpenSSL.htmlTCP调试助手调试助手02、简介 首先简单介绍一下openssl。接着描述如何在windo

新增多种图表类型,新增视频、流媒体、跑马灯组件,DataEase开源数据可视化分析工具v2.7.0发布

2024年6月11日&#xff0c;人人可用的开源数据可视化分析工具DataEase正式发布v2.7.0版本。 这一版本的功能变动包括&#xff1a;图表方面&#xff0c;新增对称条形图、桑基图、流向地图、进度条等图表类型&#xff0c;并对已有的仪表盘、指标卡、明细表、汇总表、水波图、象限…

(十二)人工智能应用--深度学习原理与实战--模型编译及训练参数的选择

神经网络模型的编译实际上是为网络指定几个非常重要的运行参数,包括优化器、损失函数(误差函数】和评价指标,这三者也代表着神经网络的核心运行机制----通过损失函数来计算网络误差、通过优化器来调整网络参数以降低误差、通过评价指标来衡量网络的性能。神经网络训练时除了…

香港裸机云多IP服务器与普通独享IP服务器的区别

在当前的云计算和服务器托管领域&#xff0c;香港裸机云多IP服务器和普通独享IP服务器是两种常见的选择。它们各自具有独特的特点和优势&#xff0c;适用于不同的应用场景。以下是对这两种服务器类型的详细比较&#xff1a; 一、概念定义 香港裸机云多IP服务器&#xff1a;这是…

【因果推断python】28_面板数据和固定效应2

目录 固定效应 固定效应 为了方面后面更正式地讲述&#xff0c;让我们首先看一下我们拥有的数据。按照我们的例子&#xff0c;我们将尝试估计婚姻对收入的影响。我们的数据包含多年以来多个个体 (nr) 的这两个变量&#xff0c;married 和lwage。请注意&#xff0c;工资采用对数…