文章目录
📖前言
在我们之前的学习中,我们学习了初阶的模板,函数模板、类模板,并学会了简单的运用,本章将继续深入学习模板的内容,模板进阶…
1. typename的深一层应用
(1)先看一段代码:
void print_list(const list<int>& lt)
{
list<int>::const_iterator cit = lt.begin();
while (cit != lt.end())
{
cout << *cit << " ";
cit++;
}
cout << endl;
}
上述代码为打印一个链表中节点数据的代码。
(2)我们将上述函数改为函数模板:
template<class T>
void print_list(const list<T>& lt)
{
typename list<T>::const_iterator cit = lt.begin();
while (cit != lt.end())
{
cout << *cit << " ";
cit++;
}
cout << endl;
}
- 用泛型写一个print函数
- list确定了,但是list的模板参数没确定
STL中list的迭代器的实现:
以list为例子:
- 这里的Iterator的类型是__list_iterator<T, Ref, Ptr>,
- 虽然list中的迭代器类型被实例化成了假如是int类型的,在list的迭代器的类中T就是int
- 但是这里却没有实例化,还是T,是个虚拟的类型
- 取内嵌类型都要加上typename
- 类模板 模板虚拟类型 没有实例化之前不能去它里面找内嵌定义的类型
- 类模板没有实例化,找出来也是虚拟类型,后期无法处理 – 编译报错
typname 告诉编译器后面这一串是一个类型,等Iterator实例化之后,再去它里面找这个内嵌类型。
(3)接着用一下适配器模式:
template<class Container>
void print_container(const Container& lt)
{
//typename Container::const_iterator cit = lt.begin();
//用auto则不用加typename,因为auto是根据返回值类型自动推导
//去类模板里面取的话都要加上typename
auto cit = lt.begin();
while (cit != lt.end())
{
cout << *cit << " ";
cit++;
}
cout << endl;
}
上述代码是容器适配器的思想,适配一个Container容器,同样的道理,容器虽然确定了,但是容器内的迭代器却是虚拟的。
整体模板的虚拟类型都没确定:
- 不是类模板不行,而是,模板包含中包含虚拟类型时,就需要用到typename
- 类模板是不允许去取的,有可能还会取出来虚拟类型
注意:
- 用auto则不用加typename,因为auto是根据返回值类型自动推导
- 去类模板里面取的话都要加上typename
2. 非类型模板参数
2.1 非类型模板参数的引入:
定义一个静态的栈:
#define N 100
template<class T>
class Stack
{
private:
T _a[N];
int _pop;
};
这里有个缺陷,这里的栈的大小是固定死的,每次创建出来的栈都是一样的大小。
问题:
模板让我们实现了同时可以创建不同数据类型的栈,但是不能实现不同数据个数的栈。
如下就是上述代码不能实现的功能:(缺陷)
- Stack< int > – 100个数据
- Stack< char > – 500个数据
我们这时候就引入了非类型模板参数:
直接见代码:
template<class T, size_t N = 100>
class Stack
{
public:
//非类型模板参数是不能修改的 -- 因为常量是不能修改的
void f()
{
N = 10;
}
private:
T _a[N];
int _pop;
};
- 非类型模板参数 – 还可以给一个缺省值
- <>里也可以不定义类型,也可以定义常量
- N是常量不能修改
- 模板不仅可以不定义类型,还可以定义常量
- 比宏更好,可以传一个常量过去,定义不同的对象,可以传不同的常量过去
- 非类型模板参数基本上是整形。
- 非类型的模板参数必须在编译期就能确认结果。
2.2 array的特性和使用:
C++引入了一个新的类模板 —— array
这里便用到了非类型模板参数,size_t N
array和vector的区别:
- array是一个静态的数组, 不支持插入删除
- 而vector却是动态的
- array的价值是:开大数组
- array没有独特的地方,是C++11新增的
大小不一样:
- vector中只存几个指针来维护数组,实际不存数据
- 而array是直接存数据
array与vector的区别:
- array是封装过了的原生数组 - - 越界一定能查到(读写都能查得到)
- array<int, 100> a1;
- array是C++11新增的,相对于vector而言,没有独特的地方
- operator [ ] 能严格检查越界
- 而普通的数组检查越界不一定能查出来,因为是抽查
- vector可以扩容,array不可以
总结:
- array对比原生数组,还有一些越界检查的优势
- 但是实际中,统一用vector更香
- 类型模板参数 - 虚拟类型
- 非类型模板参数 – 常量
3. 模板的特化
3.1 模板的特化的引入 + 函数模板的特化:
针对某些类型特殊化处理 - - 模板的特化
概念:
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。
**比如:**实现了一个专门用来进行小于比较的函数模板。
比较大小模板,直接见代码:
//模板的特化
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//针对某些类型特殊化处理 -- 模板的特化
//首先要有一个基础的函数模板
//针对Date*进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
bool Less(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl; //可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; //可以比较,结果正确
Date* p1 = new Date(2022, 7, 16);
Date* p2 = new Date(2022, 7, 15);
//这里比较的是地址
cout << Less(p1, p2) << endl;
//只用这种方式(p1, p2相比较)可以用模板的特化来解决
return 0;
}
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
问题的解决:
我们比较的是日期的大小,直接传日期类的对象就可以调用日期类的运算符重载,但是如果传的是日期类对象的指针,比较的就是地址的大小,显然不是我们想要的比较日期类对象的大小。如果在我们只传指针还要解决问题的限制下,这时应用模板特化就可以很好的解决这种问题。
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
注意:
- 上述三个同时存在的时候,先去匹配函数
- 函数模板不一定要特化
- 类模板就需要具体的特化了
- 一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
- 因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化,直接写一个具体类型的函数更好
3.2 类模板的特化 + 全特化和偏特化(半特化):
(1)全特化:
- 全特化即是将模板参数列表中所有的参数都确定化。
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//类模板的特化 -- 全特化(写死了)
template<>
class Data<int, double>
{
public:
Data() { cout << "Data<int, double>" << endl; }
};
- 制定好了两个模板参数 —— 写死了
- 只能固定的类型走这个函数
- 例如:只能是int 和 double走全特化的模板
(2)偏特化(半特化):
- 任何针对模版参数进一步进行条件限制设计的特化版本。
//半特化 / 偏特化()半特化不是特化一半
//1、将部分模板参数列表中的一部分参数特化
template<class T1>
class Data<T1, char>
{
public:
Data() { cout << "Data<T1, char>" << endl; }
};
//2、偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
//只要T1 和 T2是指针就走这个 -- 针对指针特殊化处理
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> d1;
Data<int, double> d2;
//只要第二个是char都会匹配:半特化/偏特化
Data<int, char> d3;
Data<char, char> d4;
//只要是两个指针
Data<int*, int*> d5;
Data<int*, char*> d6;
Data<int*, string*> d7;
Data<int*, void*> d8;
//void不是类型,但是void*是一个类型,void*是不能解引用不能++
Data<int*, int> d9;//匹配原生的指针
Data<int&, char&> d10;
return 0;
}
运行结果如下:
3.3 模板特化的应用:
//模板的特化 -- 应用
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};
//偏特化
//只要是指针都走这里
template<class T>
struct Less<T*>
{
bool operator()(T* x, T* y) const
{
return *x < *y;
}
};
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 6);
Date d3(2022, 7, 8);
vector<Date> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,结果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
//可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
//此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
//但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
sort(v2.begin(), v2.end(), Less<Date*>());
vector<int*> v3;
v3.push_back(new int(3));
v3.push_back(new int(1));
v3.push_back(new int(2));
sort(v3.begin(), v3.end(), Less<int*>());
return 0;
}
4. 模板分离编译
这个问题在初识模板章节就已经讲解: