C++ —— 模板进阶 - 实践

1. 非类型模板参数

模板参数分类类型形参和非类型形参。

类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(模板)中可将该参数当成常量来使用。

template
class mystack
{
	//...
private:
	T _a[N];
	int _top;
};
int main()
{
	//mystack s1;      //10
	mystack s1;      //10
	mystack s2;   //100
	return 0;
}

注意:

1.非类型模板参数只能是整型,其他的都不行:浮点数、类对象以及字符串是不允许作为非类型模板参数的。

2.非类型的模板参数必须在编译期就能确认结果。

说到这里就要提一下STL中的容器:array(数组),少数场景可能会用到

#include
int main()
{
	array a1;
	array a2;
	return 0;
}

那么跟原生的数组有什么区别???

int main()
{
	array a1;
	array a2;
	int arr1[10];
	int arr2[100];
	return 0;
}

除了迭代器的区别没有什么区别,但是容器array还有一点点作用,对越界的检查比较严格,而C语言对于越界的情况是抽查的行为,可能查到,也可能查不到具体要看平台和编译器,一般在数组的结束位置放一些检查位(本质上是在结束的位置放一些标志位,查看越没越界就看标志位修没修改,但是只能设置一部分的标志位),并且越界读是查看不了的:

int main()
{
	int arr1[10];
	//越界检查抽查,只能检查越界写
	//arr1[10] = 10;  //检查的到
	//arr1[11] = 10;  //检查的到
	arr1[12] = 10;    //检查不到
	return 0;
}
//越界读——检查不到
cout << arr1[12] << endl;

原生数组本身访问本质就是解引用

但是对于STL容器:array来说检查越界很敏感,上面两种情况都会检查到(运算符重载)

cout << a1[10] << endl;  //越界读

a1[12] = 10;  //越界写

而对于array容器来说检查输入的值是否小于n,小于n就可以,大于等于n就报错(断言在debug下才会报错,release下就不会报错了)

array除了上面的一点点小优势外,其实并不好用,因为array是静态的数组,意味着要占用当前栈帧的空间,实际上的栈的空间是不大的,容易栈溢出。在想定义一个静态数组并且又要有更好的越界就选择array。

2. 模板的特化

2.1 概念

模板的特化就是针对模板进行特殊化处理。通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现一个专门用来进行小于比较的函数模板。模板特化分为函数模板特化和类模板特化。

2.2 函数模板特化

函数模板的特化步骤:

1.必须要有一个基础的函数模板

2.关键字template后面接一对空的尖括号<>

3.函数名后跟一对尖括号,尖括号中指定需要特化的类型

4.函数形参表:必须要和模板参数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

//日期类的实现
class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
	}
	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}
	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}
template
bool Less(T left,T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl;
	Date d1(2025, 12, 23);
	Date d2(2025, 12, 13);
	//Date d1(2025, 12, 30);
	cout << Less(d1, d2) << endl;
	cout << Less(new Date(2025, 12, 23), new Date(2025, 12, 13)) << endl;
	return 0;
}

运行结果:(多次运行就会出现问题)

这里的比较不是按照date来比较,而是按照指针来比较,new出来的指针就是不确定的一会大一会小,就会出现各种各样的问题。这个时候就可以用过模板特化来进行处理上面的情况。

特化需要注意的是,是在原模版的基础上进行特殊化处理,不能单独存在:

//特化:
//针对某些特殊类型进行特殊化处理
template<>  //模板参数去掉了,后面也有不去掉模板参数的
bool Less(Date* left, Date* right)
{
	return *left < *right;
}

运行结果:

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。(建议都用下面的普通函数)

//一个完整的函数——更好更方便
bool Less(Date* left, Date* right)
{
	return *left < *right;
}

这种方式简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。

为什么函数特化可以用普通函数来替代,是因为函数大多数是不需要实例化的,自动匹配的。

函数的特化并没有多大的用,但是类模板的特化有用。

2.3 类模板特化

