⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++初阶
⭐代码仓库:C++初阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
模板
前言
模板进阶体现了很多的不同的方法和思想,这里提供了不同的分类和特化以及分离编译,我们需要掌握百分之八九十并在后面慢慢打磨不断历练。
一、模板参数分类
1、介绍
模板参数分类类型形参与非类型形参。
类型形参 :出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
cpp
// 非模板常量的优势:传多少就是多少很方便
// 非模板常量的局限:1、必须是整型 2、是一个常量
template<class T, size_t N> // N是一个常量 不能被修改
class Stack
{
public:
private:
T _a[N];
int _top;
};
int main()
{
Stack<int, 10> st1; // 10个空间
Stack<int, 100> st2; // 100个空间
return 0;
}
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
2、定长数组的非类型模板参数
我们来简单了解一个定长数组,就是用这个非类型模板参数来解决的。
这是简单的数组:
这是定长数组:
虽然看起来没什么问题,但是最奇怪的一个点为为什么定长数组不初始化所有的呢?这个就是委员会没有进行更新的原因了。那么定长数组唯一的优势是检查越界很有用,我们看下面的代码:
2、利用类型形参进行不同容器适配打印
cpp
#include<iostream>
#include<vector>
#include<list>
using namespace std;
template<class Container>
void Print(const Container& v)
{
// 编译不确定Container::const_iterator是类型还是对象
// typename就是明确告诉编译器这是类型,等模板实例化再去找
typename Container::const_iterator it = v.begin();
// auto是类型所以可以用auto
//auto 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);
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
Print(lt);
return 0;
}
设计一个容器,我们在main函数中设计简单的容器并将其传到Print函数中,Print函数中利用模板自行去寻找相对应的容器进行输出打印。
3、介绍typename
以前我们讲typename和class一样,但是这里我们讲一个区别的点,当我们分不清一个代码到底是类型还是对象还是变量的时候,我们加一个typename告诉编译器这个是类型,不是对象,因为编译器没那么聪明,加入进来一个静态的变量呢?编译器根本分不清,如果编译器茫然地去寻找,那肯定是不符合规定的。而编译器知道了这是个类型,会在类模板实例化的时候自动去找这个容器类型的实现规则并进行操作。
二、模板的特化
1、概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板:
我们发现,我们传参数过去比较的是地址大小,没有实现我们的目的,所以我们有了一个模板的特化帮助我们进行实现传参的比较。
2、函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
但一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
3、类模板特化
(1)全特化
全特化即是将模板参数列表中所有的参数都确定化。
(2)偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
i、部分特化
将模板参数类表中的一部分参数特化。
ii、参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
4、总结
cpp
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data(T1, T2)" << endl; }
};
// 全特化
template<>
class Data<int, double>
{
public:
Data() { cout << "Data(int, double)" << endl; }
};
// 偏特化 -- 特化部分参数
// 可能对某些类型的进一步限制
template<class T1>
class Data<T1, double>
{
public:
Data() { cout << "Data(T1, double)" << endl; }
};
// 偏特化 -- 特化成指针
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data() { cout << "Data(T1*, T2*)" << endl; }
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
int main()
{
Data<int, int> d1;
Data<int, double> d2;
Data<int*, double> d3;
Data<double, double> d4;
return 0;
}
三、模板分离编译
stack和普通函数的例子
举个简单的例子:
mobel.h:
cpp
#include<iostream>
#include<vector>
#include<deque>
namespace JRH
{
// 容器适配器
template<class T, class Container = std::deque<T>>
class stack
{
public:
// 插入 尾插
void push(const T& x);
// 删除 尾删
void pop();
// 取栈顶元素
T& top()
{
return _con.back();
}
// 输出个数
size_t size()
{
return _con.size();
}
// 判空
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
class A
{
public:
void func1();
void func2();
};
}
mobel.cpp:
cpp
#include"mobel.h"
namespace JRH
{
template<class T, class Container>
// 插入 尾插
void stack<T, Container>::push(const T& x)
{
_con.push_back(x);
}
template<class T, class Container>
void stack<T, Container>::pop()
{
_con.pop_back();
}
void A::func1()
{}
}
test.cpp:
cpp
#include<iostream>
#include<vector>
#include<list>
#include<array>
using namespace std;
#include"mobel.h"
int main()
{
JRH::stack<int> st;
st.push(1); // 崩溃
st.push(2); // 崩溃
st.pop(); // 崩溃
st.size(); // 通过
JRH::A a;
a.func1(); // 通过
a.func2(); // 崩溃
return 0;
}
我们分析一下main函数中有些函数是通过的有些函数是崩溃的原因:
(1)func1通过和fun2崩溃的原因
func1是在汇编的时候将定义的func1函数转化成地址指令,然后在链接的时候直接找到func1定义的地址并进行链接,链接成功,能够通过运行。
func2没有定义,自然在链接的时候找不到定义的地址,链接不成功直接崩溃。
(2)size()通过的原因
size()本来就能够通过,因为它的定义和声明不是分离的,是合在一起的,在编译的时候直接能找到了。
(3)push()和pop()崩溃的原因
push()和pop()因为有个模板参数,也就是在汇编的时候转化成为xxpushi(?)与xxpushxx(?)不同而我们的定义是一个模板!没有进行实例化!它没有指定类型是int还是double还是char,它并不知晓是什么,同样也没有地址。所以链接失败。
三种解决方法:
1、显示实例化
一种巧妙的方法:使用模板显示实例化(不推荐)。
mobel.cpp:
2、同一文件
还有一种最好的方法,stl也是这样做的就是将声明定义分离在同一文件中:
3、.hpp方式
最后一种好的方法是:
将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。
四、模板总结
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强了代码的灵活性 例如适配器和仿函数。
【缺陷】
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。