【C++】模板进阶知识点

模板进阶

    • 前言
    • 正式开始
      • 非类型模板参数
      • 模板的特化
        • 对于函数模板
        • 对于类模板特化
          • 类模板特化分类
      • 模板的分离编译
      • 模板总结

【C++】模板进阶知识点_第1张图片

前言

本篇是关于模板进阶得一些知识点,如果你没有看过我写的模板初阶,可以先看看我模板初阶的博客:模板的一点简单介绍

本篇主要内容有

  1. 非类型模板参数
  2. 模板的特化

正式开始

上面挨着一个个的来。

非类型模板参数

我们之前用的模板参数,像T、iterator这种的都是某一特定类型。
下面要说的是非类型的模板参数。

下面的这些代码都是在非std命名空间中的,因为下面代码中的array会和std中的array冲突。我这里用的命名空间叫做FangZhang。
【C++】模板进阶知识点_第2张图片

比如说,现在我们要实现一个静态的数组。
以我们学C的经验,就是#define。

像下面这样:
【C++】模板进阶知识点_第3张图片
如果我们用这个模板类去实例化对象的时候,生成对象的存储数据个数都是相同的。
比如下面这样:
【C++】模板进阶知识点_第4张图片

此时a1和a2,虽然存储元素的类型是不同的,但是存储的元素个数都是10个。

但是如果我们想要实现存储不同元素个数的对象时该怎么办?
此时就要用到非类型模板参数。像下面这样:
【C++】模板进阶知识点_第5张图片

N代表的就是元素个数,而不是一个特定的类型。
此时我们就可以这样定义对象:
【C++】模板进阶知识点_第6张图片

这个非类型模板参数也是可以有缺省值的。
【C++】模板进阶知识点_第7张图片

此时我们就可以定义如下对象:
【C++】模板进阶知识点_第8张图片
二者都为存储10个元素的数组。

其实就和我们函数的参数很相似。
但是要注意:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的,只能是整形,而且传参时必须传常量。
  2. 非类型的模板参数必须在编译期就能确认结果。

给个传变量的例子:
【C++】模板进阶知识点_第9张图片

上面提到了array这个容器,简单讲两句。
固定大小,就一静态数组,支持迭代器、[ ]、不支持插入(因为数组大小是固定的,不能扩容),没有构造函数,不能初始化。

定义对象时像下面这样:
【C++】模板进阶知识点_第10张图片

不能初始化:
【C++】模板进阶知识点_第11张图片

第二个参数也是只能传常量。
【C++】模板进阶知识点_第12张图片

其实这个容器比较鸡肋,因为我们学C时就已经有了数组这个东西,二者功能基本一样的,而且数组定义的时候还更简便一点。

但是还是有点用处的。
【C++】模板进阶知识点_第13张图片
二者的主要区别在越界检查上。

对于a1而言。
a1的[ ]为函数调用,就是重载的[ ]。其会在[ ]内部检查所访问的下标是否超过了其内部的size,那么如果越界了是一定能检查到的。但是普通的数组就不是了。

对于a2而言。
a2为指针,其检查是抽检。而且只针对越界写,越界读不做检查。
什么意思嘞?看例子:

在这里插入图片描述
这就是越界读,就是访问了内部的数据,但是并没有修改。是可以访问到的,编译器不会报错。
但是我们是知道已经越界了的。

还有就是抽检,意思就是其只会在有效元素的后面那几个位置是否被修改。如果我们往后多走几步,就检查不到了。
【C++】模板进阶知识点_第14张图片
这个就非常bug了,已经远远越界了。
只有在后面仅限的几个位置写才会检查到。比如说这里修改a2[10]就能检查到。

但是array用的也不多,这里就不细讲了。

然后就是模板的特化。

模板的特化

也叫模板的特殊化。就是当模板参数传参时,有一种类型所执行的功能不是我们本来模板函数或模板类中所想要的功能时,就要对这个类型进行特殊化处理。
说起来比较绕,上例子:

对于函数模板

比如说我现在用一下以前写的日期类Date。

代码如下(这些代码不用细看,我这里就用一下其中的<重载):

