【C++初阶】第十三篇:模板进阶(非类型模板参数、模板的特化以及模板的分离编译)

news2024/11/28 8:35:16

文章目录

  • 一、非类型模板参数
  • 二、模板的特化
    • 2.1 概念
    • 2.2 函数模板特化
    • 2.3 类模板特化
      • 2.3.1 全特化
      • 2.3.2 偏特化/半特化
  • 三、模板的分离编译
    • 3.1 什么是分离编译
    • 3.2 模板的分离编译
    • 3.3 解决方法
  • 四、模板总结

一、非类型模板参数

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

类型模板参数示例:

例如,我们要实现一个静态数组的类,在我们没学非类型模板参数之前,我们需要定义宏。

#define N  10

template<class T>
class Array
{
public:
	size_t arraysize()
	{
		return N;
	}
private:
	T _a[N];
};

非类型模板参数示例:

在我们了解非类型模板参数后,我们就可以用以下定义:

template<class T, size_t N> //N:非类型模板参数
class Array
{
public:
	size_t arraysize()
	{
		return N;
	}
private:
	T _a[N]; //利用非类型模板参数指定静态数组的大小
};

使用非类型模板参数后,我们就可以在实例化对象的时候指定所要创建的静态数组的大小了。

int main()
{
	Array<int, 10> a1; //定义一个大小为10的静态数组
	cout << a1.arraysize() << endl; //10
	
	Array<double, 100> a2; //定义一个大小为100的静态数组
	cout << a2.arraysize() << endl; //100
	return 0;
}

注意:

  1. 非类型模板参数只允许使用整型家族浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  2. 非类型的模板参数在编译期就需要确认结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数。(只能传常量,不能传变量)
template<class T, double N>	//报错--非类型模板参数只允许使用整形家族
void Func(const T& x)
{
	cout << N << endl;
}

template<class T, size_t N>	//传入的不是直接的变量100,报错
void Func(const T& x)
{
	cout << N << endl;
}

int main()
{
	int N = 100;
	Func<int, N>(1);//不能传变量

	return 0;
}

C++11中array - 自定义类型可变数组使用示例

#include<iostream>
#include <array>
using namespace std;

int main()
{
	int a1[10];	//C语言和C++的对称用法,数据都存于栈上
	array<int, 10> a2;	//自定义类型的可变数组
	array<int, 100> a3;

	// C语言数组对于越界的检查
	// 越界读不检查
	// 越界写  抽查
	cout << a1[10] << endl;	//越界读不报错
	cout << a1[11] << endl;

	//a1[10] = 0; - 报错,越界写不一定报错,不同平台的抽查方式不同
	//a1[15] = 0; - 不报错

	// C++11  array		重载过[],可以检查出来
	cout << a2[10] << endl;
	cout << a2[11] << endl;

	//a2[10] = 0;
	//a2[15] = 0;

	return 0;
}

二、模板的特化

2.1 概念

这里举一个简单的例子来说明什么是特化,下面是用于比较两个任意相同类型的数据是否相等的函数模板。

template<class T>
bool equal(T x, T y)
{
	return x == y;
}

我们大概会这样使用该函数模板:

cout << equal(1, 1) << endl; //1
cout << equal(1.1, 2.2) << endl; //0

以上这样使用是没有问题的,它的判断结果也是我们所预期的,但是我们也可能会这样去使用该函数模板:

char a1[] = "abcde";
char a2[] = "abcde";
cout << equal(a1, a2) << endl; //0  -- 这里传过去的其实是a1,a2的地址

判断结果是这两个字符串不相等,这很好理解,因为我们希望的是该函数能够判断两个字符串的内容是否相等,而该函数实际上判断是确实这两个字符串所存储的地址是否相同,这是两个存在于栈区的字符串,其地址显然是不同的。
类似于上述实例,使用模板可以实现一些与类型无关的代码,但对于一些特殊的类型可能会得到一些错误的结果,此时就需要对模板进行特化,即在原模板的基础上,针对特殊类型进行特殊化的实现方式。
模板特化中分为函数模板特化类模板特化

2.2 函数模板特化

对于上述实例,我们知道当传入的类型是char时,应该依次比较各个字符的ASCII码值进而判断两个字符串是否相等,或是直接调用strcmp函数进行字符串比较,那么此时我们就可以对char类型进行特殊化的实现。

函数模板的特化步骤:

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

