👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
前言
- 初阶模板地址:点击跳转
目录
一、typename和class的区别
定义模板参数关键字可以用typename
,也可以用class
,是没有区别的,可是真的没有区别吗?
- 假设要打印容器的数据,要封装一个打印函数(以迭代器的方式)
#include <iostream>
#include <vector>
using namespace std;
void Print(const vector<int>& v)
{
vector<int>::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
}
int main()
{
vector<int> v{ 10,20,30,40,50 };
Print(v);
return 0;
}
【输出结果】
以上代码虽然可以正常输出,但它只能打印vector<int>
类型容器的数据,若要打印vector<double>
,又或者是list
容器的数据,那么这样就写死了,有人想可以用函数模板。
代码如下:
#include <iostream>
#include <vector>
using namespace std;
// 把class换成typename也是可以的
template<class Container>
void Print(const Container& v)
{
Container::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
}
int main()
{
vector<int> v{ 10,20,30,40,50 };
Print(v);
return 0;
}
【输出结果】
使用函数模板后发现以上代码报错了,提示说需要在Container
前加上typename
- 那么为什么必须要加上
typename
呢?
这是因为代码在编译阶段,会进行语法检查,当编译到Container::const_iterator it
时,由于Container
还没实例化,那么此时编译器区分不了Container
是类名(类名::
访问静态变量)还是类型。因此,编译器要求加上typename
告诉Container
是类型。
二、非类型模板参数
2.1 概念
模板参数分为:类型形参与非类型形参
-
类型形参:通过类型模板参数,我们可以轻松地创建支持不同类型的通用模板,从而实现代码的复用性。
-
非类型形参:将常量作为类或者函数模板的一个参数,在类或者函数模板中可将该参数当成常量来使用。
2.2 实际用途
举个例子:定义一个静态栈
#define N 10
template <class T>
class stack
{
public:
private:
T _a[N];
int _top;
};
如果用define
那么栈的固定大小就已经被定死了。因此,C++引入新语法:非类型模板参数
// size_t N 可以给默认缺省参数,也可以不给
template <class T, size_t N = 10>
class stack
{
public:
private:
T _a[N];
int _top;
};
int main()
{
// 不同的对象可以有不同的大小
stack<int, 100> s1;
stack<int, 50> s2;
}
需要注意的是,非类型模板参数必须满足以下两点:
- 必须是常量,不可被修改
- 必须是整型。
因此可以得出,非类型模板参数一般是用来定义一个数组的大小的
2.3 array容器(C++11)
在C++11标准中,引入了一个容器array
,它的底层使用了非类型模板参数,是一个真正意义上的泛型数组(定长数组),这个是用来对标C语言传统数组的。
以下是array
容器的基本用法:
看完以上接口,array
支持的,C数组也都是支持的。那么它们有什么区别呢?
array
也并没有进行初始化。
array
对于越界读、写检查更为严格。
传统数组越界读写,不会发生报错;而array
数组则会报错。
虽然对越界行为检查严格 ,但在实际开发中,很少使用array
容器,因为它对标传统数组,连初始化都没有,而vector
也是类似于数组的容器,在功能和实用性上可以全面碾压,因此可以说array
是一个鸡肋的容器。
三、模板特化
3.1 概念
-
模板特化顾名思义就是对模板进行特殊化处理 。模板特化分为函数模板特化与类模板特化。
-
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。
比如:实现一个专门用来进行小于比较的函数模板
#include <iostream>
using namespace std;
template<class T>
bool Less(T x, T y)
{
return x < y;
}
int main()
{
int a = 1;
int b = 2;
cout << Less(a, b) << endl; // 可以比较,结果正确
int* p1 = &a;
int* p2 = &b;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
【输出结果】
上述示例中,p1
指向的a
显然小于p2
指向的b
,但是Less
内部并没有比较p1
和p2
指向的对象内容,而比较的是p1
和p2
指针的地址。
因此,就需要对模板进行特殊化处理。即在原模板函数的基础上,针对特殊类型所进行特殊化的实现方式。
3.2 函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板。
- 关键字
template
后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
- 函数形参必须要和基础的模板函数的基础参数类型完全相同。
【输出结果】
不过对于函数模板特化来说,存在一个更加方便的东西:函数重载同样也能解决特殊需求
3.3 类模板特化(重点)
模板特化主要用在类模板中。类模板特化还可以分为:全特化
和偏特化
3.3.1 全特化
全特化指将原模板参数列表中所有的参数都确定化
注意:在进行全特化必须满足以下条件
- 必须要先有一个基础的类模板
- 全特化模板中的模板参数可以不用写
- 需要在类名之后,指明具体的参数类型,否则无法实例化出对象
调用时会优先选择更为匹配的类模板
3.3.2 偏特化(半特化)
- 部分特化:只特化一部分模板参数
// 原模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 偏特化
// 将第二个成员变量类型特化为int
template <class T1>
class Data<T1, int>
{
public:
Data()
{
cout << "Data<T1, int>" << endl;
}
private:
T1 _d1;
int _d2;
};
- 偏特化的衍生:参数更进一步的限制
不仅仅指特化部分参数,而是针对模板参数的更进一步的条件限制所设计出来的一个特化版本
// 原模板
template <class T1, class T2>
class Date
{
Data() { cout << "Data<T1, T2>" << endl; }
}
template <class T1, class T2>
class Data <T1*, T2*> // 参数更进一步的限制
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
};
四、模板分离编译问题
4.1 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
4.2 模板的分离编译
假如有以下场景,类模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
【程序结果】
出现了链接错误!!!(什么时候会出现链接错误:函数有声明,没定义)
【分析】
代码从文本变为可执行程序所需要的步骤:
【头文件展开】
- 可是明明在
stack.cpp
中定义了,为什么连接不上呢?
原因是:类模板的成员函数实现必须在使用模板的地方可见才能实例化,编译器无法确定函数原型(实例化),也就无法生成函数地址,在进行链接时,无法找到函数的地址,自然而然就报错了。
因此建议 类模板声明和定义不分离。
五、小结
模板的优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(
STL
)因此而产生。 - 增强了代码的灵活性(适配器、仿函数)。
模板的缺陷:
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误(报错信息复杂)。