在C++入门初阶时我们提到,为了让Swap函数既可以交换double类型数据,也可以交换int类型数据,C++实现了函数重载。但是我们重复的实现相同的逻辑依然显得较为臃肿,如何做才能只实现一次大致逻辑从而减少代码的拷贝呢?我们由此情景引入并介绍C++中的初阶模版。
1.泛型编程
定义:编写与类型无关的通用代码,是代码复用的一种手段
模版是泛型编程的基础。
2.模版初阶
template<Typename T>
注意:T只能代表一种类型的参数,T的名字可以根据需要更换,但是我们一般习惯使用Type的 首字母缩写T。
typename需要几个定义几个,typename处也可以用class(class和typename可以互换)代替((切记:不能使用struct代替class)
例如之前的Swap:
template<typename T>
void Swap(T& x, T& y) {
T tmp = x;
x = y;
y = tmp;
}
报错的原因是,T只会被当作一种参数,T不能在作为int的同时作为double。
template<typename T1,typename T2>
void Swap(T1& x, T2& y) {
T1 tmp = x;
x = y;
y = (T1)tmp;
}
这样调整出两个参数就不会报错了,但是自然会产生精度的损失。
回到继续使用一个T的时候:
我们通过调试观察两次调用:
看起来去的是同一个函数(泛型的Swap),其实是简化给操作者看的。
但是其本质不是同一个函数,通过汇编我们会发现两次调用call的函数位置不一样。
两次call泛型的Swap的地址不一样。
模版原理:
编译器根据你的需要,用模版生成你需要的函数,也就是函数模版的实例化。
实质上会降低效率,因为编译器会根据你的数据类型进行推演,写出一个新函数。但是编译器自动生成也一定是比程序员手动实现更快的。
一个int 一个double时,推演实例化时会出现问题。
因为编译器不知道T该是什么类型。我们有以下三种处理方法:
第一种解决方案:强转 但是会丢失精度
第二种解决方案:显式实例化(推荐使用)(显式实例化:在函数名后的<>中指定模板参数的实际类型)
第一次显示的指令T是int 第二次显式的指令T是double
第三种解决方案:改变返回值类型
简单返回T不可行。因为不知道返回值T的类型,无法返回
不过我们可以使用auto作为返回值类型。
template<typename T1,typename T2>
auto Add(const T1& x,const T2& y) {
return x + y;
}
易错:
1.
此时编译器推演不出来T是什么类型,没有信息能让编译器推理。
所以此时必须使用显示实例化。
2.
模版参数列表每一次使用之前都得再声明。
或者我们声明类模版时,想在外部实现一个该函数,尽管我们在class Vector开始处就使用了参数列表T,再次实现该函数时还是得再写一次模版参数列表。模版不建议声明和定义分离到.h 和.cpp会出现链接错误,具体原因后面会讲
template <class T>
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}
3.
template<typename T>
void Swap(T& x, T& y) {
T tmp = x;
x = y;
y = tmp;
}
int a=1;double b= 1.1;
Swap<int>(a,b);
如上代码,会报错吗?
当然会报错。
显式实例化的int让T不再需要推理的过程,直接全部变成int,但是将b隐式类型转换过程中就涉及到生成临时变量,临时变量又具有常性,所以将b的int形式的拷贝赋值给T& y时,又造成了权限的放大,需要使用const修饰两个变量x y,才不会报错。
template<typename T>
void Swap(const T& x,const T& y) {
T tmp = x;
x = y;
y = tmp;
}
int a=1;double b= 1.1;
Swap<int>(a,b);
关于匹配的先后问题:
普通函数和函数模版可以同时存在,编译器会优先去调用更匹配的。
有专门的int类(现成的普通函数)存在,则会优先走普通函数,不会去走模版(半成品)
但是如果只有一个现成的普通函数,也行(能隐式类型转换就行)
4、有显示实例化时使用编译器生成的版本。
也就是说:
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函 数
// 专门处理int的加法函数
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版本
}
5、优先使用"成品"
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板函数
3.类模板
(此处的class也可以用typename):
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
在C语言中,我们用typedef的方法决定向一个数据结构中放置哪种类型的数据:
现在,我们可以通过类模版实现一个什么类型的数据都可以装的顺序表Vector。
回顾上文,注意:类模板中函数放在类外进行定义时,需要加模板参数列表
但是模版不建议声明和定义分离到.h 和.cpp,会出现链接错误,具体原因后面会讲
将之前的DateType都换成T即可。
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (T*)malloc(sizeof(T) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
易错:
1.类模板是一个类家族,模板类是通过类模板实例化的具体类。不要混淆模版类和类模版的概念。
2.模板类是一个家族,编译器的处理会分别进行两次编译,其处理过程跟普通类不一样。
同理,函数模版也是一个家族。
3.模板运行时不检查数据类型,也不保证类型安全,但是其具有较强的可移植性。