【C++】模板(相关知识点讲解 + STL底层涉及的模板应用)

news2024/11/14 17:21:55

目录

模板是什么?

模板格式

模板本质

函数模板

格式介绍

显式实例化

模板参数匹配原则

类模板

类模板的实例化

非类型模板参数

模板特化——概念

函数模板特化

类模板的特化

全特化

半特化

偏特化

三种类特化例子(放一起比较)

模板分离编译

STL中比较经典的模板应用(不包含argus)

容器适配器

仿函数

结语


​​​​​​​

模板是什么?

假设我们要写一个函数,这个函数的参数我们设置了两个int

但假如我现在在main函数里面调用的时候,我不光想传两个int,我想传一个int,一个double,再或者我想一个传double,一个传float

但是我的函数参数只写了两个int

void func(int i, int j)
{
	cout << "hello world" << endl;
}

int main()
{
	func(1, 2);
	return 0;
}
hello world

想要解决这种情况只有两个方法:

  1. 每种参数的函数都写一遍
  2. 模板

什么是模板,模板就是我们自己当老板,让编译器帮我们打工

template<class T1, class T2>
void func(T1 i, T2 j)
{
	cout << "hello world" << endl;
}

int main()
{
	func(1, 2);
	func(1.1, 2);
	func(1.1, 2.2);
	func(1.1, 'x');
	return 0;
}

模板格式

我们要写模板的话,得按照如下格式:

template<class T1, class T2>

如果要加参数的话就在后面加,如果要加缺省值也可以,这个我们后面再讲(STL中的容器适配器就是一个例子)

当然我们也可以将class换成typename

template<typename T1, typename T2>

目前来讲,两者并没有区别,所以我们写class即可(单词少)

模板本质

模板的本质就是,我们写了一个类模板或是一个函数模板,在我们看来我们是只写了一份,但是编译器就会在我们编译之后,根据我们传的参数,在背后默默实现出多份

举个例子:

template<class T1, class T2>
void func(T1 i, T2 j)
{
	cout << "hello world" << endl;
}

int main()
{
	func(1, 2);      //int int
	func(1.1, 2);    //double int
	func(1.1, 'x');  //double char
	return 0;
}

在我们眼里,这是一份

在编译器眼里,代码长这样:

void func(int i, int j)
{
	cout << "hello world" << endl;
}

void func(double i, int j)
{
	cout << "hello world" << endl;
}

void func(double i, char j)
{
	cout << "hello world" << endl;
}

也就是说,编译器就是在背后默默打工,我们不苦,苦了编译器而已

函数模板

格式介绍

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

如上这就是我们的函数模板,我们写的模板参数T在函数里面可以直接当成类型去传

到时候我们传了什么参数给编译器,编译器就将T实例化成什么

但是这时我们会遇到一个问题,如果我模板参数只写了一个T

按理来说,我们传的应该就是两个一样的对象是吧

但是这时我就不,我就要传两个不一样的,我传一个int,一个double,那在编译器看来,就不知道你这个T想变成什么了

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

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

	return 0;
}

显式实例化

像上面的代码,我们只有两个解决方法:

  1. 我们自己传过去的时候强转——a1,   (int)d1
  2. 显示实例化
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	
    //方法一
    Add(a1, (int)d2);

    //方法二
	Add<int>(a1, d2);

	return 0;
}

我们可以看到,显示实例化就是在函数后面加一个尖括号,然后里面写的类型就是我们想让模板参数成为的类型

比如模板参数只有一个T,这时我显式实例化传了一个int,那么int就是T的类型

如果类型不匹配的话,编译器会尝试强转,如果强转不了,就报错

模板参数匹配原则

我们的模板也是有匹配原则的

比如我很喜欢吃牛肉,但是今天家里面没有牛肉,这时我吃一桶泡面一顿就勉强过去了是不是也可以

但是如果我家这时刚好有牛肉,那我是不是就不吃泡面了呀(假设只能二选一)

