C++模板(函数模板,类模板)的基本使用与非类型模板参数与模板的特化

news2024/12/29 13:25:23

C++模板

  • 模板初阶
    • 泛型编程
    • 函数模板
      • 函数模板概念
      • 函数模板格式
    • 函数模板的原理
    • 函数模板的实例化
      • 隐式实例化
      • 显式实例化:在函数名后的<>中指定模板参数的实际类型
    • 模板参数的匹配原则
    • 类模板
      • 类模板的定义格式
      • 类模板的实例化
  • 模板进阶
    • 非类型模板参数
    • 模板的特化
      • 概念
      • 函数模板特化
      • 类模板特化
      • 全特化
      • 偏特化
    • 模板分离编译
      • 什么是分离编译
      • 模板的分离编译
      • 模板总结

在这里插入图片描述

📌————本章重点————📌
🔗了解泛型编程
🔗会基本使用函数模板和类模板
🔗知道函数模板的两种实例化
🔗了解非非类型模板参数与模板的特化
🔗了解模板分离编译
✨————————————✨

模板初阶

泛型编程

我们先来思考一个问题,如果有人让你实现一个通用的交换函数你们会怎么做?有的小伙伴会说,我会使用函数重载的方式,把每一种类型重载,但…难道就没有更容易的办法,不用敲那么多次吗?ok,就来看看我们今天要介绍的模板,能很好的帮你解决这个问题。

使用函数重载虽然可以实现,但是有一下几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错

这时候我们就需要告诉编译器一个模子,让编译器按照不同的类型来利用该模板生成代码!比如以下例子:

在这里插入图片描述
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

在这里插入图片描述

函数模板

函数模板概念

函数模板代表了一个函数家族,该函数模板与 类型 无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

函数模板格式

template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}

template<typename T>

void Swap( T& left, T& right)
{
 T temp = left;
 left = right;
 right = temp;
}

注意:typename是用来定义模板参数关键字,也可以使用class,编译器会自动判别类型(切记:不能使用struct代替class)

函数模板的原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
在这里插入图片描述
编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

注意:1.如果有多个函数参数时,可以在模板函数参数链表处多加一个参数,也可以使用强转。
2.函数模板不是一个函数,而是一个模具,是编译器更具实例化自动生成的规则

函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

隐式实例化

让编译器根据实参推演模板参数的实际类型

template<class T>

T Add(const T& left, const T& right)
{
 return left + right;
}
//注意这里使用const引用,这里相当于是个万能引用,因为它既可以调用变量,也可以调用常量,而这里
//因为后边调用的是常量,所以必须采用const修饰过的

int main()
{
 int a1 = 10, a2 = 20;
 double d1 = 10.0, d2 = 20.0;
 Add(a1, a2);
 Add(d1, d2);
 
 /*

 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,
 通过实参d1将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int 或者
  double类型而报错注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
 Add(a1, d1);

 */

 
 // 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化

 Add(a, (int)d);
 return 0;
}

显式实例化:在函数名后的<>中指定模板参数的实际类型

:明确指明了参数的类型,如果传入的参数不符合,会自动强转成规定类型

int main(void)
{
	int a = 10;
	double b = 20.0;
	// 显式实例化
	Add<int>(a, b);
	return 0;
}

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。

函数模板的编译原理:
1.在实例化之前:只是对模板进行简单的语法检测
2.在实例化之后:根据用户对函数模板的实例化结果来生成处理对应类型的代码,并对这些代码进行再次编译

模板参数的匹配原则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数

int Add(int left, int right)
{
	return left + right;
}


// 通用加法函数

template<class T>

T Add(T left, T right)
{
	return left + right;
}


void Test()
{
	Add(1, 2); // 与非模板函数匹配,编译器不需要特化

	Add<int>(1, 2); // 调用编译器特化的Add版本

}
  1. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数

int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T1, class T2>

T1 Add(T1 left, T2 right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函}
  1. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

类模板

类模板的定义格式

template<class T1, class T2, ..., class Tn>

class 类模板名

{
	// 类内成员定义

};

== 举例:==

// 动态顺序表

// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具

template<class T>

class Vector

{
public:
	Vector(size_t capacity = 10)
		: _pData(new T[capacity])
		, _size(0)
		, _capacity(capacity)
	{}
	// 使用析构函数演示:在类中声明,在类外定义。
	~Vector();
	void PushBack(const T& data)void PopBack()// ...
		size_t Size() { return _size; }
	T& operator[](size_t pos)
	{
		assert(pos < _size);
		return _pData[pos];
	}
private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
	if (_pData)
		delete[] _pData;
	_size = _capacity = 0;
}

类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的 类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类

/ Vector类名,Vector<int>才是类型

 Vector<int> s1;
 Vector<double> s2;

模板进阶

非类型模板参数

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

namespace bite

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

	template<class T, size_t N = 10>

	class array

	{
	public:
		T& operator[](size_t index) { return _array[index]; }
		const T& operator[](size_t index)const { return _array[index]; }

		size_t size()const { return _size; }
		bool empty()const { return 0 == _size; }

	private:
		T _array[N];
		size_t _size;
	}}

