在讲解模板前,我提出一个问题:
如何实现一个通用的swap交换函数?
也许你可以这样:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
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;
}
......
把每种类型都进行重载,写出n多种交换函数。
但这很明显是一个费力不讨好的方法,不仅会让代码冗余重复,而且写出这样的代码,也会耗费精力。
那么有没有一种办法,我们给编译器一个模板,编译器自动生成函数?
C++就样提供了模板,而C++最重要的STL库,也就是起源于模板的。
模板分为函数模板与类模板,我们先通过函数模板来了解模板的大部分规则:
功能:
函数模板代表了一个函数家族,在使用时被参数化,根据实参的类型生成特定类型的版本
也就是说,我们可以通过一个函数模板,让编译器生成一整个同类型的函数家族。
语法:
template <typename T1, typename T2 ......>
template <class T1, class T2 ......>
以上两种都是创建模板的方式,class
和typename
在里面是一样的功能,没有区别。在STL中多用class
,所以本博客也以class
为主。
而被class
和typename
定义是模板参数,它可以被任意类型代替。如果你不能理解,我们不妨看看示例。
示例:
template <class T>
T Add(T x, T y)
{
return x + y;
}
以上就是一个函数模板,T是一个模板参数,它代表一个类型。如果T是int,那么以上函数就是:
int Add(int x, int y)
{
return x + y;
}
这个函数是不是很熟悉了?
也就是说,T是可以被替换的类型,那么我们要如何确定这个T的类型?
编译器会根据调用函数时传入的参数,自动判断类型。
比如以下调用:
Add(1, 5);
Add(3.0, 5.0);
对于Add(1, 5);
,其两个参数都是int
类型,那么此时模板就会生成一个int
类型的Add
函数。
对于Add(3.0, 5.0);
,其两个参数都是double
类型,此时模板就会生成一个double
类型的Add
函数。
以此类推,我们不论想要多少种类型,只需要传入参数,让编译器自动识别,而我们只需要写一个模板,就可以衍生出无数种函数,这就是模板的优势。
那么我们现在来实现一下一开始swap函数的模板:
template <class T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
不过要注意,模板只是一个蓝图,本身不是函数,当我们传入指定类型参数,其就会生成相应的函数。
再回到刚刚的Add
函数模板。
template <class T>
T Add(T x, T y)
{
return x + y;
}
如果我们传入两个不同类型的参数怎么办?
比如这样:
int a = 5;
double b = 3.0;
Add(a, b);
请问这个模板是转化为double
类型好呢,还是转化为int
类型好呢?
对编译器来说,这就是一个大问题了,如果转化错误了,编译器就要背黑锅。所以遇到这种情况,编译器不会为我们做决定,而是报错,必须由程序员指明要用哪一种类型的模板。
比如使用强制类型转换:
int a = 5;
double b = 3.0;
Add(a, (int)b);
Add((double)a, b);
上述代码,Add(a, (int)b);
将b转化为了int
,此时模板推演出int
类型的函数;而Add((double)a, b);
将a转化为了double
类型,此时模板推演出double
类型的参数。
此外,我们还可以使用显式实例化的方式:
显式实例化,就是在使用模板时,明确的告诉模板,要用什么类型。
语法:
函数名 <类型> (参数);
比如:
int a = 5;
double b = 3.0;
Add<int> (a, b);
Add<double> (a, b);
Add
此代码就是推演出int
类型的函数;而Add
就是推演出double
类型的函数。
在设置模板参数时,可以设置缺省值,在显式实例化时,对于没有指明的模板参数,会被推演为缺省值。
看到以下模板:
template <class T1, class T2>
void func(T1 a)
{
T2 b = 5;
cout << a / b;
}
这个模板中,函数func
只有一个形参,而T2
这个模板参数不在形参中,而是用于定义b
这个变量了。此时T2
是无法根据函数的实参推演出来的,必须显式实例化中指明。
比如这样:
func<int, int>(8);
此时T1
为int
类型,T2
为int
类型,执行整数除法8/5=1
;
接下来我们给模板参数缺省值试试:
template <class T1, class T2 = double>
void func(T1 a)
{
T2 b = 5;
cout << a / b;
}
此处我们给了T2
一个缺省值double
,也就是说我们不传入第二个模板参数,T2
就会被推演为缺省值double
。
func<int>(8);
func(8);
对于func
,我们只传了一个模板参数,此时T2
就会被推演为缺省值double
,变量b
就是double
类型了,此时执行小数除法8 / 5.0 = 1.6
。
对于func(8);
,我们没有进行显式实例化,此时对于T1
,由于我们传入了参数a
为int
类型,此时T1
被推演为int
,而T2
得到缺省值double
,执行小数除法8 / 5.0 = 1.6
。
模板本身不是一个函数,所以同名的函数和模板是可以共存的。
比如这样:
template <class T>
T func(T x, T y)
{
return x + y;
}
int func(int x, int y)
{
return x + y;
}
以上代码中我们创建了一个Add
的模板,一个Add
的int
类型函数。
那么我们调用函数时,会这么调用呢?
调用函数时,如果函数有现成的,完全匹配的函数,那么不会调用模板
如果我们这样调用函数:
Add(1, 2);
这个调用,两个参数都是int
类型,而我们刚好写了一个两个参数都是int
类型的函数,那么此次调用就不会调用模板,而是直接用我们写过的函数。
调用函数时,如果可以通过模板产生更加匹配的函数,那么会调用模板进行推演
如果我们这样调用函数:
Add(1.0, 2.0);
此时两个参数都是double
类型,如果不存在模板的话,double
就会被转化为int
,然后调用int,int
的函数。但是由于模板存在,可以推演出更加匹配从函数double,doouble
类型。所以此次调用会调用模板。
类模板的特性与函数模板几乎一致,此处不额外讲解了,只讲解类模板的特殊的地方。
语法:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
先简单为大家展示一个类模板:
template <class T>
class stack
{
public:
stack(size_t capacity = 10)
:_pData(new T[capacity])
,_size(0)
,_capacity(capacity)
{}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
这就是一个stack
类的模板,有了这个模板,我们的栈就可以存放int
,double
等等的其他类型了。
我们的模板参数为T,由于类没有传参的概念,不能通过参数来推演类型,所以一般而言类的模板都是要显式实例化的。
比如:
stack<int> s1;
stack<double> s2;
通过模板创建的类,其类名与类型也有所不同,接下来我们看看规则:
在一般的类中,类的类名和类型符号相同
比如stack
的类,其类名为stack
,类型也为stack
。
而在类模板中,不能单纯的将类名作为类型了,比如:
stack<int> s1;
stack<double> s2;
请问s1和s2的类型都是stack吗?
s1明明是用int推演的类,s2是用double推演的类,两者有很大的区别,如果都是stack类,后续如何区分?
所以我们用了其他规则来修饰这个类型符号,从而区分开同一个模板推演出来的不同类型。
类型 = 类名<模板参数>
对于stack
其类名为stack
,类型为stack
;
对于stack
其类名为stack
,类型为stack
;
当我们希望把一些类中的成员定义在类的外部时,那就需要声明和定义分离。
假设我们希望分离析构函数~stack
。
对于一般的类,我们会这样分离:
class stack
{
public:
stack(size_t capacity = 10)
:_pData(new int[capacity])
,_size(0)
,_capacity(capacity)
{}
~stack();//声明
private:
int* _pData;
size_t _size;
size_t _capacity;
};
stack::~stack()
{
//函数体
}
首先要用类型::函数名
来限定作用域,然后再开始定义函数。
所以我们的类模板也要类型::函数名
来限定作用域。类模板的类型刚刚介绍过,就是stack
,所以函数的声明应该这样写:
stack<T>::~stack()
{
//函数体
}
但是这还不是一个合法的声明。
对于类模板,当在类外定义函数时,要添加模板参数列表。
也就是说要这样:
template <class T>
stack<T>::~stack()
{
//函数体
}
这才是一个模板类的成员函数声明。
我们的模板参数也可以不是一个类型,而是一个数值。
对于指定类型的参数,我们称为类型形参,比如int,double。
对于一个数值的参数,我们称为非类型形参。
比如以下类模板:
template <class T, int N>
class Array
{
public:
private:
T _arr[N];
};
我们在模板参数列表中有一个类T
,一个int
类型的N
,此时T
就是类型形参,N
就是非类型形参。
在定义类时,可以通过这个非类型参数,为这个类传入一些值:
Array<int, 10> a;
Array<double, 20> b;
对于a
这个对象,我们在创建时,为T
传入了int
,N
传入了10
。那么经过初始化,_arr
就指向了10
个int
的数组。
对于b
这个对象,我们在创建时,为T
传入了double
,N
传入了20
,那么经过初始化,_arr
就指向了20
个double
类型的数组。
注意:非类型模板参数必须是整型,bool,char类型的常量
通常情况下,使用模板可以实现一些与类型无关的代码,但是对于一些特殊的类型,有可能会得到错误的结果。
比如以下代码:
template<class T>
bool Less(T x, T y)
{
return x < y;
}
int main()
{
int* p1 = 5;
int* p2 = 10;
cout << Less(p1, p2) << endl;
return 0;
}
这个函数模板中,我们用Less
来比大小,我们此时传入了两个指针p1
,p2
,原本的意图是通过指针来比较数字5和10的大小。但是当传入后,我们比较的是p1 < p2
,也就是对两个指针比大小了,这不符合我们的预期。也就是说在面对指针的时候,我们需要特殊处理,这就需要模板特化了。
模板特化的功能就是:
在原模版的基础上,针对特殊类型进行特殊化的实现方式
其分为函数模板的特化与类模板的特化:
我们先看到一个函数模板特化,再讲解语法:
//基础模板
template<class T>
bool Less(T x, T y)
{
return x < y;
}
//模板特化
template<>
bool Less<int*>(int* x, int* y)
{
return *x < *y;
}
第一段代码是一般的函数模板,而第二段是对int进行了特化的版本,当我们传入参数类型为int时,就会调用这个特化版本,执行*x < *y
,先解引用再比较。
那么这个模板特化有什么特点呢?
首先,我们将T
特化为了int*
,所以T
不再是一个需要推演的参数了,此时将T
从模板参数列表中抽离出来,改为int*
放到函数名Less
后面,用尖括号括起来,然后把函数参数中所有的T改为特化后的int*
。
模板特化要满足以下语法:
但是函数模板是一个没有必要的东西,因为相比于对模板进行特化,不如直接重载一个函数,模板特化在类模板中较为有用。
比如这样:
bool Less(int* x, int* y)
{
return *x < *y;
}
可以达到一样的效果,而且无需繁杂的语法。
模板特化的主要用处体现在类模板特化上,函数并不推荐使用这个模板特化。
类模板特化的语法和刚才是一样的:
模板特化要满足以下语法:
类模板特化分为全特化和偏特化。
全特化是指将模板参数的所有参数都确定下来
比如以下案例:
//基础模板
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data" <<endl;}
private:
T1 _d1;
T2 _d2;
};
//模板特化
template<>
class Data<int, char>
{
public:
Data() {cout<<"Data" <<endl;}
private:
int _d1;
char _d2;
};
此处我们将T1
,T2
两个参数都设立了特化,这就叫做全特化。
只有模板参数第一个值为int,第二个参数为char,调用此类。
比如:
Data<int, char> d1;
这里的d1
就是一个模板特化创造出来的类对象。
偏特化是指并没有把模板参数确定下来,但是对满足特定条件的模板参数,执行特化版本
偏特化分为部分特化和限制特化:
部分特化是只将一部分参数特化
比如以下案例:
//基础模板
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data" <<endl;}
private:
T1 _d1;
T2 _d2;
};
//模板特化
template<class T1>
class Data<T1, char>
{
public:
Data() {cout<<"Data" <<endl;}
private:
T1 _d1;
char _d2;
};
以上的第二段代码就是一个部分特化,其只特化了第二个模板参数为char
,只有当第二个参数为char
类型,不论第一个参数类型是什么,都会调用特化版本的类了。
由于T1
没有被确定下来,仍然需要推演,所以第一行的模板参数列表保留T1
。
比如:
Data<int, char> d2;
Data<double, char> d3;
这里的d2
,d3
都是通过模板特化创建出来的对象,因为它们满足第二个模板参数是char
类型。
限制特化是对参数进行条件限制,但是没有把参数类型确定下来
比如以下案例:
//基础模板
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data" <<endl;}
private:
T1 _d1;
T2 _d2;
};
//模板特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data() {cout<<"Data" <<endl;}
private:
T1 _d1;
T2 _d2;
};
此处
是限定:当T1
,T2
为指针是,调用此模板特化。
也就是说,这个过程中,T1
,T2
的类型是不确定的,任然需要推演,所以第一行的模板参数列表保留了T1
,T2
。
而这样对模板参数进行限制,就是限制特化了。