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

目录

模板是什么?

模板格式

模板本质

函数模板

格式介绍

显式实例化

模板参数匹配原则

类模板

类模板的实例化

非类型模板参数

模板特化——概念

函数模板特化

类模板的特化

全特化

半特化

偏特化

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

模板分离编译

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值