14.模板进阶
前言:
本篇文章将进行模板的进阶学习,如果对模板不了解的同学可以先阅读之前的模板初阶
点击此处跳转:06.模板初阶
目录
前言:
一、非类型模板参数
二、模板特化
1. 函数模板特化
2. 类模板特化
偏特化与全特化
三、模板的分离编译
1. 分离编译
2. 解决办法
总结:
一、非类型模板参数
参数模板分为类型模板参数和非类型模板参数,之前我们在模板初阶使用的template<class T>都是类型参数模板,即代表一种数据类型。而今天我们要介绍的非类型模板参数,是以一个常量或函数作为参数模板
非类型模板参数可以用于构造动态数组,但是传入的量也需要是常量(注:C99以后支持了动态数组,但visual studio一直都没有支持)
template<class T, size_t N = 10>
class Stack {
private:T _a[N];size_t _capacity;size_t _size;
};int main() {Stack<int> st1();Stack<int, 20> st2();
}
但是浮点数、类对象和字符串是不能作为非类型模板参数的!
二、模板特化
通常情况下,使用模板参数可以实现一些无关类型的代码,但如果我们想要对一些特殊的类型做特殊处理该怎么办呢?这个时候就需要用到模板特化了,模板特化就是专门用来处理特殊情况的。
模板特化又分为函数模板特化和类模板特化
1. 函数模板特化
函数模板特化一般步骤:
- 首先需要有一个基础的模板
- 需要有一个template关键字,后面跟上一个空的尖括号<>
- 特化的函数名后跟上<>,尖括号内指定需要特化的变量类型
- 函数用到的形参必须和<>内指定的基础参数类型相同,如果不同的话编译器可能会报错
示例代码:
可以看到,示例代码对char类型进行了特化,所以传入char类型数据的时候走了特化的部分
2. 类模板特化
类模板的特化和函数模板也是相似的。
并且类模板支持全特化和偏特化(函数模板只支持全特化)
偏特化与全特化
- 全特化是将模板参数列表中的参数全部特化
- 偏特化又分为两种形式:部分特化和对参数进一步限制的特化
- 部分特化:即对参数列表种的一部分参数进行特化,另一部分保持模板
- 限制特化:限制参数类型,如对所有指针类型/引用类型都特化
示例代码:
// 定义Date类
template<class T1, class T2>
class Date {
public:Date() {cout << "Date<T1, T2>" << endl;}
private:T1 _x;T2 _y;
};// 全特化
template<>
class Date<int, int> {
public:Date() {cout << "Date<int, int>" << endl;};
private:int _x;int _y;
};// 偏特化之部分特化
template<class T>
class Date<char, T> {
public:Date() {cout << "Date<char, T>" << endl;};
private:char _x;T _y;
};// 偏特化之限制特化
template<class T1, class T2>
class Date<T1*, T2*> {
public:Date() {cout << "Date<T1*, T2*>" << endl;};
private:T1* _x;T2* _y;
};// 引用类型
template<class T1, class T2>
class Date<T1&, T2&> {
public:Date(const T1& x, const T2& y):_x(x), _y(y){cout << "Date<T1&, T2&>" << endl;};
private:const T1& _x;const T2& _y;
};int main() {Date<double, double> d1;// 默认模板类Date<int, int> d2;// 全特化<int, int>Date<char, int> d3;// 部分特化<char, T>Date<int*, int*> d4;// 限制特化<T1*, T2*>Date<int&, int&> d5(1,1);// 限制特化<T1&, T2&>
}
可以看到示例每种定义了特化的类型都成功特化了。
注意:我们在引用类型进行初始化时,参数需要加上const,因为传递参数时会产生临时变量,而临时变量具有常性,不用const修饰的化无法正常接收。
三、模板的分离编译
之前我们学习的时候提到了使用模板时,不能把声明定义分离到两个文件,接下来我们就来了解一下这个问题的本质。
1. 分离编译
首先要知道编译器编译的规则:
一个程序(项目)通常由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件。
举个例子:对于Stack.h、Stack.cpp、Test.cpp这三个文件来说,头文件Stack.h是不直接编译的,他会在有#include<Stack.h>的文件中展开(所有的.h文件都是如此),而.cpp文件会在头文件展开后,按从上到下的顺序编译,最后编译完成后再将不同的.cpp文件链接起来。也就是说在编译阶段,Stack.cpp和Test.cpp这两个文件是独立的,只有完成链接后才会产生交集。
还是用Stack作为例子解释,假设Stack类中有一个push成员函数,普通函数当声明和定义分离到两个文件时,由于Stack.cpp包含了Stack.h头文件,push的声明和定义都在一个文件,编译时会记录函数的地址,之后在Test.cpp中调用push函数时,只需要call到相应的函数地址即可调用。
而模板函数他本身不是函数,只有当调用时检测了输入,编译器才会检测类型再临时生成一个函数。这就导致Stack.cpp和Stack.h文件在编译时只有声明,而没有一个真正实现的函数,也就不能记录函数的地址。之后Test.cpp与Stack.cpp链接时会发现根本没有记录下push函数的地址,也就无法调用。而如果声明和定义都在.h文件中,那就会在Test.cpp中展开,这样就可以在本文件中找到push函数的声明和定义,以便编译器根据传入参数类型去临时实现函数。
2. 解决办法
以函数模板为例,我们可以通过显示实例化来解决这个问题:
显示实例化时使用关键字template后面不加<>,函数名后跟<>,尖括号内需要具体的数据类型
template
Stack::push<int>(){// ...
}
但是这样只能应对int类型的数据,输入其他类型时仍然会报错,这就失去了模板的功能,所以我们一般不会这样去使用
所以最好的办法还是在遇到模板时将声明定义放到同一个.h文件中
总结:
通过本篇文章我们学习了模板的进阶使用技巧,这样以后就可以将模板在更广泛的场景下使用了。
如果觉得本篇文章对你有帮助的话可以点赞收藏加关注支持一下!