1. 泛型编程
如何实现一个通用的交换函数呢?
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
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;
}
模板的格式
template<class T>
,< >
中可以写typename
,也可以写class
;其中T
为一个虚拟类型template<class Tp>
, 虚拟类型我们可以自己取名,并不会进行限制template<class K>
// 函数模板 (根据我们所需要的类型,编译器会根据我们给出的模板进行实例化)
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
int x = 1, y = 2; // 类型都为int,因此只会实例化一个函数
Swap(x, y);
double c = 1.1, d = 2.2;
Swap(c, d);
char e = 'a', f = 'b';
Swap(e, f);
return 0;
}
2. 函数模板
2.1 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 函数模板格式
// 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)
2.3 函数模板的原理
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double
类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double
类型,然后产生一份专门处理double
类型的代码,对于字符类也是如此。
2.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化
- 隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
// 自动推演实例化 (编译器根据实参自己推演)
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
// 错误演示
// cout << Add(a1, d2) << endl;
// 此时,编译无法通过,因为推演只能使虚拟类型T实例化为int,或者double,并不可以兼顾两者
// 因此有以下两种解决方案
// 1.用户自己进行类型强转
cout << Add((double)a1, d2) << endl;
// 将a1强转为double类型,会存放到一个临时变量(临时变量具有常性), 因此,被引用时,一定要用const修饰
cout << Add(a1, (int)d2) << endl;
// 显示实例化
cout << Add<double>(a1, d2) << endl;
// <>中放置类型,表示我们想让编译器用这种类型来实例化(也就是说编译器不会自己去推演)
cout << Add<int>(a1, d2) << endl;
return 0;
}
// 定义两个虚拟类,来进行实例化,也可以很好的解决下面的问题
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
// 自动推演实例化
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
cout << Add(a1, d2) << endl;
cout << Add(d1, a2) << endl;
return 0;
}
2.5 模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数(可以和通用加法函数同时存在)
int Add(int left, int right) // _Z3Addii
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right) // _Z3TAddii
{
return left + right;
}
int main()
{
int a = 1, b = 2;
Add(a, b); // 如果有专门处理int的加法函数,会优先调用
Add<int>(a, b); // 会调用通用加法函数
return 0;
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
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函数
}
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
3. 类模板
// 如果使用typedef就只可以定义一个类型
// typedef int STDataType;
// typedef double STDataType;
// 使用类模板
template<typename T>
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = )" <<capacity<<endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(const T& x)
{
// ....
// 扩容
_a[_top++] = x;
}
private:
T* _a;
int _top;
int _capacity;
};
int main()
{
// 类模板一般没有推演时机,函数模板实参传递形参,推演模板参数
// 因此,我们直接显示实例化
// 他们是同一个类模板实例化出来的
// 但是模板参数不同,他们就是不同类型
Stack<double> st1; // double
st1.Push(1.1);
Stack<int> st2; // int
st2.Push(1);
return 0;
}
#define N 10
namespace qwy
{
template<class T>
class array
{
public:
T& operator[](size_t i)
{
assert(i < N);
return _a[i];
}
private:
T _a[N];
};
}
int main()
{
//int a2[10];
//a2[20] = 0; //只可以检查写越界
//a2[10]; // 读的话,检查不出来越界
qwy::array<int> a1;
for (size_t i = 0; i < N; ++i)
{
// 编译时会转化为a1.operator[](i)= i;
a1[i] = i;
}
for (size_t i = 0; i < N; ++i)
{
// 编译时会转化为a1.operator[](i)= i;
cout << a1[i] << " ";
}
cout << endl;
for (size_t i = 0; i < N; ++i)
{
a1[i]++;
}
for (size_t i = 0; i < N; ++i)
{
cout << a1[i] << " ";
}
cout << endl;
//a1[20]; // 两种都可以检查出来越界
//a1[10];
return 0;
}
4.模板的声明与定义分离
// 方法1
// stack.h
#include<iostream>
using namespace std;
template<typename T>
class Stack
{
public:
Stack(int capacity = 4);
~Stack();
void Push(const T& x);
private:
T* _a;
int _top;
int _capacity;
};
// stack.cpp (将声明与定义分离)
#include "Stack.h"
//此处一定要声明T是虚拟类型,每个函数前声明一次
template<class T>
Stack<T>::Stack(int capacity = 4)
{
cout << "Stack(int capacity = )" << capacity << endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
//此处一定要声明T是虚拟类型,每个函数前声明一次
template<class T>
Stack<T>::~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//此处一定要声明T是虚拟类型,每个函数前声明一次
template<class T>
void Stack<T>::Push(const T& x)
{
// ....
// 扩容
_a[_top++] = x;
}
// 显示实例化
// 此处一定要显示实例化,如果没有显示实例化,那么在汇编的时候,就找不到对应的函数放到符号表里面
template
class Stack<int>;
// test.cpp
#include "Stack.hpp"
int main()
{
// 这种显示实例化,只能够处理一种类型
Stack<int> st;
st.Push(1);
st.Push(2);
// 如果想要运行double类型,则需要在stack.cpp中添加double类型的显示实例化
// Stack<double> stt;
// stt.Push(1.1);
// stt.Push(2.2);
return 0;
}
思考:
如果不添加显示实例化为什么程序会报错?
// 因此,对于上述情况,我们提出了两种解决方案
/*
1.显示实例化
template
class Stack<int>
2.不分离(不将声明和定义分别放到.h和.cpp文件中)
我们可以将声明和定义放到.hpp文件中
具体如下:
*/
#include<iostream>
using namespace std;
template<typename T>
class Stack
{
public:
Stack(int capacity = 4);
~Stack();
void Push(const T& x);
private:
T* _a;
int _top;
int _capacity;
};
template<class T>
Stack<T>::Stack(int capacity = 4)
{
cout << "Stack(int capacity = )" << capacity << endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
template<class T>
Stack<T>::~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
template<class T>
void Stack<T>::Push(const T& x)
{
// ....
// 扩容
_a[_top++] = x;
}
// 思考:为什么不直接将声明和定义都放到类里面
// 如果们开放,更方便我们查看声明,了解类的功能