📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
【掌握C++模板进阶】:高级编程的艺术
- ♊️ 非类型模板参数
- ♊️ 模板的特化
- 🕐 概念
- 🕐 函数模板
- 🤾🏻 问题的引入
- 🤾🏻 问题的解决--函数模板的特化
- 🕐 类模板的特化
- 🤾🏻 全特化
- 🤾🏻 偏特化
- ♊️ 模板的分离编译问题
- 🕐 什么是分离编译
- 🕐 模板的分离编译问题
- 🤾🏻 找不到函数地址的原因
- 🤾🏻 解决办法
♊️ 非类型模板参数
我们在模板初阶的时候介绍到:模板是用于传类型的,其实模板还可以传非类型的参数,比如一个值(整形)。可以将其和函数传参的形参和实参联系起来理解,下面我们用一段代码来演示一下,非类型模板参数的使用:
// 定义一个预处理指令,用于在编译时忽略与安全性相关的警告。
// 这通常用于Visual Studio编译器中,以避免在使用某些旧的C库函数(如strcpy, sprintf等)时出现的警告。
#define _CRT_SECURE_NO_WARNINGS 1
// 引入所需的头文件
#include<iostream> // 用于输入输出流操作,如cout
#include<vector> // 引入STL中的动态数组模板
#include<map> // 引入STL中的关联容器模板,用于存储键值对
#include<string> // 引入STL中的字符串类
#include<stack> // 引入STL中的栈
// 使用std命名空间,这样我们就可以直接使用其中的名称(如cout、vector等),而无需前缀std::
using namespace std;
// 定义一个模板类A,该类接受一个非类型模板参数n(一个size_t类型的常量表达式)
template<size_t n>
class A
{
public:
// 类的构造函数,用于初始化对象
A()
{
// 使用memset函数将数组a的所有元素初始化为0
// sizeof(int) * n计算数组a的总字节数
memset(a, 0, sizeof(int) * n);
}
// 一个成员函数,用于打印数组a的所有元素
void PrintArr()
{
// 使用for循环遍历数组a
for (int i = 0; i < n; ++i)
// 输出数组a的当前元素,并在每个元素后添加一个空格
cout << a[i] << " ";
// 输出一个换行符,使输出更整洁
cout << endl;
}
private:
// 类的私有成员,一个大小为n的整数数组
int a[n];
};
// 主函数,程序的入口点
int main()
{
// 创建一个A类的对象x,其中n被指定为12(即数组a的大小为12)
A<12> x;
// 调用x的PrintArr函数来打印数组a的所有元素(此时所有元素都是0)
x.PrintArr();
// 返回0,表示程序正常退出
return 0;
}
运行结果:
也可以给缺省值,就可以什么都不传,一样可以运行成功。
运行结果:
- 注意,这里的非类型模板参数的类型只能为整形类型,像浮点型、自定义类型是不允许作为非类型的模板参数的。
非类型模板参数的值应该在编译时就被确定,而不是运行时,否则会报错,也就是说它应该是一个常量。
♊️ 模板的特化
🕐 概念
模板的特化我们可以理解为一种模板的特殊处理,因为可能普通的模板函数或者模板无法满足我们传的这个类型的要完成的功能,需要自己特化一个出来。
🕐 函数模板
🤾🏻 问题的引入
看下面的代码,让你更加直观的感受模板特化所要解决的问题:
// 定义一个预处理指令,用于在编译时忽略与安全性相关的警告。
// 这通常用于Visual Studio编译器中,以避免在使用某些旧的C库函数(如strcpy, sprintf等)时出现的警告。
#define _CRT_SECURE_NO_WARNINGS 1
// 引入所需的头文件
#include<iostream> // 用于输入输出流操作,如cout
#include<vector> // 引入STL中的动态数组模板
#include<map> // 引入STL中的关联容器模板,用于存储键值对
#include<string> // 引入STL中的字符串类
#include<stack> // 引入STL中的栈
// 使用std命名空间,这样我们就可以直接使用其中的名称(如cout、vector等),而无需前缀std::
using namespace std;
class Date
{
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);
}
friend ostream& operator<<(ostream& _cout, const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
template<class T>
bool Greater(const T& left, const T& right)
{
return left > right;
}
int main()
{
int a, b;//创建两个整形变量
a = 1;
b = 2;
double c, d;//创建两个浮点数变量
c = 1.1;
d = 2.3;
char e, f;//创建两个char型变量
e = 'c';
f = 'a';
//创建两个Date* 变量
Date* d1 = new Date(1983, 1, 1);
Date* d2 = new Date(1980, 1, 1);
//分别调用比较函数比较大小
cout << Greater(a, b) << endl;
cout << Greater(c, d) << endl;
cout << Greater(e, f) << endl;
cout << Greater(d1,d2) << endl;
return 0;
}
运行结果:
可以看到比较非自定义类型的时候,这个模板函数都没有问题,问题就出现了比较自定义类型的指针类型时,我们的本意是希望它去调用自定类型的operator >
,但是它是直接比较的指针地址的大小。这个时候就需要我们使用模板特化来解决问题。
🤾🏻 问题的解决–函数模板的特化
改进后的代码(只给出了关键部分):
template<class T>
bool Greater(T left,T right)
{
return left > right;
}
template<>
bool Greater<Date*> (Date* left,Date* right)
{
return *left > *right;
}
这里特化的语法要注意:
- 必须先要有一个函数模板。
- 特化的函数前面要加
template<>
,在函数名后面加<>
里面指定需要特化的类型。 - 模板函数的参数与其对应的特化函数的参数类型要一致(
&
)。
如果遇见函数模板特化不能满足要求的情况,可以将那个函数直接给出(将函数直接给出的可读性高,所以一般建议直接给出,而不走函数模板的特化)。
特化函数的调用规则:如果实参的类型和模板特化函数的形参类型一致就会去调用,否则会去走普通模板函数,不会特化(包括修饰符const
、&
也要一致)。
运行结果:
🕐 类模板的特化
🤾🏻 全特化
全特化是将类的模板参数全部给出。
// 定义一个预处理指令,用于在编译时忽略与安全性相关的警告。
// 这通常用于Visual Studio编译器中,以避免在使用某些旧的C库函数(如strcpy, sprintf等)时出现的警告。
#define _CRT_SECURE_NO_WARNINGS 1
// 引入所需的头文件
#include<iostream> // 用于输入输出流操作,如cout
#include<vector> // 引入STL中的动态数组模板
#include<map> // 引入STL中的关联容器模板,用于存储键值对
#include<string> // 引入STL中的字符串类
#include<stack> // 引入STL中的栈
// 使用std命名空间,这样我们就可以直接使用其中的名称(如cout、vector等),而无需前缀std::
using namespace std;
class A
{
public:
A()
{
cout << "A<T1,T2>" << endl;
}
private:
T1 t1;
T2 t2;
};
template<>
class A<int,char>
{
public:
A()
{
cout << "A<int,char>" << endl;
}
private:
int t1;
char t2;
};
int main()
{
A<int, int> a;
A<int, char> b;
return 0;
}
运行结果:
🤾🏻 偏特化
类的偏特化有两种,一种是类的模板参数部分特化。另一种是将参数进一步限制,比入加上修饰符*
、const
、&
。
代码演示:
// 定义一个预处理指令,用于在编译时忽略与安全性相关的警告。
// 这通常用于Visual Studio编译器中,以避免在使用某些旧的C库函数(如strcpy, sprintf等)时出现的警告。
#define _CRT_SECURE_NO_WARNINGS 1
// 引入所需的头文件
#include<iostream> // 用于输入输出流操作,如cout
#include<vector> // 引入STL中的动态数组模板
#include<map> // 引入STL中的关联容器模板,用于存储键值对
#include<string> // 引入STL中的字符串类
#include<stack> // 引入STL中的栈
// 使用std命名空间,这样我们就可以直接使用其中的名称(如cout、vector等),而无需前缀std::
using namespace std;
template<class T1, class T2>
class A
{
public:
A()
{
cout << "A<T1,T2>" << endl;
}
private:
T1 t1;
T2 t2;
};
template<class T1>//将第二个参数特化为char类型的
class A<T1,char>
{
public:
A()
{
cout << "A<T1,char>" << endl;
}
private:
T1 t1;
char t2;
};
template<class T1,class T2>
class A<const T1,const T2>
{
public:
A()
{
cout << "A<const T1,const T2>" << endl;
}
private:
T1 t1;
T2 t2;
};
template<class T1, class T2>
class A<T1*,T2*>
{
public:
A()
{
cout << "A<T1*,T2*>" << endl;
}
private:
T1 t1;
T2 t2;
};
template<class T1, class T2>
class A<T1&, T2&>
{
public:
A()
{
cout << "A<T1&,T2&>" << endl;
}
private:
T1 t1;
T2 t2;
};
int main()
{
A<int, int> a;//走普通的模板
A<int, char> b;//走指定第二个参数为char的模板
A<double, char> c;//走指定第二个参数为char的模板
A<int*, char*> d;//走加了指针限制的模板
A<int&, int&> e;//走加了引用限制的模板
A<const int, const int> f;//走加了const限制的模板
return 0;
}
运行结果:
全特化指定了具体的类型,但是偏特化的第二种可以加修饰符的方式更加常用,这样如果是指针类型使用偏特化一份代码就全部解决了,但是全特化得写很份代码。
- 当一个类模板既有全特化、偏特化、正常模板时,调用顺序是全特化 、偏特化、正常模板(前提得满足要求能够调用)。
♊️ 模板的分离编译问题
🕐 什么是分离编译
一个程序由很多源文件组成,要将所有的源文件单独编译生成目标文件,最后再将这些目标文件链接起来生成单一可执行文件的过程叫做分离编译。
🕐 模板的分离编译问题
假设我们现在要写一个加法的函数,但是这个加法函数是函数模板,我们将其声明和定义分别放在头文件(.hpp
/.hp
)和源文件(.cpp
)。
.h
中放的:
template<class T1,class T2>
int Add(T1 a, T2 b);
.cpp
中放的:
#include"Add.h"
template<class T1, class T2>
int Add(T1 a, T2 b)
{
return a + b;
}
main.cpp
:
#include"Add.h"
int main()
{
cout << Add(1, 3) << endl;
return 0;
}
报错结果:
很奇怪,报的是链接错误,找不到函数地址。我们正常函数声明和定义放在两个文件里面就是这样来做的呀,为什么到函数模板这里就不行了呢,我们来画图分析原因:
🤾🏻 找不到函数地址的原因
🤾🏻 解决办法
- 将模板加法函数的声明和实现放在一个
.h/.hpp
文件中。(常用做法)
程序成功运行:
- 显示实例化
Add.cpp
中加上两行代码即可。
#include"Add.h"
template<class T1, class T2>
int Add(T1 a,T2 b)
{
cout << "Add(T1 a,T2 b)" << endl;
return a + b;
}
template
int Add<int,int>(int ,int);
template
int Add<int,int>(int ,int);
会告诉编译器让它生成这个实例化的函数。但是其它类型的Add函数不会生成。
运行结果:
所以这种方式并不常用,太单一,一般使用第一种较多。