【C++从0到王者】第二十站:模板进阶

文章目录


前言

在前面我们使用模板主要是为了解决两类问题。一类是解决类里面某个数据类型,可以使用模板。 第二类就不单单是控制某种数据类型,而是控制某种逻辑,比如我们的适配器模式:传一个正向迭代器,可以适配出反向迭代器。传一个普通的容器,可以适配出栈、队列、优先级队列等。这样的好处就是我们的栈不是死的。并不单单只是一个链式栈、或者顺序栈等等,或者传一个类型过去,这个类型可以仿造函数,即仿函数,一般这个类也就是一个普通的类,只不过其重载了()运算符,导致其生成的对象可以像函数一样进行调用。它可以控制sort的升序或降序,堆的大小堆

一、typename 和 class的一些区别

typename和class在绝大多数场景下都是没有区别的,但是在一些场景下还是存在一些区别的。

如下代码所示:

在我们想要写一个打印vector里面的数据的时候,我们会写出如下代码。

cpp 复制代码
#include<iostream>
#include<vector>
#include<list>
using namespace std;

void Print(const vector<int>& v)
{
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}
int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	Print(v);
	return 0;
}

确实上面方法挺好用的,但是我们这个Print是否可以利用模板往泛型去写呢?答案当然是可以的,于是我们可能就会写出这样的代码,结果当我们运行的时候,报错了。

