在前面的学习中了解到C++是支持函数重载的,我们可以给同一作用的函数起同一个函数名,只是彼此之间参数的类型不能相同。先简单看一个变量交换的代码
//整形变量的交换
void Swap(int& a, int& b)
{
int c = a;
a = b;
b = c;
}
//浮点类型变量的交换
void Swap(double& a, double& b)
{
double c = a;
a = b;
b = c;
}
这样看起来好像还可以,但是我们知道数据的类型还有多种,像char,short,long…,要是我们给每一种类型都写一个专门的交换函数,那么仅仅是为了交换一个数据,就写那么多代码,显得有点多余了。那么C++在这里有什么优化呢?
既然那些可重载的函数彼此之间只是变量的类型不同,其他的操作都相同,那就可以给他定义一个模板,然后每次只需要类似“typedef”的操作,给函数的参数类型换一个名字,在模板中用这个名字来代替这个类型的操作,就显得方便很多。
template<typename T1,typename T2....,typename Tn>
//替换后的交换函数
void Swap(T& a, T& b)
{
T c = a;
a = b;
b = c;
}
typename是用来定义模板的关键字,也可以用class来表示自定义的类,但是不能用struct来代替。
为什么要有模板呢?
模板那么好用,那么关于模板的底层是怎么实现的呢?
可以把模板想象成一个模子,对于这个模子的模具,每个模具都有自己的名字,T1,T2.,…,Tn。在每次调用的时候,他只需要先获取模具对应的类型,然后依据对应的类型去推演真正操作的函数代码。
个人感觉这里的模具在获取对应类型的时候,就相当于我们的typedef,然后给这个类型起一个别名,然后使用这个函数。。。
当我们使用不同类型的参数使用模板的时候,这个过程就是模板的实例化。
模板参数的实例化可以分为隐式实例化和显示实例化。
在使用隐式实例化的时候,如果变量的类型是相同的,编译器是可以通过,那要是不同的呢?
我们发现编译器会报错,显示模板的第二个参数的类型不明确。既然这样,我们就来两个模具看看效果
这样写的话好像显得代码比较乱,还会多出一个警告,浮点数类型转为整形类型会出现数据的丢失,为了解决这个警告,可以给他来一个强制类型转换。
发现这里好像又出错了。。。报错是两个参数是(int , int),不能转为(int&,int&)。这是怎么回事,问题只能出在 (int)d,这里了。
为了排查错误,首先我先给模板的参数去掉引用类型,发现这个时候编译器的报错都取消了,那么问题一定是引用了。
如果在使用引用类型的时候,如果引用前后类型不匹配,就需要进行强制类型转换的问题。因为强制类型转换的过程会在内存中新开辟一段空间,来储存转换后的数据,这时这个数据是一个临时变量,他只能在转换的过程生效,所以说它具有常性,也就是只读性。而我们调用引用类型后,引用的类型具有可读可写的特性,就相当于把权限放大了,我们知道,权限是只能缩小,不能放大的。这就出现了权限的错误,解决这个问题可以给第二个参数加一个const 类型,表面他是只读的,但是这样就不能满足交换函数的本意了。
也可以这样理解,引用相当于给一个变量起了一个别名,再来看看我们传的这个变量是一个double类型的,本名是d,但是我们强制类型转换后的那个数据又是什么名字呢?他没有名字。。。
解决这个问题,可以在调用swap函数之前,就给double类型的变量强制转换后的临时变量一个名字,让他变成可读可写的,然后就可以调用了。好恶心
通过调用C++中的swap交换函数,我们发现其实C++也没有解决不同类型交换的问题,这个锅我不背
显示实例化就是在函数名后的<>中指定模板参数的实际数据类型,如果指定类型和参数类型不匹配,编译器会常识隐式类型转换,如果不能转换成功编译器就会报错。
嘿嘿,要是我们在定义了一个模板的情况下,还自己定义了一个重载的函数,那么编译器会执行哪一个呢?
我们发现编译器并没有用模板推演出函数来执行,只是调用我们写的重载函数。
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
类模板实力话相比普通函数来说,他是在类模板名字的后面加<>,然后把实例化的类型放在<>中,类模板的名字并不是真正的类,他实例化的结果才是真正的类。
// stack类名,stack才是类型
stack<int> s1;
stack<double> s2;
STL中有空间配置器,容器,配接器,仿函数,算法,以及迭代器几种组件
模板参数分为类型形参和非类型形参
没有模板,我们可以这样写一个模板类型的静态数组,但是数组的大小N,却只能是一个固定的数字,不能改变
const int N = 10;
template<class T>
class arr
{
public:
arr()
:_size(0)
{}
private:
T _arr[N];
size_t _size;
};
int main()
{
arr<int> a;//创造一个10个大小数组
}
利用模板的参数,我们可以这样来
template<class T, size_t N = 10>
class arr
{
public:
arr()
:_size(0)
{}
private:
T _arr[N];
size_t _size;
};
int main()
{
arr<int,10> a;//创造一个10个大小数组
arr<int, 100> b;//一个100大小的数组
}
注意:
特化就是针对模板在使用过程中的特殊情况,给这个案例一个特别处理。
对于这样的一个比较两个数大小的模板
template<class T1,class T2>
class cmp
{
public:
cmp(T1 a,T2 b)
:_a(a)
, _b(b)
{}
//比较两个参数的大小
void Compare()
{
if (_a < _b)
cout << "_a < _b" << endl;
else if (_a == _b)
cout << "_a == _b" << endl;
else if (_a > _b)
cout << "_a > _b" << endl;
}
private:
T1 _a;
T2 _b;
};
全特化就是在特化的时候,把模板的参数全部确定化
显然,对于前面的比较函数,当两个变量的类型是字符串char*
的时候,直接比较大小就会出错的,我们就需要全特化一下
template<>
class cmp < char*, char* >
{
public:
//涉及到指针,得要深拷贝
cmp(char* a, char* b)
{
char* ta = new char[strlen(a) + 1];
char* tb = new char[strlen(b) + 1];
strcpy(ta, a);
strcpy(tb, b);
_a = ta;
_b = tb;
}
~cmp()
{
delete[] _b;
delete[] _a;
_a = _b = nullptr;
}
//比较两个参数的大小
void Compare()
{
int ans = strcmp(_a, _b);
if (ans < 0)
cout << "_a < _b" << endl;
else if (ans == 0)
cout << "_a == _b" << endl;
else if (ans > 0)
cout << "_a > _b" << endl;
}
private:
char* _a;
char* _b;
};
类似于这样的,用strcmp
来对字符串类型进行比较,然后返回结果
任何针对模板参数进一步进行条件限制的特化版本。也就是值限制一部分
将模板参数类表中的一部分参数进行特化
template<class T1>
class cmp<T1,int>
{
public:
cmp(T1 a, int b)
:_a(a)
, _b(b)
{}
//比较两个参数的大小
void Compare()
{
if (_a < _b)
cout << "_a < _b" << endl;
else if (_a == _b)
cout << "_a == _b" << endl;
else if (_a > _b)
cout << "_a > _b" << endl;
}
private:
T1 _a;
int _b;
};
针对模板参数进行更进一步的条件限制,从而设计出来的模板
指针类型的特化模板。这里没有写
template<class T1,class T2>
class cmp <T1*, T2*>
{
public:
//涉及到指针,得要深拷贝
cmp(T1* a, T2* b)
{
_a = a;
_b = b;
}
private:
T1* _a;
T1* _b;
};
引用类型的特化模板
template<class T1, class T2>
class cmp < T1&, T2& >
{
public:
//涉及到指针,得要深拷贝
cmp(T1& a, T2& b)
:_a(a)
, _b(b)
{}
private:
T1& _a;
T1& _b;
};
对于一个模板,我们把模板的声明放在.hpp文件中,把模板的定义放在.cpp文件中,最后在主函数中调用模板的功能函数,这样写可以吗?
//aa.hpp
#include
#include
using std::cin;
using std::cout;
using std::endl;
template<class T>
T Add(const T& a, const T& b);
//aa.cpp
template<class T>
T Add(const T& a, const T& b)
{
return a + b;
}
//text.cpp
#include "aa.hpp"
int main()
{
cout << Add(1, 2) << endl;
return 0;
}
在编译的时候会出现这样的错误
程序是在一个.obj
文件中出错了,回想一下编译器编译程序的几个过程
预处理(进行宏替换)阶段
预处理功能主要包括宏定义,文件包含,条件编译,去注释等。就是将以#开始的头文件
展开,define和typedef 宏定义
的名称进行替换,去掉注释,生成一个.i
文件
编译(生成汇编)
在这个阶段中,编译器首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,编译器把代码翻译成汇编语言。生成一个.s
文件
汇编(生成机器可识别代码)
汇编阶段是把编译阶段生成的.s
文件转成目标文件。生成.obj
文件
链接(生成可执行文件或库文件)
在成功编译之后,就进入了链接阶段,生成可以执行的.exe
文件
aa.obj
文件中,编译器没有看到对Add
模板函数实例化的过程,因此就不会生成具体的调用的函数。因为模板的作用只是在调用的时候才会根据类型推演具体的函数。text.obj
中,编译器发现了函数调用的过程,就使用call
指令去找调用的函数,但是因为在aa.obj
中并没有生成这个实例化后的函数,因此链接的时候会报错具体流程类似于这样
为了解决这一问题,我们需要把模板的声明和定义放在同一个XXX.hpp
文件下。因为预处理的过程会进行头文件展开工作,然后在汇编中主函数调用哪个函数,就转换哪个函数。