如果其没有牛肉,也没有方便面,只有你最讨厌的肥猪肉,你闻一下都感觉恶心,那这顿是不是就不在家里吃了,只能出去觅食了

编译器也是这样的,有最合适的模板,就用最合适的,如果没有合适的,强转一下也能用,那也行

要是根本就没有匹配的,那编译器就只能报错了

template<class T>
void Add(const T& left, const T& right)
{
	cout << "T" << endl;
}

void Add(const int& left, const double& right)
{
	cout << "int  double" << endl;
}


int main()
{
	
	Add(1, 1);
	Add(1, 1.1);

	return 0;
}

注意,在模板调用的时候,会优先调用非模板参数(如果匹配的话)

另外,一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

int Add(int left, int right)
{
	cout << "1" << endl;
	return left + right;
}

template<class T>
T Add(T left, T right)
{
	cout << "2" << endl;
	return left + right;
}

int main()
{
	Add(1, 1);
	Add<int>(1, 1);
	return 0;
}

类模板

函数模板其实是一个大坑,稍有不注意的话,就会狠狠报错

相比之下,更多人会更愿意直接使用函数(非模板)

但是类模板就不一样了,这个可就太牛了

template<class T1, class T2, ..., class Tn>
class 类模板名
{
    类内成员定义
}; 

如上是类模板的格式

template<class T>
class date
{
public:
	date(T year, T month, T day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "类模板" << endl;
	}

private:
	T _year;
	T _month;
	T _day;
};


int main()
{
	date<int> d1(1, 1, 1);
	return 0;
}

注意,类模板是一定需要显示实例化的(除非有缺省值)

我们可以看到,在这个date类里面,我们将其显式实例化为int,所以里面的内容都会变成int

但是如果我们此时有这么一个需求:我们类里面的一些函数,我们不想在类里面实现,我想在类外面实现,因为这些函数太长了,我在外面实现,里面会简洁且美观

这时,我们就需要在函数前面加上类域限定,并且在函数上面我们还要加上类模板

如下(就拿上面date函数的析构来举例):

template<class T>
class date
{
public:
	date(T year, T month, T day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "类模板" << endl;
	}

	~date();

private:
	T _year;
	T _month;
	T _day;
};

template<class T>
date<T>::~date()
{
	cout << "~date()" << endl;
}

类模板的实例化

vector<int> v1;
vector<double> v2;
vector<string> v3;

如上,我们类模板是需要显示实例化的,因为他不像函数模板那样子,可以根据传的参数去一定程度上判断,所以是必须要传的!!

我们需要记住的是,我们使用模板实现的类只是一个模具

而编译器则会根据这些模具实现出各种不同的类,也就是我们当老板,编译器当打工人

我们不苦,写一个类就好,编译器苦,编译器要在底层实现很多个类

非类型模板参数

模板参数分为两种:

  1. 类型形参
  2. 非类型形参

类型形参就是我们上面一直在写的 template<class T>

非类型形参就是一个常量传过去  template<class T, size_t N = 10>

值得注意的是,在C++20以前,浮点数,类对象,字符串这些,是不能作为非类型模板参数的

但是在C++20之后又支持了(不是所有编译器都支持C++20)(所以我们日常最好不要这么用)

模板特化——概念

我们在日常写代码的时候,会遇到一些情况,比如我们在使用堆(优先级队列)的时候,会用到仿函数,这时我们的标准库里的仿函数可能并不满足我们的要求,所以我们就需要自己再动手写一个

我们可以这么理解:特化就是在原模板类的基础上,针对特殊类型所进行特殊化的实现方式

比如,我在外面做手工艺品(小熊),一般情况下小熊的耳朵我都是涂的棕色,这时有一个客户过来说要白色的,我就只能”特化“一个白色耳朵的小熊给客户

函数模板特化

函数模板特化就是直接生成一个有指定需求的函数出来,如下:

struct Date
{
	int year;
	int month;
	int day;

	bool operator<(Date& d1)
	{
		return d1.year < year;
	}
};

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

/函数模板的特化     举例
template<>
bool myless<Date*>(Date* & left, Date* & right)
{
	return *left < *right;
}

特化有这么几个步骤:

  1. template后面的东西清空,留一对尖括号
  2. 在函数名后面加一对尖括号,里面写上要特化的类型

看着挺好的,但其实这是一个大坑啊!!!

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


template<>
bool myless<Date*>(Date* const & left, Date* const& right)
{
	return *left < *right;
}

试想一下,如果我们加上了const呢?

上面的代码是正确的,但是大多数人在写的时候,会将const写到Date*的前面

但其实我们要想明白的一点是,我们const修饰的是指针本身,当我们类型为T的时候,修饰的就是T,但是如果是指针的话,如果const在*前面,那么修饰的就是指针所指向的值,如果在*后面的话,那么修饰的就是指针本身

类模板的特化

类模板的特化分为了几种:

  1. 全特化
  2. 偏特化(部分特化)——下文叫半特化
  3. 偏特化(进一步限制参数)

全特化

// 全特化
template<class T1, class T2>
struct Date
{
	Date()
	{
		cout << "Date<T1, T2>" << endl;
	}
};

template<>
struct Date<int, char>
{
	Date()
	{
		cout << "Date<int, char>" << endl;
	}
};

首先我们要知道的是,特化是需要原模板的,没有原模板就不能特化

首先我们来看一看全特化

全特化就是将所有的参数都限制死,就必须是这个类型才能调用这个特化,一般情况下都是拿来做特殊处理使用

半特化

顾名思义,半特化就是特化一半,另一半还是类模板参数,如下:

// 半特化
template<class T>
struct myless<T, int>
{
	myless() { cout << "半特化" << endl; }

	bool operator()(T& x, int& y)
	{
		return x < y;
	}
};

我们可以看到,这里和全特化的区别就是,全特化是全固定死的,但是半特化这里是只有指定数量的是固定死的,另一部分就还是模板

偏特化

偏特化就比较特殊了,一般情况下用来表示一类数据

// 偏特化
template<class T1, class T2>
struct myless<T1*, T2*>
{
	myless() { cout << "偏特化" << endl; }

	// 此处的T类型不为T*,而是T
	// 如果我此时传过来的是int*,则此时T的类型为int
	bool operator()(T1* x, T2* y)
	{
		return *x < *y;
	}
};

如上代码表示的是:只要你是指针类型,就走我这个特化

但是有一点需要注意,就是,我们上面特化的是T1*,T2*,这时假设我们传的是一个int*过去,这时我们的T就是int,而不是int*

这时因为如果T为int的话,我们就能通过自己控制来整出int对象和int*对象,但是如果T是int*的话就只能是指针了

我们将三者结合到一起来看一看

三种类特化例子(放一起比较)

// 原模版
template<class T1, class T2>
struct myless
{
	myless() { cout << "原模版" << endl; }
	bool operator()(T1& x, T2& y)
	{
		return x < y;
	}
};

// 全特化
template<>
struct myless<char, double>
{
	myless() { cout << "全特化" << endl; }

	bool operator()(char& x, double& y)
	{
		return x < y;
	}
};

// 半特化
template<class T>
struct myless<T, int>
{
	myless() { cout << "半特化" << endl; }

	bool operator()(T& x, int& y)
	{
		return x < y;
	}
};

// 偏特化
template<class T1, class T2>
struct myless<T1*, T2*>
{
	myless() { cout << "偏特化" << endl; }

	// 此处的T类型不为T*,而是T
	// 如果我此时传过来的是int*,则此时T的类型为int
	bool operator()(T1* x, T2* y)
	{
		return *x < *y;
	}
};

