之前学习函数重载的时候,我们写了一个交换函数Swap
void Swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
void Swap(char& left, char& right)
{
char tmp = left;
left = right;
right = tmp;
}
void Swap(double& left, double& right)
{
double tmp = left;
left = right;
right = tmp;
}
int main()
{
int a = 0, b = 1;
char c = 'A', d = 'B';
double e = 1.1, f = 2.2;
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
虽然C++支持函数重载,让我们可以同时实现多个类型的变量进行交换,但是这仍存在不好的地方:
C++为了解决以上问题,提出泛型编程的概念!!!
泛型编程是指编写与类型无关的通用代码,是代码复用的通用手段。模板是泛型编程的基础
其中模板分为函数模板和类模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
template
返回值类型 函数名(参数列表)
{
//…
}
// 以上面介绍的Swap函数为例
template <class T> //模板参数列表 ——参数类型
void Swap(T& left, T& right) //函数参数列表 ——参数对象
{
T tmp = left;
left = right;
right = tmp;
}
注意:typename关键字可以替换class用于定义模板参数,但struct不能代替class。
函数模板并不是一个函数,是编译器用于产生特定类型函数的模具,所以模板将本应该我们进行的复杂操作交给了编译器执行。
思考一个小问题,主函数中的3个Swap函数是一样的吗?
template <class T>
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
int main()
{
int a = 0, b = 1;
char c = 'A', d = 'B';
double e = 1.1, f = 2.2;
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
答案显然是不一样的,因为每个Swap函数参数类型都不相同,3个Swap函数会依据实参参数类型转换成不同的模板函数,如下图所示
同时我们通过汇编发现不同Swap函数Call的地址不同,也可以验证结果是正确的!
编译器编译阶段:编译器通过传入的实参类型来推演生成对应的模板函数。
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
template <class T> //模板参数列表 ——参数类型
T Add(const T& left, const T& right) //函数参数列表 ——参数对象
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.10, d2 = 20.20;
// 隐式实例化
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
return 0;
}
注意:cout << Add(a1, d2) << endl;
是不对的,因为两个实参类型不同,编译器无法推演出T为int还是double。
此时有两种处理方式:
(1)用户自己强制转换
cout << Add(a1, (int)d2) << endl;
cout << Add((double)a1, d2) << endl;`
(2)使用显示实例化
cout << Add<int>(a1, a2) << endl;
cout << Add<double>(d1, d2) << endl;
cout << Add<int>(a1, d2) << endl;
cout << Add<double>(a1, d2) << endl;
// 非模板函数
int Add(int left, int right)
{
return left + right;
}
// 函数模板
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
Add(1, 2);
会直接与非模板函数匹配,因为编译器检测到存在专门处理int类型的Add函数,便不会用函数模板特化;
Add
是显示实例化,编译器会根据函数模板特化出一个实例Add函数。
// 非模板函数
int Add(int left, int right)
{
return left + right;
}
// 函数模板
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函
数 }
Add(1, 2);
与非模板函数完全匹配,不需要模板实例化;
Add(1, 2.0);
虽然可以隐式类型转换,但这里可以用函数模板特化出更加匹配的Add(int, double)
函数。
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
以栈为对象,编写一个栈类模板,代码如下:
template <class T>
class Stack
{
public:
Stack(int capacity = 4)
:_top(0)
, _capacity(capacity)
{
_a = new T[capacity];
}
~Stack()
{
delete[] _a;
_a = nullptr;
_top = _capacity = 0;
}
private:
T* _a;
int _top;
int _capacity;
};
PS:此时如果想把类的成员函数在类中声明,类外定义,需要加模板参数列表
template <class T>
class Stack
{
public:
Stack(int capacity = 4)
:_top(0)
, _capacity(capacity)
{
_a = new T[capacity];
}
// 在类中声明,类外定义
~Stack();
void Push(const T& x);
private:
T* _a;
int _top;
int _capacity;
};
// 析构函数在类外定义
template <class T>
Stack<T>::~Stack()
{
delete[] _a;
_a = nullptr;
_top = _capacity = 0;
}
// 插入函数在类外定义
template <class T>
void Stack<T>::Push(const T& x)
{
//...
}
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
int main()
{
Stack<int> s1;
Stack<double> s2;
Stack<char> s3;
Stack<int*> s4;
return 0;
}
注意:在之前学习类的时候,类名和类型是一样的。但在类模板中,以栈为例,Stack
为类名,Stack
,Stack
,Stack
和Stack
为类型