首页 > 编程语言 >【C++】模板(相关知识点讲解 + STL底层涉及的模板应用)

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

时间:2024-08-09 18:59:34浏览次数:17  
标签:知识点 函数 STL int 编译器 template 模板 特化

目录

模板是什么?

模板格式

模板本质

函数模板

格式介绍

显式实例化

模板参数匹配原则

类模板

类模板的实例化

非类型模板参数

模板特化——概念

函数模板特化

类模板的特化

全特化

半特化

偏特化

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

模板分离编译

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′○)

标签:知识点,函数,STL,int,编译器,template,模板,特化
From: https://blog.csdn.net/2302_80023639/article/details/141064003

相关文章

  • 移情别恋c++ ദ്ദി˶ー̀֊ー́ ) ——4.模板
    1.泛型编程如何实现一个通用的交换函数呢?voidSwap(int&left,int&right){inttemp=left;left=right;right=temp;}voidSwap(double&left,double&right){doubletemp=left;left=right;right=temp;}voidSwap(char&left,char&right)......
  • 【线段树合并/树上差分】[P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并
    【线段树合并/树上差分】P4556[Vani有约会]雨天的尾巴/【模板】线段树合并思路对\(x,y,lca(u,v),fa_{lca(u,v)}\)四个点进行树上差分,然后用线段树合并动态权值线段树。#include<bits/stdc++.h>usingnamespacestd;usingi64=longlong;template<classNode>str......
  • 线段树合并模板
    template<classNode>structPersidentSegmentTree{#definelc(u)tr[u].l#definerc(u)tr[u].rconstintn;inttot=0;vector<Node>tr;vector<int>root;PersidentSegmentTree():n(0){}PersidentSegmentTree(in......
  • C++标准模板库(STL)|容器|vector| queue|
    对STL进行总结,STL是standardtemplatelibrary的简写,是C++中的一个标准模板库,用于实现常用的数据结构和算法,它是C++程序员经常使用的一个工具箱。STL的主要目的是提高开发效率和代码质量,使得程序员可以更加便捷地完成常见的操作。里面包括:算法(algorithm)、容器(container)、仿函......
  • 七、1 ADC模数转换器介绍+有关知识点
    目录1、介绍(1)ADC,模拟信号转换为数字信号(2)DAC和PWM,数字信号转换为模拟信号(3)ADC的两个关键参数(4)(5)(6)(7)(8)2、逐次逼近型ADC(1)型号为ADC0809的普通ADC(2)STM32的ADC1)2)触发ADC开始转换的信号3)4)3、ADC通道与引脚的对应关系4、转换模式(4种)5、触发控制6、数据对齐7、转......
  • 大质数分解模板
    jiangly的(偷一下i64mul(i64a,i64b,i64m){returnstatic_cast<__int128>(a)*b%m;}i64power(i64a,i64b,i64m){i64res=1%m;for(;b;b>>=1,a=mul(a,a,m))if(b&1)res=mul(res,a,m);......
  • C# 设计模式之模板方法模式
    总目录前言在日常的工作中,有时候我们做PPT,做合同,做简历,如果我们自己从头去写这些文档,不免有些太过耗时耗力;大多时候都是去找相关的PPT模板,合同模板,简历模板,拿过来直接用。为什么可以使用模板,因此这些资料大部分的信息和信息框架都是一致的,我们只需要将自己差异化的内容填......
  • 黑神画Ⅱ--Unix 是下一代人工智能的模板吗?
    有一张图被用来描述GPT5比GPT4大多少,GPT3被描绘成一条大白鲨,GPT4被描绘成一条虎鲸,然后GPT5被描绘成一条座头鲸,这表明它们训练的数据量大幅增加。这是一个有趣的类比,因为它传达了规模的概念,但当你思考这些类比代表什么时,它就更加有趣了。GPT3是鱼类世界中的顶级捕食......
  • 创造智能对话:在LangChain中巧妙使用变量与模板
    创造智能对话:在LangChain中巧妙使用变量与模板在人工智能的世界里,对话管理是一项艺术,也是一项技术挑战。LangChain作为一个前沿的对话管理框架,提供了一套强大的工具,让开发者能够创建动态、个性化的对话体验。本文将深入探讨如何在LangChain中创建和管理变量,通过详细的步骤......
  • 网站源码医疗机构pbootcms模板网页设计主题
    医疗机构的网站设计分享我很高兴向大家介绍我刚刚制作的医疗机构的网站设计。友好的站点界面,是打动访客的第一步。医疗机构网站的主题网站设计需要充分考虑到医疗行业的特殊性和用户需求,以下是一个清晰的设计介绍:1.设计原则用户友好性:网站的设计和功能应便于用户理解和使......