模板
泛型编程
特点:
- 通用
- 灵活
函数模板
模板函数不是一个单独的函数,时编译器生成代码的规则
格式:
//例子说明:
template<class T>//template关键字用来创建模板,其中T不是类名,是代表是一种类型或数据,也可以使用typename代替class
T Add(T left, T right)//生成函数的规则,运行的时候确认T之后就会替换所有的T
{
return left + right;
}
int main()
{
Add(1,2);//运行此代码时,T就为int
Add('a', 'b');//运行此代码时,T就为char
Add(1.0, 2.0);//运行此代码时,T就为double
Add(1,2.0);//报错,因为1为int型,而2.0为double类型(课件模板对类型要求的严格,有第一个参数确定式int,但是第二个参数却是double型)
//以上都是隐式的实例化,详情看下面的解释
//下面式显式实例化,详看下面的解释
Add(1,(int)2.0);//T为int
Add<int>(1,2.0);//T为int并且会对第二个参数2.0进行隐式类型转换
}
函数模板的原理
- 模板是一个蓝图,它本身不是一个类或者函数,编译器用模板产生指定的类或者函数,产生模板的模板特定类型的过程成为函数模板实例化
class和typename的区别:
- class是一直都有的,但是typename 是最后添加进C++的,所以使用typename的时候,在一些旧的编译器上面可能不能使用
隐式实例化:
- 在编译期间,编译器根据传入的第一个实参的类型确定模板参数的类型
- 隐式实例化的过程:
- 编译器首先会看调用的方式中的第一个参数,来确定T的真实类型
- 一旦T以确定,所有的T都会进行替换,然后生成具体类型的函数,然后进行调用
- 隐式的实例化不会进行隐式类型转换
显式实例化:
- 手动在函数模板后面加上
<类型>
实现显式的实例化 - 模板函数会在编译的时候生成此类型的代码(也就是直接之指定类型),并且对类型不同的参数会进行隐式的类型转换
- 显式的实例化就相当于自己写了一个此类型的函数,再进行调用一样
typeid([类型]).name()
- typeid是一个类,接收一个类型,其name方法是将传入的类型以字符串的方式返回
模板函数的调用
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,再调用的时候会优先调用非模板函数,而不会从函数模板产生出一个实例。如果函数模板可以产生一个具有更好匹配的函数,那么将会选择模板。
- 调用的时候,显式的指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,而且所有的模板参数都应该根据实参演绎出来
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
类模板
将一个类里面的类型或者数据作为模板,也是在编译的时候编译器根据具体的形参类型生成具体类型的类的代码
典型实例:
stl标准模板库
- 所有的组件都是使用模板实现
特点:
- 通用性强,使用时没有类型的局限性
类模板重载输出输入运算符
类模板在重载输出输入运算符的时候,不能在类模板中声明类模板外实现,只能在类模板中进行声明和实现
原因:
- 如果在模板外实现的话就需要将重载函数设为模板函数,不然不知道类的具体类型
- 如果将重载函数设置为模板函数的话那么就相当于一个新的模板了,编译器就不会认为跟类模板是同一个模板,所以在类外实现跟没有实现一样
- 但是直接在类模板中定义就会直接使用类模板的模板T所以在自然跟类是同一个模板
class和struct的区别
- 默认的访问权限不同
- 默认的继承权限不同
- 模板中只能使用typename或者class,不能使用struct
模板的缺点
- 增长了代码编译的时间(因为编译的过程中需要编译器通过判断类型生成代码)
- 模板一旦出错,不好检查,因为比那一起报的错误往往不准确
模板的编译过程
- 对模 数进行简单的语法检测
- 根据实例化生成代码
typename关键字
typedef typename DF::PDF PDF;//使用typename关键字的目的是告诉编译器PDF是一个DF类中的一个内置类型,并且只能使用typename来标识
非类型模板参数
以上的所有的模板参数都是为了让编译器在编译的时候确定类型,但是,模板不光可以指定类型,也可以指定一个值
template<class T, size_t N=10>
class Array
{
private:
T _arr[N];
}
此处的N在类里面就相当于一个常量
模板的特化
对于有的类型对象,使用模板实例化出来的方法可能会出bug(例如拷贝函数模板拷贝string类型的时候),所以会使用模板的特化,对种具体的类型进行操作。
template<class T1>
class Test
{
public:
Test();
private:
T1 _t1;
}//这时一个普通的模板类,调用的方式举例:Test<int>
Template<>
class Test<int>
{
public:
Test();
private:
int _t1;
}//这时调用类型如果是:Test<int>,它调用的将会是下面这个类,并不会调用第一个实话出来的类,所以说应该是特化出来的类优先被调用
全特化:
- 以上就是一种全特化,及所有的模板参数都被特化
偏特化(局部特化):
模板参数有两个或两个以上,但是并不是特化所有的模板参数
template<class T1, class T2> class TestPart { public: TestPart(); private: T1 _t1; T2 _t2; }//这是一个普通的多模板参数的模板 template<class T1> class TestPart<T1, int> { public: TestPart(); private: T1 _t1; int _t2; }//这是一个局部特化的模板,如果调用的方式为TestPart<string,int>的时候,会优先调用这个版本,因为我特化的就是第二个参数为int型
局部特化也可以是将传入的参数类型在类中特化为更具体的类型
template<class T1, class T2> class TestPtr<T1*, T2*> { public: TestPtr(); private: T1 _t1; T2 _t2; T1* _pt1; T2* _pt2; }//只要使用:TestPtr<int*, char*>这样的调用方式,都会使用此模板
不光是具体为指针,还可以具体为引用
类型萃取—特化的应用
如上所说,某些类型的对象使用模板实例化出来的方法是会出bug的,所以我们在使用的时候,会将这些类型提取出来,在方法的实现中进行分类实现,如果是会出bug类型的对象使用一种方法,不会出bug类型的对象使用另一种方法
//实现一个拷贝的模板
//首先进行类型的萃取
template<class T>
class TestPart
{
public:
static bool GetType()
{
return false;//GetType方法返回的值为是否是内置类型
}
};//这个模板是任何类型都会接收,并将进来的类型都认为不是内置类型,因为内置类型会进入一下的特化之后的类
template<>
class TestPart<int>//如果参数为int,那就进入此类
{
public:
static bool GetType()
{
return true;//内置类型返回真
}
};
template<>
class TestPart<char>
{
public:
static bool GetType()
{
return true;
}
};
template<class T>
void Copy(T* des, T* src, int size)
{
if (TestPart<T>::GetType())//首先判断是否是内置类型
{
memcpy(des, src, size);//如果是内置类型,就使用memcpy,效率较高
}
else
{
for (int i = 0; i < size; ++i)
{
des[i] = src[i];//如果不是内置类型就使用=运算符,因为一般非内置类型会将=重载
}
}
}
模板的分离编译
模板是不支持普通的分离编译,也就是说并能将模板的声明和定义分开来存放
原因:
- 首先一个原程序是需要进行先编译,如果模板的声明和实现和调用放在一起,那么在编译的时候编译器会自动生成具体类型的代码,这样就可以使用了,但是如果是份文件使用,那么编译的时候只会编译本文件,但是只通过一个文件不能将具体的代码生成出来,所以,各个文件编译过后的符号表是不同的,所以会出现链接的错误。
解决办法:
- 使用.hpp文件替换.h头文件
- .hpp是专门为模板的头文件,当然也可以当作普通的.h来使用