目录
1 泛型编程
- 例如,为了交换不同类型的数据,需要根据不同类型设计相应的交换函数,如下
void Swap(int& p1, int& p2){
int tmp = p1;
p1 = p2;
p2 = tmp;
}
void Swap(double& p1, double& p2){
double tmp = p1;
p1 = p2;
p2 = tmp;
}
- 通过函数重载可满足交换函数的易用性,但每当需要处理新的数据类型,又需要新增相应的交换函数,因此导致代码的复用性和可维护性较低,因此前人提出了泛型编程的概念。
- 泛型编程也就是编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础
2 模板
2.1 函数模板
2.1.1 函数模板的概念和格式
格式如下:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
函数模板代表了一整个同作用的函数家族,函数模板与参数类型无关,只有在函数调用时,根据实参的类型产生对应类型的函数。如上述的交换函数
template<typename T>
void Swap(T& left, T& right) {
T tmp = left;
left = right;
right = tmp;
}
int main() {
int i = 1, j = 2;
Swap(i, j);
char a = 'a', b = 'b';
swap(a, b);
cout << a << b << endl;
return 0;
}
template<typename T>
——typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
typename后面类型名字 T 是随便取的,一般是大写字母或者首字母大写单词,T 代表是一个模板类型(虚拟类型)
- 在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:
int i = 1, j = 2;Swap(i, j);
当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定int类型,然后产生一份专门处理int类型的代码,如下
void Swap(int& p1, int& p2){
int tmp = p1;
p1 = p2;
p2 = tmp;
}
模板参数可以定义多个,以满足不同需求,如下
template<class T1, typename T2>
void myless(T1 a1, T2 a2) {
if (a1 == (a1 < a2 ? a1 : a2)) {
cout << a1 << endl;
}
else {
cout << a2 << endl;
}
}
int main() {
myless(1.1, 2);
myless(2, 1.2);
return 0;
}
2.1.2. 函数模板的实例化
隐式实例化
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
// 隐式实例化
// 编译器自动推演
cout << Add(1, 2) << endl;
//Add(1.1, 2); // 推演实例化报错
return 0;
}
Add(1.1, 2);
推演错误的原因在于。因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参1.1将T推演为 double 类型,通过实参2将T推演为 int 类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为 int 或者 double 类型而报错。有两种处理方式,一种是强制类型转换,另一种是显示实例化
// 强制类型转换
cout << Add((int)1.1, 2) << endl;
cout << Add(1.1, (double)2) << endl;
显式实例化
在函数名后的 <> 中指定模板参数的实际类型,如上文Add模板的显式实例化如下
cout << Add<int>(1.1, 2) << endl;
cout << Add<double>(1.1, 2) << endl;
- 有一种情况必须使用显式实例化,如下
template<class T>
T* Func(int n) {
T* a = new T[n];
return a;
}
int main() {
// 必须显示实例化才能调用
Func<A>(10);
return 0;
}
- 该情况编译器无法通过实参的类型,确定T应该推演为何种类型,因为形参的类型已经确定为int类型,故必须显示实例化
2.1.3 函数模板的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
// 专门处理int的加法函数
int Add(int left, int right){
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right){
return left + right;
}
int main(){
Add(1, 2);
Add(1, 2.0);
return 0;
}
Add(1, 2);
——与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0);
——模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
2.2类模板
- 格式如下
template<class T1, class T2, ..., class Tn>
class 类模板名{
// 类内成员定义
};
- 先前创建的栈数据结构,是固定存放一个类型的栈,如int。现使用模板,创建一个能存放多种类型的栈,如下
template<typename T>
class Stack {
public:
Stack(size_t capacity = 4)
:_a(nullptr)
, _capacity(0)
, _top(0) {
if (capacity > 0) {
_a = new T[capacity];
_capacity = capacity;
_top = 0;
}
}
~Stack() {
delete[] _a;
_a = nullptr;
_top = _capacity = 0;
}
void Push(const T& x) {
if (_top == _capacity) {
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
// 1.开辟新空间
T* tmp = new T[newCapacity];
// 判断是否是第一次
if (_a) {
// 2.拷贝数据
memcpy(tmp, _a, sizeof(T) * _top);
// 3.释放旧空间
delete[] _a;
}
_a = tmp;
_capacity = newCapacity;
}
_a[_top++] = x;
}
void Pop() {
assert(_top > 0);
--_top;
}
bool Empty() {
return 0 == _top;
}
T& Top() { // 对象存放在堆,可使用传引用返回,可改变栈顶的数据
assert(_top > 0);
return _a[_top - 1];
}
private:
T* _a;
size_t _top;
size_t _capacity;
};
int main() {
Stack<int> st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
st1.Push(5);
st1.Top() *= 2;
while (!st1.Empty()) {
cout << st1.Top() << " ";
st1.Pop();
}
cout << endl;
return 0;
}
- 类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在 <> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类,
Stack是类名,Stack<int>是类型
int main() {
// 类模板都是显示实例化
Stack<int> st1;
Stack<char> st2;
Stack<int> st3(100);
return 0;
}
- 虽然st1、st2都是使用一个类模板,但其不是同一个类型。
3 非类型模板参数
- 模板参数分类类型形参与非类型形参,上文出现的模板参数均为类型参数
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
- 非类型模板参数必须使用整型,故浮点数、类对象以及字符串是不允许作为非类型模板参数的。非类型的模板参数必须在编译期就能确认结果
#define N 100
template<class T>
class array {
private:
T _a[N];
};
上述类中,给定一个确定的N,无法满足个性化需求,如N要10000,因此需要借助非类型模板参数,使用结果如下
template<class T, size_t N = 100>
class array {
private:
T _a[N];
};
int main() {
array<int> a0;
array<int, 100> a1;
array<double, 1000> a2;
return 0;
}
4 模板的特化
- 使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。因此在原模板类的基础上,模板的特化是针对特殊类型所进行特殊化的实现方式
4.1 函数模板特化
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号<>,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
4.2 类模板特化的全特化和偏特化
- 全特化即是将模板参数列表中所有的参数都确定化
template<class T1, class T2>
class Data{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 全特化
template<>
class Data<int, char>{
public:
Data() { cout << "Data<int, char>" << endl; }
private:
//int _d1;
//char _d2;
};
- 偏特化:任何针对模版参数进一步进行条件限制设计的特化版本
- 部分特化,如下
// 偏特化
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 Data<T1*,T2*>{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
};
template<class T1, class T2>
class Data<T1&, T2&>{
public:
Data() { cout << "Data<T1&, T2&>" << endl; }
};
template<class T1, class T2>
class Data<T1&, T2*>{
public:
Data() { cout << "Data<T1&, T2*>" << endl; }
};
int main(){
Data<int, int> d0;
Data<double, int> d1;
Data<int, char> d2;
Data<double, double> d3;
Data<double*, double*> d4;
Data<int*, char*> d5;
Data<int*, char> d6;
Data<int&, char&> d7;
Data<int&, double&> d8;
Data<int&, double*> d9;
return 0;
}
4.3 模板特化使用举例
- 首先创建一个日期类
struct Date{
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator>(const Date& d) const{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day)){
return true;
}
else{
return false;
}
}
bool operator<(const Date& d) const{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day)){
return true;
}
else{
return false;
}
}
int _year;
int _month;
int _day;
};
- 使用函数模板特化举例
// 函数模板 -- 参数匹配
template<class T>
bool Greater(T left, T right){
return left > right;
}
// 特化--针对某些类型进行特殊化处理
template<>
bool Greater<Date*>(Date* left, Date* right){
return *left > *right;
}
int main(){
cout << Greater(1, 2) << endl;
// 可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Greater(d1, d2) << endl;
// 可以比较,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Greater(p1, p2) << endl;
// 可以比较,如果缺少特化,比较的会是指针的地址,故会结果错不可知
return 0;
}
- 使用类模板特化举例
namespace ylq{
template<class T>
struct less{
bool operator()(const T& x1, const T& x2) const{
return x1 < x2;
}
};
// 全特化
//template<>
//struct less<Date*>{
// bool operator()(Date* x1, Date* x2) const{
// return *x1 < *x2;
// }
//};
// 偏特化
template<class T>
struct less<T*>{
bool operator()(T* x1, T* x2) const
{
return *x1 < *x2;
}
};
}
int main(){
ylq::less<Date> lessFunc1;
cout << lessFunc1(d1, d2) << endl;
ylq::less<Date*> lessFunc2;
cout << lessFunc2(p1, p2) << endl;
return 0;
}
5 模板的分离编译
5.1 分离编译定义
- 一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式
5.2 模板的分离编译场景
- 首先,我们将vector类的声明放在vector.h文件,将其中三个较长函数放在vector.cpp文件
1. vector.h
#pragma once
#include <assert.h>
// 声明
namespace ylq{
template<class T>
class vector{
public:
typedef T* iterator;
vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
~vector(){
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
size_t capacity() const{
return _end_of_storage - _start;
}
const T& operator[](size_t pos) const{
assert(pos < size());
return _start[pos];
}
T& operator[](size_t pos){
assert(pos < size());
return _start[pos];
}
size_t size() const{
return _finish - _start;
}
void reserve(size_t n);
void push_back(const T& x);
iterator insert(iterator pos, const T& x);
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
2. vector.cpp
#include"vector.h"
namespace ylq{
template<class T>
void vector<T>::reserve(size_t n) {
if (n > capacity()) {
size_t sz = size();
T* tmp = new T[n];
if (_start) {
//memcpy(tmp, _start, sizeof(T)*sz);
for (size_t i = 0; i < sz; ++i) {
tmp[i] = _start[i]; // T对象是自定义类型时,调用T对象operator=
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
template<class T>
void vector<T>::push_back(const T& x){
insert(_finish, x);
}
template<class T>
typename vector<T>::iterator vector<T>::insert(typename vector<T>::iterator pos, const T& x){
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _end_of_storage){
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
//
iterator end = _finish - 1;
while (end >= pos){
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
}
- 其次在Test.cpp文件中运行如下代码
#include "vector.h"
int main(){
ylq::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for (size_t i = 0; i < v.size(); ++i){
cout << v[i] << " ";
}
cout << endl;
ylq::vector<double> vd;
vd.push_back(1.1);
return 0;
}
编译报错,报错原因是 “无法解析的外部符号” ,原因如下
在vector实例化时,vector.h中有定义的成员函数会实例化,编译阶段就确定了地址,而resreve push_back insert在vector.h中只有声明,没有定义,其地址只能在链接阶段确认,但是,由于T无法确定,也就是说push_back insert根本无法实例化,因此不会生成具体代码,那么resreve push_back insert也就无法在符号表找到,因此链接阶段找不到地址,故链接失败
- 解决方法:在模板定义的文件中,进行显式实例化,即在vector.cpp的文件中添加如下代码,便可完成模板的分离编译,但是缺点是,当要使用vector类存放其他类型的数据时,需要添加相应的显式实例化
// 显式实例化
template
vector<int>;
5.3 模板分离编译总结
- 模板的声明和定义最好放在一个文件xxx.h中,如果真的想分离,可在同一文件中,类内声明,类外定义模板定义的位置显式实例化
template vector<数据类型>;
- 缺点是不同的类型都需要显式实例化,使用新类型就得添加一次显式实例化