class Date
{
	friend ostream& operator<<(ostream& out, const Date& date);
public:
	Date(int year = 2023, int month = 5, int day = 7)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& date)
	{
		cout << "Date(const Date& date)" << endl;
		_year = date._year;
		_month = date._month;
		_day = date._day;
	}
	
	void show() const;
	void show();

	Date& operator=(const Date& date);

	Date operator++(int);

	Date& operator++();

	int GetMonthDay(int year, int month);

	Date& operator+=(int day);

	Date operator+(int day);

	Date& operator-=(int day);

	Date operator-(int day);

	bool operator < (const Date& date);

	bool operator <= (const Date& date);

	bool operator > (const Date& date);

	bool operator >= (const Date& date);

	bool operator!=(const Date& date);

	bool operator==(const Date& date);

	int operator-(const Date& date);

	//ostream& operator<<(ostream& out) const;

private:
	int _year;
	int _month;
	int _day;

void Date::show() const
{
	cout << "void Date::show() const" << endl;
	cout << _year << "/" << _month << "/" << _day << endl;
}

void Date::show()
{
	cout << "void Date::show()" << endl;
	cout << _year << "/" << _month << "/" << _day << endl;
}

bool Date::operator==(const Date& date)
{
	return _year == date._year
		&& _month == date._month
		&& _day == date._day;
}

int Date::GetMonthDay(int year, int month)
{
	int arr[13] = { 0, 31, 28, 31, 30,31,30,31,31,30,31,30,31 };

	if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
	{
		return 29;
	}
	
	return arr[month];
}

Date& Date::operator=(const Date& date)
{
	cout << "Date & Date::operator=(const Date & date)" << endl;
	if (!(*this == date))
	{
		_year = date._year;
		_month = date._month;
		_day = date._day;
	}

	return *this;
}

Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		*this -= -day;
	}
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}

	return *this;
}


Date Date::operator++(int)
{
	Date tmp = *this;

	*this += 1;//+=也是重载的函数,等会再讲
	return tmp;
}

Date& Date::operator++()
{
	*this += 1;//+=也是重载的函数,等会再讲

	return *this;
}

Date Date::operator+(int day)
{
	Date tmp = *this;
	tmp += day;

	return tmp;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		*this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

Date Date::operator-(int day)
{
	Date tmp = *this;
	tmp -= day;

	return tmp;
}

bool Date::operator < (const Date& date)
{
	if (_year < date._year)
	{
		return true;
	}
	else if (_year == date._year && _month < date._month)
	{
		return true;
	}
	else if (_year == date._year && _month == date._month && _day < date. _day)
	{
		return true;
	}

	return false;
}

bool Date::operator <= (const Date& date)
{
	if (*this < date || *this == date)
		return true;
	
	return false;
}


bool Date::operator > (const Date& date)
{
	if (!(*this <= date))
		return true;

	return false;
}

bool Date::operator >= (const Date& date)
{
	if (!(*this < date))
		return true;

	return false;
}

bool Date::operator!=(const Date& date)
{
	if (!(*this == date))
		return true;

	return false;
}

int Date::operator-(const Date& date)
{
	int flag = 1;
	int n = 0;
	Date max = *this;
	Date min = date;
	if (max < min)
	{
		max = date;
		min = *this;
		flag = -1;
	}
	
	while (min != max)
	{
		++min;
		++n;
	}

	return n * flag;
}

};


inline ostream& operator<<(ostream& out,const Date& date)
{
	out << date._year << "_" << date._month << "_" << date._day;
	return out;
}

这里写一个less模板函数,用来比较两个数据的大小。
【C++】模板进阶知识点_第15张图片

然后我们定义几个对象来测试一下:
分别为int、Date、Date*
【C++】模板进阶知识点_第16张图片
我这里想用Date*来比较两个Date的大小,但失败了。
因为这里实例化对象的时候实例化出来的是Date*的对象,p1和p2比较的时候是纯指针的比较,并没有调用Date对象的 < 重载。所以就出问题了。

但是如果我们想用Date*来比较两个Date对象呢?
我们可没有 if(T == Date*) 这种玩法,所以此时就要对Date*进行特殊化处理。
像下面这样:
【C++】模板进阶知识点_第17张图片
写法比较特殊,这个要留个印象。

实现了这个测试就能过去了。
【C++】模板进阶知识点_第18张图片

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

上面的是函数模板,下面来类模板的。

对于类模板特化

就用上一篇博客的仿函数,如果有看不懂的小伙伴可以点击传送门:只看仿函数部分即可

定义如下类lessStruct:
【C++】模板进阶知识点_第19张图片

当我们进行同样的比较的时候也会出现上面函数模板中的问题。
【C++】模板进阶知识点_第20张图片
想要解决的话也是特化就行。

但是类模板的特化和函数模板的特化有些不一样。
【C++】模板进阶知识点_第21张图片
此时再测试:
【C++】模板进阶知识点_第22张图片
就好了。

再说一个能够用到特化的地方:
就用我前面讲过的优先级队列,如果不知道优先级队列的,可以点这篇:优先级队列

例子:
我们在优先级队列中存方Date类型,就能得到最大或最小日期。

如下:
【C++】模板进阶知识点_第23张图片
如果传Date:
【C++】模板进阶知识点_第24张图片
就是正常情况下的大堆。

但是如果我传参是Date*,底层要用到库中的仿函数less,但是库中的仿函数less是没有我们写的那个Date*的特化的。所以此时就有问题了。

如果直接用库中给的less,打印出来的结果就是乱的:
【C++】模板进阶知识点_第25张图片

而且每次都会变。
【C++】模板进阶知识点_第26张图片

但是如果我们将刚刚的仿函数传过去:
【C++】模板进阶知识点_第27张图片
此时就是正常情况下的大堆。

类模板特化分类

类模板特化可分为 全特化和偏特化。

先给出没有特化的例子:
一个普通的类模板Data
【C++】模板进阶知识点_第28张图片
测试:
【C++】模板进阶知识点_第29张图片

然后用这个来特化。

全特化

根据名字就能猜出大概是将全部的模板参数进行特化。
【C++】模板进阶知识点_第30张图片

测试:
【C++】模板进阶知识点_第31张图片

偏特化(也叫半特化,但这种说法描述不准确)

就是一部分的模板参数进行特化。
【C++】模板进阶知识点_第32张图片

测试:
【C++】模板进阶知识点_第33张图片

这里只要第二个参数为int就走的是这个模板的特化。
根特化不沾边的类型,就走的是原模版。

还可以下面这样特化,指定其为指针或者引用。

【C++】模板进阶知识点_第34张图片

测试:
【C++】模板进阶知识点_第35张图片

也可以指针和引用混着来:
【C++】模板进阶知识点_第36张图片

测试:

【C++】模板进阶知识点_第37张图片

那么这里体现的就是有更匹配的就走更匹配的,没有的话就将就一下。

我们刚刚lessStruct的例子中就可将Date*改为T*。
【C++】模板进阶知识点_第38张图片

测试也是能过的。

【C++】模板进阶知识点_第39张图片

下面说模板的分离编译。

模板的分离编译

在我模板初阶的那篇中,已说过了,模板不要分离编译(声明和定义放在不同文件中),出错了很麻烦,这里就细模板分离编译。

我这里用一下之前模拟实现的vector。

声明部分:

// 声明
template<class T>
class vector
{
public:
	typedef T* iterator;

	vector()
		:_start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{}

	~vector()
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}

	size_t capacity() const
	{
		return _end_of_storage - _start;
	}

	const T& operator[](size_t pos) const
	{
		assert(pos < size());

		return _start[pos];
	}

	T& operator[](size_t pos)
	{
		assert(pos < size());

		return _start[pos];
	}

	size_t size() const
	{
		return _finish - _start;
	}

	void reserve(size_t n)
	{
		if (n > capacity())
		{
			size_t sz = size();
			T* tmp = new T[n];
			if (_start)
			{
				//memcpy(tmp, _start, sizeof(T)*sz);
				for (size_t i = 0; i < sz; ++i)
				{
					tmp[i] = _start[i]; // T对象是自定义类型时,调用T对象operator=
				}
				delete[] _start;
			}

			_start = tmp;
			_finish = _start + sz;
			_end_of_storage = _start + n;
		}
	}

	void push_back(const T& x);
	iterator insert(iterator pos, const T& x);
private:
	iterator _start;
	iterator _finish;
	iterator _end_of_storage;
};

定义部分(这里只将push_back和insert分离)

template<class T>
void vector<T>::push_back(const T& x)
{
	insert(_finish, x);
}

template<class T>
typename vector<T>::iterator vector<T>::insert(typename vector<T>::iterator pos, const T& x)
{
	assert(pos >= _start);
	assert(pos <= _finish);

	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}

	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
	*pos = x;

	++_finish;

	return pos;
}

上面的代码都是放在FangZhang命名空间中的,就是为了和库中的vector分开。

先看下库中的:
【C++】模板进阶知识点_第40张图片
正常运行。

再看下我们分离的:
【C++】模板进阶知识点_第41张图片
出现了链接问题,细看的话就是push_back那有问题。

