目录
1.非类型模板参数
2.函数/类模板的特化
3.模板的分离编译
4.总结:模板的优缺点
1. 代码复用性高
2. 类型安全
3. 性能优化
2. 错误信息难以理解
3. 代码膨胀
易错易忽略的语法点:
1. 模板声明和定义分离问题
2. 模板参数推导问题
1.非类型模板参数
说到非类型模板参数,先让我们来看看一般的模板长什么样子:
template<class T>
void Swap(const T& a, const T& b)
{
T tmp = a;
a = b;
b = tmp;
}//这里是一个函数模板
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
vector()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{}
//...
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};//这里是一个模板类
- 模板参数分类类型形参与非类型形参。
- 类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
- 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
我们来看看库中对非类型模板参数的使用:
这是库中的数组类,设计出来的初衷是为了代替掉C语言中的数组,并把数组封装成一个类,有什么好处呢?数组类可以对越界访问进行断言,但是这一功能vector同样可以做到,不常用。在这里就是由非类型模板参数传参的。
2.函数/类模板的特化
为什么有模板的特化这一个概念?假如我们定义一个函数模板,这个模板虽然可以自动识别传参类型,但如果我想要对某个类型做一下特殊处理呢?这便是类模板进行特化的意义所在。
template<typename T1,typename T2>
void Identification(const T1& a, const T2& b)
{
cout << "tempalte<T1,T2>" << endl;
}
template<>
void Identification<double, int>(const double& a, const int& b)
{
cout << "template<double,int>" << endl;
}
void test6()
{
Identification(1.1, 2.2);
Identification(1.1, 2);
Identification(1, 2);
}
上面的运行结果什么样?
可见,对于指定类型特化double和int,编译器调用的是特殊处理的函数。我们可以针对特殊要求进行特化。
类模板的特化:
类模板分为全特化和偏特化,假定有两个参数A,B,全特化指的是将这两个参数类型全部指定,而偏特化只会针对某一个参数的类型进行限制,另一个还是会让编译器自动匹配。
演示:
template<class T1,class T2>
class Date
{
public:
Date()
{
cout << "Date<T,T>" << endl;
}
private:
T1 a;
T2 b;
};
//特化就是特殊话一个类或者函数,并且不能够独立存在,必须有模板化了的类
//可以在某一种类中进行特殊处理,也可以为了简化实例化而进行特化
//特化还分为全特化和偏特化,前者全部限制,后者是对模板参数更进一步的限制
template<>
class Date<int, int>
{
public:
Date()
{
cout << "Date<int,int>" << endl;
}
private:
int a;
int b;
};
template<class T>
class Date<T, int>
{
public:
Date()
{
cout << "Date<T,int>" << endl;
}
private:
T a;
int b;
};
void test3()
{
Date<int, int> d1;
Date<double, int> d2;
Date<double, double> d3;
}
代码结果:
3.模板的分离编译
什么是模板的分离编译呢?假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// 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;
}
如果我们试图运行这一程序,那么就会报错链接错误。为什么呢?
原因:程序编译要经过一下几个阶段:预处理,编译,汇编,链接。
其中预处理会进行头文件展开,条件编译,注释的去除,宏的替换
编译会将.i文件转为.s文件,并转为汇编代码
汇编会将.s文件转为.o文件,并转为二进制代码
链接会将每个cpp文件合并,并根据符号表合并进行函数的重定位以及段表的合并
这里要说的是,如果将模板的声明与定义进行分离,那么进行前三个阶段编译器因为不知道具体的类型就不会生成对应的函数,那么到了链接的时候就会找不到具体函数的地址去调用,当然就发生了链接错误。
解决办法也很简单:将声明和定义一并放到.h头文件中,或者将.h文件后缀改为.hpp,并将文件属性中的编译器改为C/C++标头,重新生成解决方案就可以了。
补充:如果要实现一个函数,参数类型是容器适配器,要对传入的容器进行遍历输出,该怎么写?
template<typename Container>
//这里class typenme都可以
void print(const Container& c)
{
//“const_iterator”: 类型 从属名称的使用必须以“typename”为前缀
//凡是这里没有实例化的,都要在前面加上typename,这其实是因为要访问的这个类中可能有static成员或者是类型
//编译器不知道要调用类型还是变量,所以就会报错
typename Container::const_iterator cit = c.begin();
while (cit != c.end())
{
cout << *cit << " ";
cit++;
}
cout << endl;
}
在Container前必须加上typename!来说明这是个类型,否则编译器可能会认为Contain中要访问的是const_iterator这个静态变量或函数从而报错!
4.总结:模板的优缺点
优点:
1. 代码复用性高
模板允许编写与类型无关的代码,这意味着可以为不同的数据类型编写通用的算法和数据结构,避免为每种数据类型都编写重复的代码。
2. 类型安全
模板在编译时进行类型检查,确保使用正确的数据类型。编译器会根据实际使用的类型实例化模板,因此可以在编译阶段捕获类型不匹配的错误,而不是在运行时才发现问题。
3. 性能优化
模板可以在编译时进行优化,因为编译器知道具体的类型信息。例如,在使用模板实现的函数中,编译器可以进行内联展开等优化操作,减少函数调用的开销。
缺点:
1. 编译时间长
模板的实例化发生在编译时,当模板被多次实例化或者模板代码非常复杂时,会显著增加编译时间。因为编译器需要为每个不同的模板参数组合生成相应的代码。
2. 错误信息难以理解
当模板代码中出现错误时,编译器生成的错误信息往往非常冗长和复杂,尤其是在嵌套模板的情况下,很难定位和理解错误的根源。
3. 代码膨胀
由于模板会为每个不同的模板参数组合生成一份代码,可能会导致可执行文件的大小增加,特别是在大量使用模板的项目中。
易错易忽略的语法点:
1. 模板声明和定义分离问题
在 C++98 中,模板的声明和定义通常不能像普通函数那样分别放在头文件和源文件中。因为模板的实例化需要在编译时完成,编译器需要看到模板的完整定义才能进行实例化。如果将模板的声明和定义分离,可能会导致链接错误。
解决方法:将模板的定义也放在头文件中。或直接定义hpp文件。
2. 模板参数推导问题
在某些情况下,编译器可能无法正确推导模板参数。例如,当模板函数有多个参数,并且希望编译器根据部分参数推导模板参数时,可能会出现问题。
解决方法:显示实例化模板。