我们在C++中如何实现一个通用的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 ......>
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的类型?
让我们利用模板特点实现一下最初说的通用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);
比如使用强制类型转换:
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);//推演出int类型的函数
Add<double> (a, b);//推演出double类型的函数
template <class T1, class T2>
void func(T1 a)
{
T2 b = 5;
cout << a / b;
}
这个模板中,函数func只有一个形参,而T2这个模板参数不在形参中,而是用于定义b这个变量了。此时T2是无法根据函数的实参推演出来的,必须显式实例化中指明。
比如这样:
func<int, int>(8);
接下来我们给模板参数缺省值试试:
//T2缺省值为double
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);
模板本身不是一个函数,所以同名的函数和模板是可以共存的。
比如这样:
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);//调用函数
Add(1.0,2.0);//调用模板
类模板的特性与函数模板几乎一致,此处只讲解类模板的特殊的地方。
语法:
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> s1;
stack<double> s2;
通过模板创建的类,其类名与类型也有所不同,接下来我们看看规则:
在一般的类中,类的类名和类型符号相同
而在类模板中,不能单纯的将类名作为类型了
stack<int> s1;
stack<double> s2;
//对于stack s1;其类名为stack,类型为stack
//对于stack s2;其类名为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()
{
//函数体
}
对于类模板,当在类外定义函数时,要添加模板参数列表。
template <class T>
stack<T>::~stack()
{
//函数体
}
namespace bite
{
// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:
T& operator[](size_t index){return _array[index];}
const T& operator[](size_t index)const{return _array[index];}
size_t size()const{return _size;}
bool empty()const{return 0 == _size;}
private:
T _array[N];
size_t _size;
};
}
注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T 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 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
函数模板的特化步骤:
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(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 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了
return 0;
}
但是函数模板是一个没有必要的东西,因为相比于对模板进行特化,不如直接重载一个函数,模板特化在类模板中较为有用。
比如这样:
bool Less(int* x, int* y)
{
return *x < *y;
}
可以达到一样的效果,而且无需繁杂的语法。
模板特化的主要用处体现在类模板特化上,函数并不推荐使用这个模板特化。
类模板特化的语法和刚才是一样的:
模板特化要满足以下语法:
必须存在一个基础模板
对于特化版本,template后面的<>内部不写被特化的模板参数
对于特化版本,在函数名后跟上<>,内部指定特化类型
将特化前的模板参数改为特化后的具体类型
类模板特化分为全特化和偏特化。
全特化是指将模板参数的所有参数都确定下来
比如以下案例:
//基础模板
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;
//输出Data,说明是模板特化造出来的类对象
偏特化是指并没有把模板参数确定下来,但是对满足特定条件的模板参数,执行特化版本
偏特化又分为部分特化和限制特化
//基础模板
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;
};