下面说为什么(下面的知识要求各位对编译和链接有一定的理解,如果有同学掌握得不是很熟练的话,可以看看我这篇博客:【C】程序环境和预处理 ):

我这里有三个文件
一个vector.h
一个vector.cpp
一个test.cpp

cpp文件都引了h,当我们编译了之后,两个.cpp生成对应的.o文件。
就是 vector.o 和 test.o

在test.cpp中,引了vector.h,有test_template函数进行实例化,也就是里面FangZhang::vector v;这条语句,将类模板进行了实例化。能够生成一份对应int的代码。此时.h中的构造、size、析构等函数也就实例化了。所以这些函数的地址在编译阶段就能够确定,那么test.o中是包含有这些成员函数的地址信息的,但是.h中只有push_back和insert的声明,故这两个函数的地址没法确定,只能确定函数名,所以test.o中缺少了这两个函数的函数地址信息。

在vector.cpp中,引了vector.h,而且.cpp文件中还有两个函数模板,但是该文件中没有任何的语句能够将其中的模板参数T实例化,那么push_back和insert 这两个函数也就没法实例化,也就是说编译器无法确定这两个函数的地址,vector.h中虽也有这两个函数的声明,但是都没什么用,函数地址无法确定,最终形成的符号表中只有函数名,没有对应的地址,但是编译器是能够编译通过的,因为编译器眼里还有最后链接的时也能确定函数的地址,所以最终形成的vector.o中也没有这两个函数的地址。

最终链接阶段,两个.o文件中都没有这两个函数的地址,所以这两个函数的地址都没法确定,而且我们还使用到了push_back,但是push_back并没有实例化,所以就出现了链接错误。

有两个解决方案。

  1. 模板的声明和定义不要分开放在 .h 和 .cpp 中,要分离也要分离在同一个文件中,意思就是一个文件中同时存放声明和定义(这样方便我们看代码框架)。

这里直接把分离的代码给出来:

namespace FangZhang
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;

		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{}

		~vector()
		{
			delete[] _start;
			_start = _finish = _end_of_storage = nullptr;
		}

		size_t capacity() const
		{
			return _end_of_storage - _start;
		}

		const T& operator[](size_t pos) const
		{
			assert(pos < size());

			return _start[pos];
		}

		T& operator[](size_t pos)
		{
			assert(pos < size());

			return _start[pos];
		}

		size_t size() const
		{
			return _finish - _start;
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					//memcpy(tmp, _start, sizeof(T)*sz);
					for (size_t i = 0; i < sz; ++i)
					{
						tmp[i] = _start[i]; // T对象是自定义类型时,调用T对象operator=
					}
					delete[] _start;
				}

				_start = tmp;
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

		void push_back(const T& x);
		iterator insert(iterator pos, const T& x);
		//两个函数声明
		
	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
==================================
// 此处往下是定义
	template<class T>
	void vector<T>::push_back(const T& x)
	{
		insert(_finish, x);
	}

	template<class T>
	typename vector<T>::iterator vector<T>::insert(typename vector<T>::iterator pos, const T& x)
	{
		assert(pos >= _start);
		assert(pos <= _finish);

		if (_finish == _end_of_storage)
		{
			size_t len = pos - _start;
			reserve(capacity() == 0 ? 4 : capacity() * 2);
			pos = _start + len;
		}

		iterator end = _finish - 1;
		while (end >= pos)
		{
			*(end + 1) = *end;
			--end;
		}
		*pos = x;

		++_finish;

		return pos;
	}
}

此时运行是可以通过的:
【C++】模板进阶知识点_第42张图片

  1. 显示实例化(很挫,不推荐)

这个方法是在分开定义的.cpp中显示实例化出你想用的类型。
【C++】模板进阶知识点_第43张图片
比如这里我在最下面显示实例化出一个int类型。

运行就能通过了:
【C++】模板进阶知识点_第44张图片

但是这种方法不好就在于,当我们再定义一个类型时,就要再在分离定义的.cpp中加上新添加的类型。

比如我再多搞个double的:

【C++】模板进阶知识点_第45张图片

就得再写一个double的:
【C++】模板进阶知识点_第46张图片

运行:

【C++】模板进阶知识点_第47张图片

如果没有,就会出现和开始一样的链接错误:
【C++】模板进阶知识点_第48张图片

换个类型就要再实例化一个。
所以说这种方法比较挫,不是很推荐使用。

模板总结

【优点】

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

【缺陷】

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

到此结束。。。

你可能感兴趣的:(c++,模板,数据结构,算法,编译链接)