注意:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。

  2. 非类型的模板参数必须在编译期就能确认结果。

模板的特化

概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板

// 函数模板 -- 参数匹配

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;
}

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。此时,就需要对 模板进行特化即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。

函数模板特化

函数模板的特化步骤:

  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;
}

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。

bool Less(Date* left, Date* right)
{
 return *left < *right;
}

该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。

类模板特化

全特化

:全特化即是将模板参数列表中所有的参数都确定化。

template<class T1, class T2>

class Data

{

public:
	Data() { cout << "Data<T1, T2>" << endl; }

private:
	T1 _d1;
	T2 _d2;
};


template<>

class Data<int, char>

{

public:
	Data() { cout << "Data<int, char>" << endl; }

private:
	int _d1;
	char _d2;
};


void TestVector()
{
	Data<int, int> d1;//按照最开始的方法生成
	Data<int, char> d2;//按照特化后的方法生成
}

偏特化

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:

template<class T1, class T2> 

class Data

{

public:
 Data() {cout<<"Data<T1, T2>" <<endl;}

private:
 T1 _d1;
 T2 _d2;
};

偏特化有以下两种表现方式:
部分特化:将模板参数类表中的一部分参数特化。

/ 将第二个参数特化为int

template <class T1> 

class Data<T1, int>

{

public:
 Data() {cout<<"Data<T1, int>" <<endl;}

private:
 T1 _d1;
 int _d2;
}; 

参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

//两个参数偏特化为指针类型 

template <typename T1, typename T2>

class Data <T1*, T2*>
{

public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1typename 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;
};
void test2()
{
	Data<doubleint> d1;//调用特化的int版本
	Data<intdouble> d2;//调用基础的模板
	Data<int*int*> d3;//调用特化的指针版本
	Data<int&int&> d4(1, 2); // 调用特化的指针版本
}

模板分离编译

什么是分离编译

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

模板的分离编译

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

// a.h

template<class T>

T Add(const T& left, const T& right);

// a.cpp
template<class T>

T Add(const T& left, const T& right)
{
 return left + right;
}
 

// main.cpp

#include"a.h"

int main()
{
 	Add(1, 2);
	Add(1.0, 2.0);
 
 return 0;
}

分析:
在这里插入图片描述
== 解决方法:==

  1. 将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种。

  2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。

模板总结

【优点】

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

  2. 增强了代码的灵活性

【缺陷】

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

  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
    在这里插入图片描述

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

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

相关文章

【TypeScript】TS与Vue

TypeScript与Vue 文章目录TypeScript与VuedefineProps与TypescriptdefineEmits与Typescriptref与Typescriptreactive与Typescriptcomputed与Typescript事件处理与TypescriptTemplate Ref与Typescript可选链操作符非空断言参考链接&#xff1a;https://vuejs.org/guide/typescri…

【OpenGL学习】texture

纹理 一、什么是纹理&#xff1f; 引用百度百科的定义&#xff1a; 计算机图形学中的纹理既包括通常意义上物体表面的纹理即使物体表面呈现凹凸不平的沟纹&#xff0c;同时也包括在物体的光滑表面上的彩色图案&#xff0c;通常我们更多地称之为花纹。对于花纹而言&#xff0c…

ES6 课程概述⑦

文章目录Vuex_State安装使用State在 Vue 组件中获得 Vuex 状态mapState 辅助函数Vuex_Getter通过属性访问通过方法访问mapGetters 辅助函数Vuex_Mutation在组件中提交 Mutation提交载荷&#xff08;Payload&#xff09;对象风格的提交方式使用常量替代 Mutation 事件类型Mutati…

Spring Boot(五十六):基于Redis的搜索栏热搜功能

1 功能要求 使用SpringBoot和redis实现一个简单的热搜功能&#xff0c;具备以下功能&#xff1a; 搜索栏展示当前登陆的个人用户的搜索历史记录&#xff0c;删除个人历史记录用户在搜索栏输入某字符&#xff0c;则将该字符记录下来 以zset格式存储的redis中&#xff0c;记录该…

Flink DataSet API和DataStream API 对于WordCount的演示

文章目录准备工作Flink DataSet APIFlink DataStream API结论准备工作 pom依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-insta…

如何多人配音一个作品?这3招帮你快速实现

大家平时喜欢听书吗&#xff1f;听书是一种既能释放双眼&#xff0c;又能降低压力的放松方式。那么大家平时在听书的时候&#xff0c;有没有碰到过一些多人配音的小说&#xff1f;大家有好奇过这样的小说是怎么来的吗&#xff1f;今天&#xff0c;教大家多人配音怎么制作的&…

请问想考软考,零基础的话,哪个证书最好考呢

可以直接考中级&#xff0c;软考中级中也有适合零基础报考的&#xff0c;中级的含金量也比初级的高&#xff0c;初级的用途不太大&#xff0c;建议直接中级。 系统集成项目管理工程师&#xff0c;软考中级比较热门的一个科目&#xff0c;零基础的也适合相比较容易通过。 软考…