cpp 复制代码
template<class Container>
void Print(const Container& v)
{
	Container::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

它的报错是这样的,提示说要在Container前加上typename

于是我们按照它的错误信息进行改成,代码就通过了

cpp 复制代码
template<class Container>
void Print(const Container& v)
{
	typename Container::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

那么为什么必须要加上typename呢?

这是因为编译器在编译的时候从上往下,在编译到这里的时候这里还没有Container实例化,那么此时编译器就区分不清楚Container是什么类型,之前是vector<int>的时候它以及被实例化出来了,所以不会报错。vector的话编译器就很清楚在vector里面找到这个迭代器类型即可。而现在Container没有实例化,那么此时就会出问题,就有两种可能性:一种可能就是这里是一个静态成员变量,一种就是类里面进行typedef出来的类型。也就是说,这里到底是类型还是静态成员变量是无法区分的。所以编译器要求加上typename告诉它这里是一个类型,说明这里是合乎语法的。等模板实例化以后再去找

在我们之前优先级队列里面其实也用到了typename

二、非类型模板参数

1.非类型模板参数介绍

有时候,我们需要一些不是类型的模板参数

比如在我们想要写一个静态栈的时候,我们之前需要将N进行一个宏定义,然后再类里面之间使用,现在我们可以使用非类型模板参数,直接传递一个N过去,从而修改N的容量

cpp 复制代码
template<class T,size_t N>
class Stack
{
private:
	T _a[N];
	int _top;
};
int main()
{
	Stack<int, 10> st1;
	Stack<int, 100> st2;

	return 0;
}

要注意这里的N是一个常量,不可以被修改的。否则报错。

非类型模板参数必须满足以下两点

  1. 必须是常量
  2. 必须是整型

2.array容器

数组是固定大小的序列容器:它们按照严格的线性顺序保存特定数量的元素。
在内部,数组不保留它所包含的元素以外的任何数据(甚至不保留它的大小,这是一个模板参数,在编译时固定)。就存储大小而言,它与使用该语言的括号语法([])声明的普通数组一样有效。这个类只是给它添加了一层成员函数和全局函数,这样数组就可以用作标准容器。
与其他标准容器不同,数组具有固定的大小,并且不通过分配器管理其元素的分配:它们是封装固定大小的元素数组的聚合类型。因此,它们不能动态地展开或收缩(有关可以展开的类似容器,请参阅vector)。
大小为零的数组是有效的,但它们不应该被解引用(成员front、back和data)。
与标准库中的其他容器不同,交换两个数组容器是一个线性操作,涉及单独交换范围内的所有元素,这通常是一个效率相当低的操作。另一方面,这允许两个容器中的元素的迭代器保持它们原来的容器关联。
数组容器的另一个独特特性是它们可以被视为元组对象:头重载get函数以访问数组的元素,就像它是一个元组一样,以及专门的tuple_size和tuple_element类型。

如上是关于这个容器的介绍,它就是采用了非类型模板参数,它支持的操作有下面这些

其实本质就是多加了一层函数。

array和普通的数组本质上没有太大区别,要说唯一的区别就是,对于越界的检查更加严格了。对越界读写都有检查,而普通数组不能检查越界读,少部分越界写可以检查。

三、模板的特化

1.函数模板的特化

有时候我们会遇到这样的场景

cpp 复制代码
template<class T>
bool Less(T a, T b)
{
	return a < b;
}
int main()
{
	int a = 2;
	int b = 1;
	cout << Less(a, b) << endl;
	cout << Less(&a, &b) << endl;
	return 0;
}

我们期望说,比较的时候即便是指针,也能比较里面的值,但是此时我们这里比较的是两个指针的大小

为了达到我们的期望,我们可以有多种方法进行处理

如下面就是使用了模板的特化

当遇到int*类型的时候,就走的是特化

cpp 复制代码
template<class T>
bool Less(T a, T b)
{
	return a < b;
}
template<>
bool Less<int*>(int* a, int* b)
{
	return *a < *b;
}
int main()
{
	int a = 2;
	int b = 1;
	cout << Less(a, b) << endl;
	cout << Less(&a, &b) << endl;
	return 0;
}

但是实际上,这样写特化不如直接就是一个函数重载更加来的方便

函数函数调用是有现成的就用现成的,没有现成的才用模板。

但是像下面这种情况就必须使用模板的特化了

即我们有时候还需要特化其他类型。就必须使用模板的特化来的更加方便

2.类模板的特化

1.全特化

如下所示,就是对Date类的特化。它的步骤也是一样的,需要对某种类型进行特殊处理。于是我们就写一个template<> ,然后比之前的Date多一个类型。这样我们就可以对某一类型特殊处理了

cpp 复制代码
template<class T1, class T2>
class Date
{
public:
	Date()
	{
		cout << "Date<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};


//对上面的类进行特化
template<>
class Date<int, double>
{
public:
	Date()
	{
		cout << "Date<int, double>" << endl;
	}
private:
	int _d1;
	double _d2;
};
int main()
{
	Date<int, int> d1;
	Date<int, double> d2;

	return 0;
}

运行结果如下所示

有了类了特化,这样我们之前的优先级队列就可以更加完善了。我们之前优先级队列的时候我们本身期望传入指针的时候而言按照指向的内容去比较。之前我们是直接替换了比较类,现在我们可以使用类的特化对前面进行加以修改

这样一来我们就可以进行正常比较了。(注意我们这里使用了域作用限定符,不然我们的就命名冲突了,会出事的)

像以上这些特化必须得有原模板以后才可以进行特化,向上面这种特化,将原来全部的模板参数给特化,这种特化也被称之为全特化

2.偏特化(半特化)

顾名思义,偏特化就是只特化一部分模板参数

cpp 复制代码
//偏特化
template<class T1>
class Date<T1, double>
{
public:
	Date()
	{
		cout << "Date<T1, double>" << endl;
	}
private:
	T1 _d1;
	double _d2;
};

如上代码所示,我们还是对前面的Date类进行特化,这次我们只特化一个参数,那么此时称之为半特化或偏特化

上面的偏特化的作用就是部分特化。这是偏特化的一种形式

偏特化其实有两种形式:

  1. 对模板参数做类表的一部分参数特化,即部分特化
  2. 参数的更进一步限制,即偏特化不仅仅指特化部分参数,而是针对模板参数的更进一步的条件限制所设计出来的一个特化版本

针对第二点,如下就是第二种特化形式

cpp 复制代码
template<class T1, class T2>
class Date<T1*, T2*>
{
public:
	Date()
	{
		cout << "Date<T1*, T2*>" << endl;
	}
};

有了偏特化的第二种形式的思想的,我们可以将前面优先级队列中的仿函数再次修改,只要是指针类型的,都进行特化

cpp 复制代码
	template<class T>
	class less
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};
	template<class T>
	class less<T*>
	{
	public:
		bool operator()(const T* x, const T* y)
		{
			return *x < *y;
		}
	};


	template<class T>
	class greater
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x > y;
		}
	};
	template<class T>
	class greater<T*>
	{
	public:
		bool operator()(const T* x, const T* y)
		{
			return *x > *y;
		}
	};

除了对指针类型的限制,还可以是对引用的限制,引用和指针混在一起的特化,以下是演示

cpp 复制代码
template<class T1, class T2>
class Date<T1&, T2&>
{
public:
	Date()
	{
		cout << "Date<T1&, T2&>" << endl;
	}
};
template<class T1, class T2>
class Date<T1&, T2*>
{
public:
	Date()
	{
		cout << "Date<T1&, T2*>" << endl;
	}
};
template<class T1, class T2>
class Date<T1*&, T2*>
{
public:
	Date()
	{
		cout << "Date<T1*&, T2*>" << endl;
	}
};

三、模板的分离编译

我们之前在C语言的时候特别喜欢声明和定义分类。在C++中,当我们试着分离的时候

编译器报错了报的是一个链接错误

但是如果调用size等接口的话,模板又正常了。不报错误。

可而得知,是类成员函数的声明和定义分离时出现的链接错误。即没有找到这个函数的地址。

这种错误就类似于我们定义了一个类,这个类是如下进行定义的,一个类声明了两个函数,但是只实现了一个函数。另外一个函数没有被实现。

于是此时我们的func2函数在调用的时候就会报错,且错误类型还是一样的。链接错误,即找不到地址。

这里其实就涉及到我们的编译链接过程了。在test.c文件中,对于stack类,它的其他成员函数在编译的时候就已经找到地址了。而push和pop都只有声明,在编译阶段都是没有地址的。

在编译阶段虽然他们没有地址,但是由于有声明,相当于一种承诺。所以自然不会报错

编译阶段只看声明, 声明是一种承诺,所以编译检查声明函数参数返回可以对上,等着链接的时候,拿着修饰后的函数去其他文件符号表查找

到了链接阶段我们此时的现象是

  1. func1链接查到了
  2. func2链接没有查到。因为func2没有定义
  3. push链接查不到,但是我们的push定义了

那么为什么会出现第三中情况的,我们究其原因,是因为他们是分别编译的。stack.o文件就没有生成地址,因为压根就不知道这个T是什么类型的,就没办法去生成地址。没法实例化

那么如何解决呢?其实我们可以显式实例化,即我们直接在函数是实现中,写一个template,注意不要带尖括号,然后class stack<int>即可

但是这里还是存在一些问题的,因为治标不治本,如果我们在主函数中又用一个double类型的,那么又要添加一个显式实例化。

cpp 复制代码
namespace Sim 
{
	template<class T ,class Container>
	void stack<T,Container>::push(const T& val)
	{
		_con.push_back(val);
	}
	template<class T, class Container>
	void stack<T,Container>::pop()
	{
		_con.pop_back();
	}

	template
	class stack<int>;

	template
	class stack<double>;
}

模板的声明和定义如果通过分文件的方式,显然是不太合适的。我们如果要将其分类,可以在同一个文件内进行分离。

这样是由于test文件是知道模板要实例化为什么类型的,所以就不用进行显式实例化了

cpp 复制代码
namespace Sim
{
	template<class T, class Container = deque<T>>
	class stack
	{
	public:
		void push(const T& val);
		void pop();
		const T& top()
		{
			return _con.back();
		}
		size_t size()
		{
			return _con.size();
		}

		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
	template<class T, class Container>
	void stack<T, Container>::push(const T& val)
	{
		_con.push_back(val);
	}
	template<class T, class Container>
	void stack<T, Container>::pop()
	{
		_con.pop_back();
	}

};

即便是stl库里面,也是这样做的,小函数定义在类里面,大函数定义在类外面,但是声明和定义分离是放在同一个文件的。

有时候我们会看见这些模板的库的后缀是.hpp,意思就是声明和定义放在一个文件中,这只是一个名字的暗示。

四、总结

【优点】

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

【缺陷】

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

好了本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!
有任何关于文章的问题,可以直接私信我或者评论区留言哦!!!

相关推荐
ChoSeitaku18 分钟前
链表循环及差集相关算法题|判断循环双链表是否对称|两循环单链表合并成循环链表|使双向循环链表有序|单循环链表改双向循环链表|两链表的差集(C)
c语言·算法·链表
娅娅梨20 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
DdddJMs__13524 分钟前
C语言 | Leetcode C语言题解之第557题反转字符串中的单词III
c语言·leetcode·题解
兵哥工控25 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
汤米粥26 分钟前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾29 分钟前
EasyExcel使用
java·开发语言·excel
我爱工作&工作love我32 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
拾荒的小海螺35 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
娃娃丢没有坏心思1 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++