泛型编程
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
void Swap(double& left, double& right) {
double temp = left;
left = right;
right = temp; }
void Swap(char& left, char& right) {
char temp = left;
left = right;
right = temp; }
模版初阶
✳️模版是不支持声明和定义放到两个文件中的。(进阶中回讲)
函数模版
格式
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
✳️注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替
class)
原理
✳️1.函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
✳️2.函数模版,并不是调的同一个函数,而是不同的函数。在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
✳️3.函数模版的类型**一般是(也有函数模版显示实例化)**编译器根据实参传递给形参,推演出来的;
函数模版实例化
✳️用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
函数模版隐式实例化
✳️隐式实例化:让编译器根据实参推演模板参数的实际类型;
T Add(const T& left, const T& right) {
return left + right; }
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
✳️ 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要
背黑锅
Add(a1, d1);-------❌:上面给了解释
✳️ 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a, (int)d);-------✅:解决了上面的
函数模版显示实例化
✳️ 显式实例化:在函数名后的<>中指定模板参数的实际类型;
template<class T>
T* Func(int n)
{
return new T[n];
}
int* p1 = Func<int>(10);-------➡️必须得显示实例化
double* p2 = Func<double>(10);
特殊注意点(函数模版声明和定义分离)
声明:
template<typename T>------➡️不能少
void Swap(T& left, T& right);
定义:
template<typename T>-----➡️不能少
void Swap(T& left, T& right)
{
..........
}
类模版
谈C里的typedef的缺陷
typedef int DataType;
Stack st1;--------➡️存int
Stack st2;--------➡️存double
则C语言只能有一个类型,所以无法解决
Stack<int>
Stack<double> 而不是一个;
类模版定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
打样
template<class T>
class Vector
{
public :
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
特殊注意点(类模版中函数声明和定义分离)
✳️注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()---------➡️指定类域不能忘了,若函数有返回值则要加返回值
{
if(_pData)
delete[] _pData;
_size = _capacity = 0; }
类模版实例化
✳️类模版的类型显示实例化,是明确指定的;
✳️类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
浅谈模版声明和定义分离 【5.30日】
首先template.h会在各个.cpp中展开,
然后template.c和test.c都会在预处理、编译、汇编、链接分别生成.i、.s、.o文件
分析
template.i:首先是template.h在其里面展开,
Test.i也是template.h在其里面展开;
后面就是单线化处理编译到汇编到链接;
template.o与Test.o在链接到时候合体;
编译器在template.i里面下不了手,是个空的,template.o也是空的啥都没有,最后生成文件但符号表里面是空的,因为它不知道T是什么,若明确知道T是int或double等我也好生成,但现在啥也生成不了;
再看Test里面调用的地方能不能过,是能过的,函数转换为call地址但地址不会先填上去(没有地址),因为只有声明,(老师写成call_Z4Swap…),不知道vector的构造函数地址,但会按照函数生成的命名格式来写,只不过地址不填而已,正常情况下我会等到链接的时候去找地址,但现在不能从template里面找到,虽然test知道要什么类型的函数,但template不知道因为模版并不会去生成,因为类型T并不能去确定,就会出现链接时找不到这些函数模版调用的地址
解决办法
显示实例化指定
对症下药:你链接不上的原因是什么?原因是你不知道该T用什么类型,那么我提前告诉你就能保证后面链接上了
在temolate.cpp里面加上
template
void Swap<int& left, int& right>;------➡️对于Swap函数
template
class Vector<int>;----➡️对于vector<int>(同理double)
为什么放在一起就能解决呢?
template.h中的声明与,template.cpp中的定义一起放入template.hpp后,template.hpp在Test里面展开后即有什么又有定义,则在编译的时候就将他们实例化了,直接会call所调用函数的地址,我们就不需要在链接的时候去找了。
多个模版参数
template<class K, class V>
void Func(const K&, const V& value)
{
........;
}
Func(1,1.2);
模版进阶
反向迭代器的另一种实现引发的问题
✳️
我们一开始实现反向迭代器是加了class Ref,class Ptr的,那如果我不加呢?
template<class Iterator, class Ref, class Ptr>
class Reverse_iterator
也就是我list只传正向迭代器过去
template<class Iterator>
若只传一个正向迭代器的挑战是:返回值Ref,Ptr,即operator *,operator->返回值是啥?因为我还有迭代器
那有什么办法呢?
目前主要是返回值类型不好解决;
根据看stl源代码,我们看到了他们标准正向迭代器里面会有typedef Ref reference
typedef Prr pointer
可是我们现在只有iterator,但我们想取迭代器指向的管理数据类型value_type
想取数据的引用就是iterator:reference
想取数据的指针就是iterator:pointer
那么referenc、poinrer就可以做我们想要的返回值了;
但是你现在用iterator:reference;iterator:pointer编译器会报错不认识这些东西
但是为什么会不认识呢?因为iterator它是类模版,它本身就没有被实例化,编译器无法去没有实例化的模版找东西!因为去找的话,可能会找到带参数T的,那这个时候就会出问题,出什么问题呢?
到时候开始实例化以后,你迭代器把自己实例化,把T实例化成int等等,但是他只会实例化iterator正向迭代器类模版里面的。然后我们Reserve_iterator反向迭代器类模版只会实例化引入的iterator正向迭代器.
没有人会去管 iterator:reference--➡️T&这个虚拟类型
iterator:pointer-➡️T*这个虚拟类型
因为在iterator类模版没有实例化之前,Reserve_iterator不敢去里面去找iterator内嵌定义类型
因为类模版没有实例化,找出来也是虚拟类型,后期无法处理。假设我从iterator里面去找了,找出来是T&,T* ,可我不认识,因为list只会实例化iterator那边的T,又不会给我们Reserve_iterator这边。
解决办法:此时就要加一个关键字:typename--typename iterator::referenece
(typename我们在写模版template<typename T>里面用过)
在这里typename的价值就是告诉编译器后边一坨屎一个类型,等iterator实例化以后你再到它里面去找它的内嵌定义类型!
✳️
还有要注意的是:vector这样还是不行,因为vector的迭代器是原生类型的指针,你不可能到原生类型里面去,嵌入定义类型,如:typedef Ref reference;typedef Ptr pointer等;
解决办法一:不用原生指针,而是用和list一样封装原生指针,弄一个iterator类里面包含原生指针,后面做法就如同list了
解决办法二:萃取(特化)去处理。
typename再度讲解
看一段代码
template<class T>
void PrintList(const list<T >& lt)
{
typename list<T>::const_iterator it = lt.begin()
//list<T>::const_iterator it = lt.begin()-----❌这里会报错误!
while(it != lt.end())
{
cout << *it << Lendl;
it++;
}
}
或者
template<class Container>
void PrintList(Container& con)
{
typename Container::const_iterator it = con.begin()
Container::const_iterator it = con.begin()-----❌这里会报错误!
while(it != con.end())
{
cout << *it << Lendl;
it++;
}
}
都是因为类模版还没有被实例化!自然你不能去类模版里面去拿内嵌类型,必须得加上typename才能过!告诉编译器等他实例化后去取。```
非类型模版参数—常量
模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
✳️假如我要定义静态栈
define N 500
template<class T, size_t N>
//template<class T>
class Stack
{
private:
T _a[N];
int _top;
}
int main()
{
Stack<int> st1;--我想要50个空间
Stack<double> st2;----我想要100个空间
模版能让我做到你想存int就int,要double就double
但是不能让你随意想要多少空间
但是现在非类型模版参数就可以!
Stack<int,50> st1
Stack<double,100> st2
}
模版的特化
概念
✳️通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,
来看一份代码
template<class T>
bool Less(T left, T right)
{
return left < right;
}
cout << Less(1, 2) << endl;//可以比较,结果正确
Data *d1 = new Data(2022, 7, 16);
Data *d2 = new Data(2022, 7, 15);
cout << Less(*d1, *d2) << endl;//可以比较,结果正确
cout << Less(p1, p2) << endl;---❌得到结果是随机,因为这里比较的是地址大小
若不让你传 *p1,*p2你有没有办法来解决?
✳️那就需要模版的特化来解决了!
模版特化:针对某些类型进行特殊化处理!
函数模版特化且小试牛刀(函数模版其实就别特化了,直接写个具体类型函数就行)
✳️函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
针对某些类型要特殊化处理
template<>
bool Less<Data*>(Data* left, Data* right)
{
return *left < *right;-----➡️可以在该函数内根据自己的需求来进行写比较方法
}
类模版特化
全特化且小试牛刀
✳️全特化即是将模板参数列表中所有的参数都确定化。
来看一份代码
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, char>
{
public:
Data(){cout << "Data<T1, char>"<< Lendl;}
};
int main()
{
Data<int, int> d1;
Data<int, double> d2;
Data<int, char> d3;------只要第二个是char都会去匹配半特化。
Data<char, char> d4;
}
参数更进一步的限制(我不只是想指定某一个参数的具体类型,而是所有模版参数比如都为指针但是指针类型我不管)
✳️偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型
✳️意思是说:只要你T1 和 T2是指针那就走我------意思是我可以针对指针去处理
template <class T1, class T2>
class Data <T1*, T2*>
{
public:
Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
int main()
{
Data<int, int> d1;
Data<int, double> d2;
Data<int*, int*> d3;------➡️不管你是什么类型的指针,只要你是指针
Data<int*, double*> d4;
Data<char* double*> d5;
Data<void*, string*> d6;
Data<int, int*>---➡️只有一个指针,它会匹配原生<T1, T2>,想要匹配半特化的你必须按照他的要求来!必须得是两个指针!
只要你不是指针(不符合我特化的要求)就不会让你走我的偏特化
return 0;
}
//两个参数偏特化为引用类型
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&, double&> d;
}