类模板是要显式实例化的,普通类是不存在传参数的概念,不存在自动匹配的概念,一定是要进行实例化的,所以特殊处理要特化。

2.3.1全特化

全特化:将全部的参数都进行特化(将模板参数列表中所有的参数都确定化)

template
class Data
{
public:
	Data()
	{
		cout << "Data" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};
//全特化
template<>
class Data
{
public:
	Data()
	{
		cout << "Data" << endl;
	}
};
int main()
{
	Datad1;
	Datad2;
	return 0;
}

运行结果:

2.3.2 偏特化:

偏特化有以下两种变现方式:

1.部分特化:对部分参数进行特化
//全特化
template<>
class Data
{
public:
	Data()
	{
		cout << "Data" << endl;
	}
};
template<>
class Data
{
public:
	Data()
	{
		cout << "Data" << endl;
	}
};
//偏特化
template 
class Data
{
public:
	Data()
	{
		cout << "Date" << endl;
	}
private:
	T1 _d1;
	int _d2;
};
int main()
{
	Datad1;
	Datad2;
	Datad3;
	Datad4;
	return 0;
}

运行结果:(在特化之间也会存在最匹配的问题)

2.参数更进一步的限制

偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

template 
class Data
{
public:
	Data()
	{
		cout << "Date" << endl;
	}
private:
	T1 _d1;
	int _d2;
};
int main()
{
	Datad5;
	return 0;
}

运行结果:

有全特化走全特化,无全特化走偏特化。

偏特化中的两个参数为引用类型

template 
class Data
{
public:
	Data()
	{
		cout << "Date" << endl;
	}
};
int main()
{
	Datad6;
	return 0;
}

运行结果:

偏特化中的一个参数为引用类型,一直为指针类型:

template 
class Data
{
public:
	Data()
	{
		cout << "Date" << endl;
	}
};

运行结果:

偏特化对于指针类型的比较:

int main()
{
	ysy::priority_queue q3;
	q3.push(new int(3));
	q3.push(new int(1));
	q3.push(new int(2));
	while (!q3.empty())
	{
		std::cout << ' ' << *q3.top();
		q3.pop();
	}
	std::cout << '\n';
	return 0;
}
	template
	class Less
	{
	public:
		bool operator()(T* const& x, T* const& y)
		{
			return *x < *y;
		}
	};

运行结果:

3.模板的分离编译

先看看普通函数支持声明和定义的分离:

模板(类模板和函数模板)不支持函数的声明和定义的分离:

//Func.h
template
void func2(const T& x);
//Func.cpp
template
void func2(const T& x)
{
	cout << x << endl;
}
//Test.cpp
#include"Func.h"
int main()
{
	func2(200);
	return 0;
}

运行结果:

为什么模板不行呢???

模板距离实际被编译还差实例化。

在Func.i 里面不知道T被实例化成什么,Test.i 里面知道要被实例化成什么,但是它们不会交互,为了编译速度。知道要被实例化成什么的地方只有声明,有定义的地方不知道要被实例化成什么,所以就没有被编译成指令,没有放进符号表中,解决方法:

1.显式实例化(可以做到声明和定义分离,不是最优的解决方案,不推荐使用)

//Func.cpp
#include"Func.h"
template
void func2(const T& x)
{
	cout << x << endl;
}
//显式实例化:可以做到声明和定义分离
template                                  //表示为一个模板
void func2(const int& x);

运行结果:

上述的显式实例化是不够用的,因为只实例化了int,如果是double呢?就不行了

2. 直接在.h文件中定义

//Func.h
template
void func2(const T& x)
{
    cout << x << endl;
}

函数只有声明没有定义才会在链接的时候去找地址,但是当前.h文件中有定义就不会在链接的时候去找。

4. 模板总结

【优点】

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

【缺陷型】

  1. 模板会导致代码膨胀问题,模板多了实例化的过程,也会导致编译时间变长
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
posted @ 2025-12-18 13:33  clnchanpin  阅读(41)  评论(0)    收藏  举报