对于上述实例char*类型的特化如下:

//基础函数模板
template<class T>
bool equal(T x, T y)
{
	return x == y;
}
//对于char*类型的特化
template<>
bool equal<char*>(char* x, char* y)
{
	return strcmp(x, y) == 0;
}

2.3 类模板特化

不仅函数模板可以进行特化,类模板也可以针对特殊类型进行特殊化实现,并且类模板的特化又可分为全特化偏特化(半特化)。

2.3.1 全特化

全特化即是将模板参数列表中所有的参数都确定化
例如,对于以下类模板:

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

当T1,T2分别是double和char时,我们若是想对实例化的类进行特殊化处理,那么我们就可以对T1和T2分别是double和int时的模板进行特化。

函数模板的特化步骤:

  1. 首先必须要有一个基础的类模板。
  2. 关键字template后面接一对空的尖括号<>。
  3. 类名后跟一对尖括号,尖括号中指定需要特化的类型。

对于T1是double,T2是char的特化如下:

template<>
class Data<double, char>//类型参数确定化
{
public:
	Data() { cout << "Data<int, char>" << endl; }
private:
	double _d1;
	char _d2;
};

2.3.2 偏特化/半特化

偏特化是指任何针对模板参数进一步进行条件限制设计的特化版本.

例如,对以下类模板:

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

偏特化有以下两种表现方式:

1. 部分特化
将模板参数类表中的一部分参数特化。

// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
	Data() {cout<<"Data<T1, int>" <<endl;}
private:
 	T1 _d1;
	int _d2;
}; 

2. 参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版
本。
例如,我们还可以指定当T1和T2为某种类型时,使用我们特殊化的类模板。

//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }

private:
	T1 _d1;
	T2 _d2;
};

//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data(const T1& d1, const T2& d2)
		: _d1(d1)
		, _d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
	}

private:
	const T1& _d1;
	const T2& _d2;
};

int main()
{
	Data<double, int> d1; // 调用部分特化的int版本
	Data<int, double> d2; // 调用基础的(特化前的)模板 
	Data<int*, int*> d3; // 调用特化的指针版本
	Data<int&, int&> d4(1, 2); // 调用特化的引用版本

	return 0;
}

三、模板的分离编译

3.1 什么是分离编译

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

3.2 模板的分离编译

在分离编译模式下,我们一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。
按照此方法,我们若是对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:
在这里插入图片描述
但是使用这三个文件生成可执行文件时,却会在链接阶段产生报错。

下面我们对其进行分析:
我们都知道,程序要运行起来一般要经历以下四个步骤:

  1. 预处理: 头文件展开、去注释、宏替换、条件编译等。对应Linux中生成.i文件
  2. 编译: 检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言。对应Linux中生成.s文件
  3. 汇编: 把编译阶段生成的文件转成目标文件。对应Linux中生成.o文件
  4. 链接: 将生成的各个目标文件进行链接,生成可执行文件。

以上代码在预处理阶段需要进行头文件的包含以及去注释操作。
在这里插入图片描述
这三个文件经过预处理后实际上就只有两个文件了,若是对应到Linux操作系统当中,此时就生成了 Add.i 和 main.i 文件了。
在这里插入图片描述
预处理后就需要进行编译,虽然在 main.i 当中有调用Add函数的代码,但是在 main.i 里面也有Add函数模板的声明,因此在编译阶段并不会发现任何语法错误,之后便顺利将 Add.i 和 main.i 翻译成了汇编语言,对应到Linux操作系统当中就生成了 Add.s 和 main.s 文件。

之后就到达了汇编阶段,此阶段利用 Add.s 和 main.s 这两个文件分别生成了两个目标文件,对应到Linux操作系统当中就是生成了 Add.o 和 main.o 两个目标文件。

前面的预处理、编译和汇编都没有问题,现在就需要将生成的两个目标文件进行链接操作了,但在链接时发现,在main函数当中调用的两个Add函数实际上并没有被真正定义,主要原因是函数模板并没有生成对应的函数,因为在全过程中都没有实例化过函数模板的模板参数T,所以函数模板根本就不知道该实例化T为何类型的函数。

模板分离编译失败的原因:
在函数模板定义的地方(Add.cpp)没有进行实例化,而在需要实例化函数的地方(main.cpp)没有模板函数的定义,无法进行实例化。

3.3 解决方法

解决类似于上述模板分离编译失败的方法有两个:

第一个就是在模板定义的位置进行显示实例化。

在这里插入图片描述

在函数模板定义的地方,对T为int和double类型的函数进行了显示实例化,这样在链接时就不会找不到对应函数的定义了,也就能正确执行代码了。

虽然第一种方法能够解决模板分离编译失败的问题,但是我们这里并不推荐这种方法,因为我们需要用到一个函数模板实例化的函数,就需要自己手动显示实例化一个函数,非常麻烦。

第二个方法那就是对于模板来说最好不要进行分离编译,不论是函数模板还是类模板,将模板的声明和定义都放到一个文件当中就行了。
例如:声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的

四、模板总结

【优点】:

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

【缺陷】:

  1. 模板会导致代码膨胀问题,也会导致编译时间变长。
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

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

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

相关文章

通达信SCTR强势股选股公式,根据六个技术指标打分

SCTR指标(StockCharts Technical Rank)的思路来源于著名技术分析师约翰墨菲&#xff0c;该指标根据长、中、短三个周期的六个关键技术指标对股票进行打分&#xff0c;根据得分对一组股票进行排名&#xff0c;从而可以识别出强势股。 与其他技术指标一样&#xff0c;SCTR的设计…

中国社科院与美国杜兰大学能源管理硕士项目是你职场通关的密码吗

职场是一场没有硝烟的战争&#xff0c;想要在职场取得取胜&#xff0c;就要拥有超能力。从职场小白晋升到管理层一路走来诸多不易&#xff0c;想要坐稳或升得更高&#xff0c;要不断提升自己能力&#xff0c;要不间断地学习。社科院与美国杜兰大学能源管理硕士项目是你通关的密…

navicat连接oracle报错 ORA-28547

报错 原因 Navicat自带的oci.dll并不支持oracle11g 具体操作 1. 先用idea连接oracle&#xff0c;查看oracle版本 select * from v$version; 2. 去官网下载 Instant Client 地址&#xff1a; Oracle Instant Client Downloads 下载 选择对应的版本&#xff08;下载时&#x…

未注册老域名扫描软件-免费未注册老域名挖掘

未注册老域名挖掘教程 在SEO优化中&#xff0c;老域名的价值不言而喻&#xff0c;它们的搜索引擎权重、离线广告效果等都比新域名更高。然而&#xff0c;如何挖掘出高质量的老域名并进行注册并非易事。今天&#xff0c;我们将介绍一款名为“147SEO老域名挖掘软件”的工具&…

【SpringBoot】二:自动配置

文章目录 1.自动配置类2. Import3. AutoConfigurationImportSelector4. AutoConfiguration 1.自动配置类 Spring Boot的自动装配机制会试图根据你所添加的依赖来自动配置你的Spring应用程序。 例如&#xff0c;如果你添加了Mysql依赖&#xff0c;而且你没有手动配置任何DataS…

从今天起,不再为 API 烦恼 !

做技术管理的童鞋&#xff0c;往往会陷入这样一种困境&#xff1a;疲于奔命&#xff0c;到处救火填坑&#xff0c;沟通推进&#xff0c;却挤不出时间思考对团队和项目来说真正重要的事情。 你有没有经历过这样的场景&#xff1a; 1. 下属老是改了接口但不维护文档&#xff0c;屡…

初探高并发—ExecutorCompletionService

初探高并发—ExecutorCompletionService 为什么要引入高并发 众所周知&#xff0c;程序中的代码是从下往下顺序执行的&#xff0c;当我们需要在一个方法中同时执行多个耗时的任务时所消耗时间就会大于等于这些任务消耗的累加时间。那么有没有一种办法可以让这些耗时的任务同时…

微信小程序入门04-后端脚手架搭建

我们上一篇已经介绍了权限系统的库表搭建&#xff0c;光有表还是不够的&#xff0c;我们还需要有一个后台系统和数据库进行交互。搭建后台的时候既需要选择使用什么语言&#xff0c;也需要选择框架。 框架分为前端框架和后端框架。在第一篇微信开发者工具搭建的时候我们其实前…

面试官:什么是防抖和节流?如何实现?应用场景?

防抖 与 节流 大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 前言 防抖和节流作为很多大厂的经典面试题&#xff0c;问倒了许多小伙伴&a…

【Python-ESL】python-esl安装

