1.非类型的模板参数
模板的参数分为类型参数与类型参数。
类型参数:出现在模板参数列表中,跟在class或者typedef后面的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
//定义一个模板类型的静态数组
template<class T,size_t N=10>
class PRINT
{
public:
void print()//打印该数组的元素
{
int sum=0;
for(size_t i=0;i<N;i++)
{
cout<<arry[i]<<endl;
}
}
private:
T arry[N];
};
如上就是非类型模板参数使用的的一个例子。
但对于非类型模板参数应该注意的是:
1.浮点数、类对象以及字符串是不允许作为非类型模板参数的
2.非类型的模板参数必须在编译期就能确认结果。和数组长度的定义一样
template<double VAT>
double process(double v) //error:浮点数不能作为非类型模板参数
{
return V * VAT;
}
template<string name> //error:类对象不能作为非类型模板参数
class MyClass {
...
};
由于字符串文字是内部链接对象(因为两个具有相同名称但出于不同模块的字符串,是两个不同的对象),所以不能使用它们作为模板实参。
2.模板的特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果,比如:
template<class T>
bool ISequal(T& left,T&right)
//判断传入的两个参数的是否相同。
{
return left==right;
}
对于整数类型,浮点类型等参数,上面的模板可以得到正确的答案,但对于字符串明显就是错误的。此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
2.1函数模板的特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<>
bool IsEqual<char*>(char*& left, char*& right)
{
if(strcmp(left, right) > 0)
return true;
return false;
}
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
bool IsEqual(char*& left, char*& right)
{
if(strcmp(left, right) > 0)
return true;
return false;
}
以上的两种特化方法都是可行的。
2.2.类模板的特化
2.2.1全特化
全特化即是将模板参数列表中所有的参数都确定化。
template<class T1,class T2>
class Data
{
public:
Data()
{cout<<"Data<T1,T2>"<<endl;}
private:
T1 _d1;
T2 _d2;
};
template<class T1,class T2>
class Data<int,char>
{
public:
Data()
{cout<<"Data<T1,T2>"<<endl;}
private:
T1 _d1;
T2 _d2;
};
2.2.2偏特化
偏特化:任何针对模板参数进一步进行条件限制设计的特化版本。比如对以下模板类;
template<class T1,class T2>
class Data
{
public:
Data()
{cout<<"Data<T1,T2>"<<endl;}
private:
T1 _d1;
T2 _d2;
};
偏特化有以下两种表现方式:
1.部分特化
将模板参数列表中的一部分参数特化。
template<class T1>
class Data<T1,int>
{
public:
Data()
{cout<<"Data<T1,T2>"<<endl;}
private:
T1 _d1;
T2 _d2;
};
2.参数更进一步限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型
template<typename T1,typename T2>
class Data <T1* ,T2*>
{
public:
Data(){cout<<"Data<T1*,T2*>"<<endl;}
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为引用类型
template<typename T1,typename T2>
class Data <T1& ,T2&>
{
public:
Data(){cout<<"Data<T1&,T2&>"<<endl;}
private:
T1 & _d1;
T2 & _d2;
};
void test()
{
Data<double,int >d1; //调用特化的int版本
Date<int,double>d2; //调用基础的模板
Data<int *,int *>d3; //调用特化的指针版本
Data<int &,int&>d4(1,2); //调用特化的引用版本
}
3.模板分离编译
3.1什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
3.2模板的分离编译
假如有以下场景,模板的声明与定义分离,在头文件中进行声明,源文件中完成定义:
分离编译
可以理解为将一份代码分解为多个部分,如.h,.c等部分
一个程序由若干份源文件组成,这些源文件编译成为目标文件,最后所有的目标文件链接起来构成可执行文件,这个过程就叫做分离编译。
对模板的分离编译
程序会报错,因为在编译的四个过程中:预编译,编译,汇编,链接,作用如下
1.预编译:展开头文件,宏替换,去掉注释,条件编译(ifndef等语句的判断)
2.编译:将代码转换成汇编代码,并且做了两件很重要的事。
编译器在每个文件中保存一份函数地址符表,保存当前文件里的每个函数的地址
生成一条条的汇编代码,其中调用函数的代码会生成call指令,call 指令后面跟着一条jmp指令的汇编代码地址,之后跟着的才是“被调用函数的汇编后第一条指令”的地址
但是这个地址是在链接阶段才填上的。
汇编:将汇编代码转换成机器码
链接:编译器将多个.o文件链接在一起,形成可执行文件.
需要注意的是这个时候编译器会将每个call指令的地址补齐,方式是从当前的函数地址符表,如果没有,继续向其他的文件的函数地址符表找,如果找不到,则链接失败。
//a.h
template<class T>
T Add(const T& left,const T& right);
//a.cpp
template<class T>
T Add(const T& left,const T& right)
{
return left+right;
}
//main.cpp
#include"a.h"
int main()
{
Add(1,2);
Add(1.0,2.0);
return 0;
}
上面的代码运行会报错:
原因是因为:预编译生成后,就生成了test.i和main.i文件
test.i有函数的声明实现,main.i有函数的调用和声明
因此编译器做了一件事情,fun.i生成fun.s里面保存了函数的地址,在地址符表里,同理main.i生成了main.s里面有call指令,call指令后面的地址暂时空下,等链接时填入,再汇编生成,main.o和fun.o文件
同时我们知道,模板在没有实例化时是没有代码的。
所以编译失败原因是因为,在展开头文件时,fun.o里只有函数的声明实现,没有它的实例化代码,也就没有函数地址,main.o的call指令里的地址无法填入,导致编译失败
3.3解决方法
将声明和定义放到一个文件"xxx.hpp"里面或者"xxx.h"都可以。推荐使用。
将声明和定义放到一个头文件中,在预处理的时候该头文件就会在源文件(main.cpp)中展开,然后对于main.cpp文件进行编译的时候,就可以直接在main.cpp文件中进行实例化了,从而就会产生相应的加法函数的代码。
而不是像预处理的时候,只声明了加法函数模板在其他的源文件(a.cpp)中定义这呢,但是a.cpp文件中没有调用该模板函数的代码也就不会实例化出加法函数,所以在编译的时候main.obj就会产生链接不到加法函数的错误。
模板定义的位置显式实例化。这种方法不实用,不推荐。