Fisher确切概率基本原理详解

Fisher确切概率 基本原理 比较两组有效率是否有差异。 在周边合计不变的情况下&#xff0c;计算实际频率变动时的Pi&#xff08;概率&#xff09;。然后计算累积概率&#xff0c;依据检验水平做推断。 累积概率的计算 以a从小到大的概率排序 左侧概率&#xff1a;现有样本…

【SpringCloud17】SpringCloud Alibaba入门简介

1.为什么会出现SpringCloud Alibaba Spring Cloud Netflix项目进入维护模式官网 1.1 什么是维护模式 将模块置于维护模式&#xff0c;意味着 Spring Cloud 团队将不会再向模块添加新功能。我们将修复 block 级别的 bug 以及安全问题&#xff0c;我们也会考虑并审查社区的小型 …

shell处理多盘跑fio(minimal)的结果脚本编写

作为一个专业测试storage的测试人员&#xff0c;除了对服务器&#xff0c;硬盘熟悉之外&#xff0c;还要对测试工具fio特别熟悉才行。如果在OEM或者专门的HDD&SSD厂家测试&#xff0c;会经常看到测试脚本里边&#xff0c;开发喜欢用fio minimal 模式&#xff0c;这样解析lo…

【GD32F427开发板试用】利用SPI驱动ADS8354

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;STY 前言 本文期望通过板载硬件SPI外加DMA传输的方式来实现对全差分同步采样模数转换器ADS8354的控制&#xff0c;并且将采集数据进行初步脉冲…

2.1、进程的定义、组成、组织方式、特征

整体框架 1、进程的定义 程序\color{red}程序程序&#xff1a;就是一个指令序列 早期的计算机&#xff08;只支持单道\color{red}单道单道程序&#xff09; 引入多道\color{red}多道多道程序之后&#xff1a; 为了方便操作操作系统管理&#xff0c;完成各个程序并发执行、 引…

Go map 实现原理

Go map 实现原理 go map 源码路径在&#xff1a; src/runtime/map.gogo 源码结构 |– AUTHORS — 文件&#xff0c;官方 Go语言作者列表 |– CONTRIBUTORS — 文件&#xff0c;第三方贡献者列表 |– LICENSE — 文件&#xff0c;Go语言发布授权协议 |– PATENTS — 文件&…

如何解决Prometheus的数据回填问题

去年10月底的时候&#xff0c;我们的监控系统因为一个偶然的问题&#xff0c;出乎意料地发生了重大的故障&#xff0c;这次故障暴露了当前监控系统存在的一下重大隐患。故障背景及现象我们的监控系统基于Thanos构建&#xff0c;基本架构如下&#xff08;箭头表示数据流向&#…

Docker安装Mysql8.0主从复制

1使用portainer快速创建mysql 2.mysql-master version: 3.3 services:mysql-app:image: mysql:8.0container_name: mysqlrestart: alwaysports:- 3307:3306environment:MYSQL_ROOT_PASSWORD: 123456 # root用户的密码MYSQL_ROOT_HOST: % # 访问权限# MYSQL_USER: test …

[leetcode]刷题--关于位运算的几道题

&#xff08;1&#xff09;位运算的本质&#xff0c;其实是对二进制补码储存形式的修改。 位运算常见的运算符为 <<左移n个位置&#xff08;算数移位&#xff0c;符号位不变&#xff09; >>右移动n个位置&#xff08;采用直接丢弃末尾数字的方法&#xff0c;符号…

Android Raphael使用(专治native 内存泄漏)

1.前期准备 在项目根目录build.gradle中,添加仓库地址&#xff1a; allprojects {repositories {maven { url https://jitpack.io }} }2.案例实践 构建一个新的Library Module&#xff0c;其中build.gradle中添加依赖&#xff1a; dependencies {implementation com.github…

gitlab-runner搭建CI/CD

1. 背景 每次发布代码&#xff0c;需要连接服务器更新代码&#xff0c;进行部署&#xff0c;比较繁琐&#xff0c;浪费时间。方案有jenkins或gitlab-runner。由于代码仓库是gitlab并且只需要自动部署&#xff0c;不需要其他额外功能&#xff0c;这里选择使用gitlab-runner。 …

【React】三.React组件基础学习

目录 React组件介绍 React组件的两种创建方式 使用函数创建组件 函数组件 渲染函数组件 示例 使用类创建组件 抽离为独立的JS文件 步骤 问题记录 React事件处理 事件绑定 记录问题 事件对象 有状态组件和无状态组件 无状态组件&#xff08;木偶组件&#xff09;…

XSS(Cross Site Scripting)攻击简介

环境 Ubuntu 22.04IntelliJ IDEA 2022.1.3JDK 17.0.3.1Spring Boot 3.0.1Firefox 108.0.2 问题和分析 在IntelliJ IDEA中创建Spring Boot项目 test0116 &#xff0c;并选中 Spring Web 依赖。 在 src/main/java 下创建 MyController.java 如下&#xff1a; package com.ex…