pip install python-esl 时会报错&#xff1a; “error: command ‘swig’ failed with exit status 1” 报错原因是 因为 swig 软件未正确安装&#xff0c;当然对swig的版本也是有要求的&#xff0c;目前测试以下版本没有问题&#xff1a; swig3.0.63 python-ESL1.4.18(app-…

域名年龄查询工具-域名历史查询工具

批量域名历史查询工具 在近几年的网络营销中&#xff0c;老域名已经成为获取网站排名和SEO优化的重要途径。而对于购买这些老域名&#xff0c;了解域名的过往经历&#xff0c;可以帮助我们更好地评估域名的价值&#xff0c;并避免购买不良的域名。因此&#xff0c;今天我们将向…

微信小程序入门03-搭建权限系统,建库建表

我们准备零基础搭建一个小程序&#xff0c;小程序分为两部分&#xff0c;一个是用户访问的程序&#xff0c;可以是小程序也可以是H5。另外一个就是管理员使用的管理后台&#xff0c;后台第一个要实现的就是搭建权限系统。为了搭建权限系统&#xff0c;我们先需要梳理概念 1 RB…

Oracle自增序列探秘:一篇文章教你读懂

目录 1&#xff1a;什么是Oracle 自增长序列 2 &#xff1a;创建数据-->实现自增长序列 2.1 创建序列 2.2 使用序列 3 &#xff1a;查询数据-->实现自增长序列 1&#xff1a;什么是Oracle 自增长序列 Oracle自增长序列是一种生成唯一数字的方法&#xff0c;可以用于创…

NXP MCUXPresso - .h: No such file or directory

文章目录 NXP MCUXPresso - .h: No such file or directory概述备注END NXP MCUXPresso - .h: No such file or directory 概述 在尝试迁移 openpnp - Smoothieware project 从gcc命令行 MRI调试方式 到NXP MCUXpresso工程. 快摸进门了. 按照C工程编译的. 头文件路径都加好…

Wijmo 2023 Crack添加的一些改进

Wijmo 2023 Crack添加的一些改进 改进了对React 18的支持-增加了对Reack 18严格模式的支持&#xff0c;这有助于开发人员在开发过程中发现常见的错误。 可访问性改进-以下是本版本中添加的一些改进&#xff1a; 改进了FlexGridFilter弹出窗口&#xff0c;用于按条件和值进行筛选…

文本三剑客之——sed编辑器

sed编辑器 sed编辑器sed基础语法sed查询sed删除sed 替换sed 插入 sed编辑器 sed是文本处理工具&#xff0c;依赖于正则表达式&#xff0c;可以读取文本内容&#xff0c;工具指定条件对数据进行添加、删除、替换等操作&#xff0c;被广泛应用于shell脚本&#xff0c;以完成自动…

【交直流保护用HJZ-Y910静态中间继电器 性能稳定功耗小 JOSEF约瑟】

品牌&#xff1a;JOSEF约瑟&#xff0c;型号&#xff1a;HJZ-Y910&#xff0c;名称&#xff1a;静态中间继电器&#xff0c;额定电压&#xff1a;48220VDC&#xff1b;48415VAC&#xff0c;触点容量&#xff1a;250V/5A&#xff0c;功率消耗&#xff1a;≤5W&#xff0c;动作时…

Linux的常见指令(下)

常见指令以及权限理解&#xff08;下&#xff09; 基础指令的继续学习&#xff0c;本篇博客是对于Linux的大部分常见指令的学习和使用&#xff0c;指令的选项都是比较常用的&#xff0c;基本的复制移动&#xff0c;删除文件or目录&#xff0c;查看文件的三种方式cat、more、le…

区块链技术方向的就业前景

区块链技术是一个快速发展的领域&#xff0c;目前正在被越来越多的企业和组织广泛应用。区块链技术在金融、物流、医疗、社交媒体等众多领域都有着广泛的应用。因此&#xff0c;区块链技术方向的就业前景非常乐观。 区块链技术是新一代信息技术的重要组成部分&#xff0c;区块…

SpringBoot 发送邮件(四十三)

从头开始&#xff0c;并不意味着失败&#xff0c;相反&#xff0c;正是拥抱成功的第一步&#xff0c;即使还会继续失败 上一章简单介绍了 SpringBoot 整合 ES (四十二), 如果没有看过,请观看上一章 一. 发送邮件 关于发送邮件的功能和基础知识&#xff0c;老蝴蝶这儿就不重点…