模板是泛型编程的基础,先给出泛型编程的概念。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。
应用场景:比如要实现一个通用的,进行两个变量互相交换的函数,此时可以通过函数重载的方式,用不同的参数类型来实现该函数。但是是有缺陷的,因此也引出了模板编程。
1. 函数重载只是参数类型不同,当出现新类型的参数时,就要手动的再实现一份。
2. 代码的可维护性较低,一个出错可能导致所有的同名函数都出现问题。
模板是一个模具,通过用户传入的参数类型来生成对应类型的具体代码。本质是将重复的工作交给了编译器完成。
模板分为函数模板和类模板。
函数模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
template<typename T1, typename T2, ...., typename Tn>(此处的typename也可写作class)
返回值类型 函数名(参数列表){}
函数模板是一个蓝图,它本身并不是函数,是编译器用使用特定方式产生具体类型函数的模具。
具体例子:
template<class T>
void Swap(T& num1, T& num2)
{
std::swap(num1, num2);
}
template<typename T1, typename T2>
void PrintType(T1& val1, T2& val2)
{
std::cout << typeid(val1).name() << std::endl;
std::cout << typeid(val2).name() << std::endl;
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
std::cout << a << " " << b << std::endl;//20 10
double c = 1.0;
PrintType(a, c);//int, double
return 0;
}
函数模板原理
编译器编译阶段,编译器根据传入的实参类型来推演生成对应类型的函数以供调用。例如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码。
函数模板实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。
函数模板的实例化又分为隐式实例化和显式实例化
隐式实例化
让编译器根据传入的参数类型自动进行参数类型的推演。
显式实例化
在函数名后的< >中指定模板参数的实际类型
例如vector<int> array;
模板参数匹配规则
(1)一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
(2)对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
(3)模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
类模板
类模板定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
//例子
template<class T>
class Base
{
public:
private:
T _num;
};
类模板实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的 类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
非类型模板参数
模板参数分为类型形参与非类型形参。 类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
具体例子:
template<class T, size_t N = 10>
class Base
{
private:
T _array[N];
};
注意: 1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。
模板特化
通常情况下,模板的使用可以实现与类型无关的代码,但有些特殊类型会得出错误的结果,此时需要对特殊类型进行特殊处理,称为模板特化。
函数模板的特化
具体例子:
class Base
{
public:
int _left;
int _right;
};
template<class T>
bool IsSame(T left, T right)
{
return left == right;
}
template<>
bool IsSame<Base*>(Base* left, Base* right)//模板特化,对Base* 类型进行特殊处理
{
return left->_left == right->_left && left->_right == right->_right;
}
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板。
2. 关键字template后面接一对空的尖括号<>。
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
类模板特化
类模板特化又分为全特化和偏特化
全特化就是将模板中的参数全都确定化。
template<class T1, class T2>
class Base
{
public:
T1 _left;
T2 _right;
};
template<>
class Base<char, int>
{
public:
char _left;
int _right;
};
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有两种表现形式,部分特化和进一步对参数进行限制
具体例子:
template<class T1, class T2>
class Base
{
public:
T1 _left;
T2 _right;
};
//部分特化
template<class T1>
class Base<T1, int>
{
public:
T1 _left;
int _right;
};
//对参数进行进一步限制
template<typename T1, typename T2>
class Base <T1*, T2*>//指针类型
{
public:
T1 _d1;
T2 _d2;
};
模板分离编译
分离编译:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
模板是不支持声明和定义分离的。
原因:
C/C++程序要运行,是要经过 预编译->编译->汇编->链接 四个步骤的,其中预编译主要是进行文本处理,即宏替换,头文件的包含,以及注释的删除等;编译则是会对代码根据语言特性进行词法、语法、语义的分析,并形成汇编代码,同时对符号进行整合;汇编则是在此基础上,将汇编代码进一步翻译成二进制,同时形成符号表,链接时会将对应符号的地址填到符号表中,处理没有解决的地址问题。
而因为模板的声明和定义分离之后,编译器在编译时,会默认该模板函数的地址已经确定,所以只会在最后链接阶段,寻找相应模板函数的地址进行填表,但由于模板没有实例化,所以地址并不存在,所以链接阶段就会因为找不到对应的函数地址而报错。
解决方法:
建议将模板的声明和定义统一放到一个文件中,例如.hpp文件。