int main()
{
	myless<int, char> ml;//原模版
	myless<char, double> m2;//全特化
	myless<int, int> m3;//半特化
	myless<int*, int*> m4;//偏特化
	myless<int**, int**> m5;//偏特化
	return 0;
}

模板分离编译

模板分离编译,说简单点就是:我在.h文件里面声明了模板,但是在.cpp文件里面写出模板的定义

就是把模板声明的声明和定义分离到两个文件

这时候,大坑就来了

我们来看这么一个例子:

首先我们先创建三个文件:一个头文件(.h)两个.cpp文件

我们在.h文件里面声明了两个函数,一个是普通函数,一个是带模板的函数

然后我们在test.cpp这里调用这两个函数,但是我们会发现,报错了

只调用一个普通函数就不会

这就说明,编译器在模板函数声明定义分离的情况下,是找不到的

我们来简单分析一下:

那编译器为什么不编译模板呢???

只要编译器去编译模板,那就会生成地址,就能解决问题了

友友们,在我们的未来,我们要面对的,可能是成百上千个文件,每个文件可能都有成千上万行

编译器可以去一个一个文件地去找,这个模板的声明,对应的实例化在哪里,可以

但是这时,假设我们不分离编译的话,编个代码就几秒钟,但是一个一个文件去找的话,可能就需要半个小时了,这不夸张、

所以解决这个问题最好的办法就是,把模板的声明和定义写在同一个文件里,这样子编译器就能直接找到声明和定义,就能解决问题

STL中比较经典的模板应用(不包含argus)

容器适配器

这个我们在实现栈和队列,优先级队列的底层的时候会用到,这个其实就是:

将其他的数据结构当成一个模板参数传过来

template<class T, class container = vector<T>>

我们可以看到,上述代码中,我们将vector作为一个容器传给了模板作为参数,甚至我们还可以给缺省值,如果我们不传的话,就默认是vector,如果传的话,就以我们传的为准

如果有对容器适配器的具体应用感兴趣的话,可以看看下面这两篇文章:

一篇是栈和队列的底层实现,一篇是堆(优先级队列)的底层实现

【STL】| C++ 栈和队列(详解、deque(双端队列)介绍、容器适配器的初步引入)

【C++】STL | priority_queue 堆(优先级队列)详解(使用+底层实现)、仿函数的引入、容器适配器的使用

仿函数

这个在堆、AVL树、红黑树中都有用到

具体就是,我们可以写一个类模仿函数的行为,也就是在类里面重载一个operator()

然后我们就可以将这个类作为模板的其中一个参数,然后在模板所在的那个类里面调用仿函数对应的逻辑

template<class T>
	struct myless
	{
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};

	template<class T>
	struct mygreater
	{
		bool operator()(const T& x, const T& y)
		{
			return x > y;
		}
	};

	template<class T, class container = vector<T>, class compare = myless<T>>

如果对仿函数的应用较为感兴趣的话,同样可以看看下面这篇文章(是堆的底层实现)

【C++】STL | priority_queue 堆(优先级队列)详解(使用+底层实现)、仿函数的引入、容器适配器的使用

结语

到这里,我们这篇博客就结束啦!~( ̄▽ ̄)~*

