🎸在进入正题之前 先来看一个例子!
实现一个可以支持各种类型的交换函数swap()
首先我们就会想到 函数重载 同名函数参数的类型不同就可以实现重载
如下:
void swap(int& a,int& b)//整数类型交换
{
int tmp=a;
a=b;
b=tmp;
}
void swap(double& a,double& b)//浮点型交换
{
double tmp=a;
a=b;
b=tmp;
}
//还有很多类型.....
//用模板实现就是下面这样 不需要写很多个重复的只是参数不同的代码
template <class T>
void swap(T& a,T& b)
{
T tmp=a;
a=b;
b=tmp;
}
//调用的时候就直接传参数就行 例如 编译器会自动识别参数类型生成不同的swap函数 不用我们自己来写
int a=10,b=20;
double c=1.2,d=2.2;
swap(a,b);
swap(c,d);
这仅仅是两种类型的交换函数 就要写两个 那么如果有很多种类型的函数就需要自己一个个写 那岂不是很麻烦 ?
模板就是来解决这种问题的 对于编译器而言模板就是一个模子,对于不同的类型 在编译阶段编译器自动识别实参的类型确定模板参数的类型 模板参数确定后就可生成对应的代码,就不需要我们自己写了,解放程序员的双手!
模板的种类:模板包括函数模板和类模板(上图是函数模板)
泛型编程就是针对复用性高但是适用的类型不同的代码 使其可以忽略类型的限制 只需要一份代码就可以适用多种类型的数据 减少了代码量。
而上面的模板就是泛型编程的基础,没有模板就实现不了泛型编程,两者具有紧密的联系。
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定
类型版本。
//格式
template<typename T,typename T1,typename T2,.....>
返回值类型 函数名(参数列表){}
模板函数定义和声明的时候需要在函数的上方声明一个模板(注意函数模板不能声明和定义分离,否则会出现链接错误,后边会详细介绍)
隐式实例化就是不用我们自己指定模板参数的类型,让编译器自己根根据实参类型推演对弈的代码。
例如:
template<class T>
T add(T& a,T& b)
{
return a+b;
}
int main()
{
int a=1,b=2;
double c=1.1,d=2.2;
cout<<add(a,b)<<endl;
cout<<add(c,d)<<endl;
//对于 add(a,c);这样的代码就不行了 因为a c的类型不同 而函数模板的参数又只有一个
//编译器就分不清到底要以a c俩个类型中的哪个类型来当模板参数生成对应的代码 就会编译
//失败 解决方法就是
//1.将其中一个类型手动强制类型转化为另一个类型使得两个参数的类型统一
//2.要么就是将模板参数设置为两个不同的类型 让编译器去推演
//3.显式实例化 让编译器尝试去隐式类型转换
}
当函数实参的参数和模板参数无关时,编译器就不知道模板参数是什么类型,无法自动推演代码,这时就需要我们手动显式实例化来指定函数模板的模板参数了 使得编译器能生成对应的代码
例如:
//错误的代码
template<class T>//函数模板不一定都是由编译器自动推导出来的 有时候也需要自己显式的实例化 指定模板参数
T* creatarr(int n)
{
return new T[n];
}
int main()
{
cteatarr(10);//给的实参跟模板参数T无关 编译器无法确定模板参数的类型 也就无法自动推演出对应的代码了
return 0;
}
//正确的代码
template<class T>
T* creatarr(int n)
{
return new T[n];
}
int main()
{
cteatarr<int>(10);
//显式的指定模板参数的类型为int 那么编译器就可以确定模板参数类型就可以生成对应的代码
creatarr<double>(10);
return 0;
}
函数模板的大部分的使用场景都是不需要我们显式实例化的,但是像上面这种情况就需要我们显式实例化指定模板参数类型!
例如:
template<class T>
T add(T& a,T& b)
{
return a+b;
}
int main()
{
int a=1,b=2;
double c=1.1,d=2.2;
add<int>(a,c);//显式实例化
return 0;
}
上面的代码如果不显式实例化就会编译失败,显式实例化之后就会生成int类型的add函数 但是实参的类型不统一 一个为int 一个为double那么编译器就会对其进行隐式类型转换处理 将double类型的实参转换为int(取浮点型的整数部分) 类型 然后传给形参,实现参数的类型统一。
1.同名的普通函数可以和函数模板同时存在,并且该函数模板还可以被实例化为那个普通函数
例:
template<class T>
T sub(T& a,T& b)
{
return a-b;
}
int sub(int& a,int& b)
{
return a-b;
}
int main()
{
int a=10,b=2;
sub(a,b);//调用普通函数
sub<int>(a,b);//调用函数模板实例化出来的函数
return 0;
}
sub(a,b)是显式实例化了sub这个函数模板,那么就告诉编译器我就是要调用实例化处来的,不用现成的可以匹配的。就会去实例化函数模板,生成对应的函数,并且调用。
2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
例:
template<class T,class K>
T sub(T& a,K& b)
{
return a-b;
}
int sub(int& a,int& b)
{
return a-b;
}
int main()
{
int a=10,b=20;
double c=2.3;
sub(a,c);//会调用函数模板实例化出来的函数
return 0;
}
对于上述代码 存在一个整型的sub函数 和一个含有俩个模板参数的sub函数模板 调用sub(a,c)的时候因为c是double类型 所以现成的sub函数不够匹配 那么编译器就会通过该函数模板推演出一个第一个形参为int,第二个形参为double的sub函数 并调用
类中的成员变量的类型不同可以实现不同类型的数据的存储和操作,例如一个栈的类,当成员变量是char类型的时候可以对字符类型的数据进行存储和相关操作,当成员变量的参数类型是int的时候可以实现对int类型的数据的存储和相关操作。
函数模板可以隐式实例化,也可以显式实例化,但是类模板不同,类模板必须要显式实例化,指定模板参数。
template<class T>
class 类名
{
//返回值或参数类型的函数 T func(T& s){} .....
//T类型的成员变量 T k;
}
template<class T>//类模板
class vector
{
public:
vector(int n=10)
{
_size = 0;
_capacity = n;
_a = new T[_capacity];
}
void push_back(const T& x)
{
if (int ret = _size == _capacity ? 1 : 0)
{
T* _b = new T(_size * 2);
memcpy(_b, _a, _size * sizeof(T));
_a = _b;
_capacity *= 2;
}
_a[_size++] = x;
}
void pop_back()
{
if (_size > 0)
--_size;
else
{
cout << "null" << endl;
}
}
void print()
{
for (int i = 0; i < _size; i++)
{
cout << _a[i] << endl;
}
}
int :getsize()
{
return _size;
}
int getcapacity()
{
return _capacity;
}
private:
int _size;
int _capacity;
T* _a;
};
以上述vector类的实例化为例 非常简单:
类名<模板参数> 对象名(参数列表);
vector<int> v(10);
类模板还有个需要注意的点就是声明和定义不可以分离,即不可以一个放到.h 文件中,一个放到.cpp文件中,否则会出现链接错误!
为什么呢?因为调用函数的时候到符号表里找不到地址,这里我们要回顾一下程序编译链接的过程了
在预处理阶段,头文件会在template.cpp 和 main.cpp中展开 ,生成.i文件 ,然后再由.i文件进行编译生成汇编代码,这其中就要将函数的地址放到符号表里去,但是在template.cpp中虽然有定义,压根就不知道模板参数的类型是什么,编译器无法生成对应的代码,那么符号表里自然就没有对应函数的地址了,main.cpp中又调用了对应的函数,就会有call _Zxxx(ii)(?)…之类的汇编代码,其中?代表的是调用函的地址待定,运行时到符号表里找;在两个.o文件链接的时候,函数调用到符号表里找对应函数的地址就找不到,就会链接失败!
在定义的时候显式示例化就可以生成对应的函数代码,符号表里就会有对应函数的地址,调用的时候就可以找到,完成函数调用。
格式:
template
返回值类型 函数名<模板参数类型>(参数列表);
例:
template
void swap<int>(T& a,T& b);
格式:
template
class 类名<模板参数类型>;
例:
template
class vector<int>;
声明和定义放到一个.h或者.hpp 中那么在头文件在main.cpp中展开的时候就会有也具有声明和定义 那么在函数调用的时候编译器就会直接生成对应的代码,直接调用函数,不再会去符号表里找对应的函数地址了(只有声明的时候调用函数才会去符号表中找地址,有定义直接调,根本就不会将此函数的地址放到符号表!)
格式:
template<class T>
void swap(T& a,T& b){函数实现}
格式:
template<class T>
类名<模板参数>::函数名(参数){函数实现}
以上两种方式都可以使得程序编译通过 运行成功 但是第二种方法较为推荐!