如果感觉对你有帮助的话,希望可以多多支持博主喔!(○` 3′○)

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

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

相关文章

在 CentOS 7 上安装 Redmine 的详细步骤及 20 个经典用法

目录 1. 引言 2. 安装步骤 2.1 更新系统 2.2 安装依赖包 2.3 安装 MariaDB 数据库 2.4 配置 MariaDB 2.5 安装 Ruby 2.6 安装 Redmine 2.7 配置 Redmine 2.8 安装 Bundler 和必要的 Gems 2.9 生成密钥并迁移数据库 2.10 配置 Apache 2.11 启动 Apache 并设置开机自…

1712系列 嵌入式电源系统

1712系列 嵌入式电源系统 2/3/4/5G&共享站点快速部署 简述 1712A 300A嵌入式电源系统采用模块化设计、组合式结构&#xff0c;由控制器、整流模块、交流配电单元、直流配电单元等组成。该系统将交流电转换成稳定的-48V直流电&#xff0c;用于铁塔、移动、电信、联通等公司…

修改nacos实力权重或者对某实例下线报错

在Nacos控制台进行上述操作&#xff0c;错误信息 caused: errCode: 500, errMsg: do metadata operation failed ;caused: com.alibaba.nacos.consistency.exception.ConsistencyException: The Raft Group [naming_instance_metadata] did not find the Leader node;caused:…

sql注入大总结【万字详解】

文章目录 数据库的架构sql注入概念正常语句正常回显页面在页面中使用sql语句 跨库查询sql文件读写影响条件复现读写的路径的问题 sql注入请求分类sql注入请求类型sql注入请求方式&#xff1a;sql注入数据请求格式 数据库的增删改查数据库查询数据库添加数据库删除数据库修改 盲…

【python函数】读文件(返回str数据)

大家好&#xff0c;我是一名_全栈_测试开发工程师&#xff0c;已经开源一套【自动化测试框架】和【测试管理平台】&#xff0c;欢迎大家关注我&#xff0c;和我一起【分享测试知识&#xff0c;交流测试技术&#xff0c;趣聊行业热点】。 一、函数说明&#xff1a; 使用的函数&a…

STM32IIC与SPI详解

单片机里的通信协议其实蛮多的&#xff0c;IIC&#xff1b;SPI&#xff1b;MQTT&#xff1b;CAN&#xff1b;包括串口也是一种通信协议。而串口通信虽然实现了全双工&#xff0c;但需要至少三根线&#xff0c;为了节省这一根线的成本&#xff0c;于是IIC诞生了。 目录 一.IIC…

【产业前沿】树莓集团如何以数字媒体产业园为引擎,加速产业升级?

在数字化转型的浪潮中&#xff0c;树莓集团以敏锐的洞察力和前瞻性的战略眼光&#xff0c;将数字媒体产业园打造成为产业升级的强劲引擎。这一创新举措不仅为传统行业插上了数字的翅膀&#xff0c;更为整个产业链注入了新的活力与可能。 树莓集团深知&#xff0c;数字媒体产业园…

【人工智能】AI最终会取代程序员吗?

1. 前言 到 2030 年&#xff0c;40% 的编程任务将实现自动化。这个令人难以置信的统计数据凸显了人工智能在软件工程中日益增长的影响力&#xff0c;并引发了一个问题&#xff1a;人工智能会彻底接管软件工程吗&#xff1f; 人工智能技术正在蓬勃发展&#xff0c;有望实现大量…

【实战】Spring Security Oauth2自定义授权模式接入手机验证

文章目录 前言技术积累Oauth2简介Oauth2的四种模式授权码模式简化模式密码模式客户端模式自定义模式 实战演示1、mavan依赖引入2、自定义手机用户3、自定义手机用户信息获取服务4、自定义认证令牌5、自定义授权模式6、自定义实际认证提供者7、认证服务配置8、Oauth2配置9、资源…

C语言程序设计-[11] 循环结构嵌套

1、循环结构嵌套形式 上面三种循环语句结构可以相互嵌套&#xff0c;组合非常灵活。循环嵌套需要记住最重要的一点&#xff1a;”外循环执行一次&#xff0c;内循环要完整执行一遍”&#xff0c;要通过实例加深对这一句话的理解。 注1&#xff1a;一个循环结构由四个要素构成&…

Java设计模式-建造者模式-一次性理解透

1. 建造者模式简介 今天我们将研究 Java 中的建造者模式&#xff08;Builder 模式&#xff09;。Builder 设计模式是一种创建型设计模式&#xff0c;也被称为生成器模式&#xff0c;类似于工厂模式和抽象工厂模式。 该模式用于创建复杂对象&#xff0c;允许用户创建不同类型的…

【Python】PyWebIO 初体验:用 Python 写网页

目录 前言1 使用方法1.1 安装 Pywebio1.2 输出内容1.3 输入内容 2 示例程序2.1 BMI 计算器2.2 Markdown 编辑器2.3 聊天室2.4 五子棋 前言 前两天正在逛 Github&#xff0c;偶然看到一个很有意思的项目&#xff1a;PyWebIo。 这是一个 Python 第三方库&#xff0c;可以只用 P…

100 Exercises To Learn Rust 挑战!准备篇

公司内部的学习会非常活跃&#xff01;我也参与了Rust学习会&#xff0c;并且一直在研究rustlings。最近&#xff0c;我发现了一个类似于rustlings的新教程网站&#xff1a;Welcome - 100 Exercises To Learn Rust。 rustlings是基于Rust的权威官方文档《The Rust Programming…

汽车免拆诊断案例 | 2010款劳斯莱斯古斯特车中央信息显示屏提示传动系统故障

故障现象  一辆2010款劳斯莱斯古斯特车&#xff0c;搭载N74发动机&#xff0c;累计行驶里程约为11万km。车主反映&#xff0c;起动发动机后组合仪表和中央信息显示屏均提示传动系统故障。用故障检测仪检测&#xff0c;发现发动机控制模块2&#xff08;DME2&#xff09;中存储…

SmartBI拓展包二开入门开发

前言 新接到一个项目拓展包三开的需求&#xff0c;没有相关经验&#xff0c;学习开发&#xff0c;本文尝试通过简单的定位以及指导&#xff0c;确定修改点 SmartBI帮助文档-拓展包开发 登录 http://localhost:18080/smartbi/vision/index.jsp后台配置 上传拓展包&#xff0…

MySQL和Redis的数据一致性

MySQL和Redis的数据一致性 多线程环境下的涉及读写的缓存才会存在MySQL和Redis的数据不一致问题 先删除缓存再更新数据库再延时删除缓存 线程一删除缓存线程一更新数据线程二开始查数据如果第二步线程一更新数据延时&#xff0c;那么线程二会重新从数据库加载数据&#xff0…

超好用的windows系统工具PowerToys

文章目录 Github地址基本介绍使用 Github地址 PowerToys 基本介绍 是windows官方好用的工具箱&#xff0c;包括各种工具 使用 要带上win键 此工具安装后每次运行电脑自启动&#xff0c;桌面没有快捷方式&#xff0c;只能右下角 窗口在上效果演示&#xff0c;会被蓝线框到…

基于GeoTools使用JavaFx进行矢量数据可视化实战

目录 前言 一、JavaFx展示原理说明 二、GeoTools的Maven依赖问题 三、引入Geotools相关的资源包 四、创建JavaFx的Canvas实例 五、JavaFx的Scene和Node的绑定 六、总结 前言 众所周知&#xff0c;JavaFx是Java继Swing之后的又一款用于桌面应用的开发利器。当然&#xff0…

江科大/江协科技 STM32学习笔记P22

文章目录 AD单通道&AD多通道ADC基本结构和ADC有关的库函数AD单通道AD.cmain.c连续转换&#xff0c;非扫描模式的AD.c AD多通道AD.cmain.c AD单通道&AD多通道 ADC基本结构 第一步&#xff0c;开启RCC时钟&#xff0c;包括ADC和GPIO的时钟&#xff0c;ADCCLK的分频器也需…

openvidu私有化部署

openvidu私有化部署 简介 OpenVidu 是一个允许您实施实时应用程序的平台。您可以从头开始构建全新的 OpenVidu 应用程序&#xff0c;但将 OpenVidu 集成到您现有的应用程序中也非常容易。 OpenVidu 基于 WebRTC 技术&#xff0c;允许开发您可以想象的任何类型的用例&#xf…