C++之模板进阶
目录
前言
非类型模板
模板的特化
函数模板特化
类模板特化
全特化
偏特化
模板分离编译
定义
模板的分离编译
模板的优缺点
优点
缺点
前言
我们常说头文件里面放声明,源文件中放定义。在string类模拟的时候,我们确实发现string类头文件中放声明,而源文件中放定义。但在学习了list类 和 vector类的模拟实现的时候,我们发现全部内容都放在了一个文件中,而且它们有共同点——都用了类模板。那么到底类模板有什么特别的地方呢?下面我们开始学习今天的内容,在文章中找到答案。
非类型模板
模板参数分为类型形参和非类型实参。
类型形参:出现在模板列表中,跟在class或者typename之类的参数类型。
科普typename:typename在当模板参数声明的时候和class的作用相同,但是当在模板中,存在嵌套依赖类型时一定要使用typename,比如有一个类模板T,它有一个嵌套的类型iterator。在模板声明中使用T::iterator时,编译器不确定iterator是一个类型还是一个对象。通过在前面加上typename关键字就可以明确告诉编译器iterator是一个类型。
适用场景:
模板类的嵌套类型:当模板类中嵌套了其他类型(如
iterator
、const_iterator
等)时,需要使用typename
。模板函数中的依赖类型:在模板函数中,如果使用了依赖于模板参数的类型,也需要使用
typename
。
非类型参数:用一个常数作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当作常量使用。
#include<iostream>
using namespace std;
namespace slm
{
template<class T,size_t N=10>
class arr
{
public:
arr()
:_size(1)
{
_arr[0] = 6;
std::cout << _arr[0] << endl;
}
private:
T _arr[N];
size_t _size;
};
}
int main()
{
slm::arr<int> a1;
return 0;
}
注意:
浮点数、类对象以及字符串都不允许作为非类型模板参数。
非类型的模板参数必须在编译期就能确认结果。当模板被实例化时,编译器会根据模板参数的具体值生成对应的代码。如果非类型模板参数的值在编译时无法确定,编译器就无法生成正确的代码实例。
模板的特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于特殊类型的可能会得到错误的结果。比如下面的代码:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
}
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if (_year == d._year && _month == d._month)
{
return _day > d._day;
}
return false;
}
// d1 < d2
bool operator<(const Date& d)
{
return !(*this >= d);
}
bool operator<=(const Date& d)
{
return !(*this > d);
}
// d1 >= d2
bool operator>=(const Date& d)
{
return *this > d || *this == d;
}
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
cout << Less(3, 4) << endl;//结果为1,结果正确。
Date d1(2025, 3, 12);
Date d2(2025, 3, 19);
cout << Less(d1, d2) << endl;//结果为1,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;//结果为0,结果错误。
return 0;
}
在上面第三个例子中,我们明明是想要比较的是通过传指针来比较d1和d2的值,但是Less内比较的是p1和p2指针的地址,这就不是我们想要的。
所以这就需要模板特化,在原模版的基础上,对特殊类型进行特殊化的实现。
函数模板特化
特化要求有四步:
1.要有一个基础函数模板
2.关键字template后接尖括号<>
3.函数名后跟一对尖括号,尖括号中有指定类型
4.函数形参表必须和模板函数的基础参数类型完全相同,如果不同,可能会报错。
但通常我们遇到模板不能处理或者处理有误的类型,直接用函数给出。
它一样不会走函数模板,直接调用类型匹配的函数。它的可读性高,容易书写。
但函数模板不建议特化。
类模板特化
全特化
模板参数列表中所有参数都确定。
步骤也和函数模板特化的步骤相似。
template<class T,class T1>
class Date
{
public:
Date()
{
std::cout << "Date" << std::endl;
}
private:
T a;
T1 b;
};
template<>
class Date<int, double>
{
public:
Date()
{
std::cout << "Date<int, double>" << std::endl;
}
private:
int a1;
double b1;
};
int main()
{
Date<int, int>d1;
Date<int, double>d2;
return 0;
}
除了全特化还有偏特化。
偏特化
偏特化有两种表现形式:
1.部分特化
2.参数进一步限制,比如偏特化成指针类型
template<class T,class T1>
class Date
{
public:
Date()
{
std::cout << "Date<T1,T2>" << std::endl;
}
private:
T a;
T1 b;
};
//部分偏特化
//template<class T>
//class Date<T,int>
//{
//public:
// Date()
// {
// cout << "Date<T,int>" << endl;
// }
//private:
// T a;
//};
//进一步限制,只有两个是指针才能调用。
template<class T,class T1>
class Date<T*, T1*>
{
public:
Date()
{
std::cout << "Date<T1*,T2*>" << std::endl;
}
private:
T a;
T1 b;
};
int main()
{
/*Date<int, char>d1;
Date<int, int> d2;*/
Date<int, char> d1;
Date<int*, float*>d2;
return 0;
}
模板分离编译
定义
一个程序包含若干个源文件,而每个源文件单独编译生成目标文件,最后经过链接把所有目标文件连接起来形成一个单一的可执行文件的过程。
模板的分离编译
预备知识:
在程序运行时一般要经历四个步骤:预处理——>编译——>汇编——>链接。
编译:对程序按照语言特性进行词法、语法、语义分析,错误检查无误后生成汇编代码。
注意:头文件不参与编译,编译器对工程中的多个源文件是分离开单独编译的。
链接:将多个.obj文件合并成一个,并处理没有解决的地址问题。
下面的代码能运行成功吗?
//add.h
template <class T>
T Add(T a, T b);
//add.cpp
#include"add.h"
template<class T>
T Add(T a,T b)
{
return a + b;
}
//test.cpp
#include"add.h"
int main()
{
cout << Add(3, 4) << endl;
return 0;
}
结果:会报链接错误(LNK2019 无法解析的外部符号 _main,函数 "int __cdecl invoke_main(void)" (? invoke_main@@YAHXZ) 中引用了该符号)
解决方法有两个:
1.模板定义和声明不分离。(推荐)
//add.h或者add.cpp文件也可以
template <class T>
T Add(T a,T b)
{
return a + b;
}
//test.cpp
#include"add.h"
int main()
{
cout << Add(3, 4) << endl;
return 0;
}
2.模板定义的位置显式实例化(不推荐)
//add.h
template <class T>
T Add(T a, T b);
extern template int Add<int>(int t1, int t2);//函数模板实例化的声明,有没有都可以运行成功
//add.cpp
#include"add.h"
template<class T>
T Add(T a,T b)
{
return a + b;
}
template int Add<int>(int t1, int t2);//函数模板实例化的定义,必须要有
//test.cpp
#include"add.h"
int main()
{
cout << Add(3, 4) << endl;
return 0;
}
第二种方法是如果你想调用哪个类型的函数,就必须要去挨个去进行实例化定义,不方便,所以不推荐。
模板的优缺点
优点
1.模板复用了代码,节省资源,更快的迭代开发
2.增强了带吗的灵活性
缺点
1.模板会导致代码膨胀问题,也会导致编译时间变长。
2.出现模板编译错误时,错误信息凌乱,不易定位错误。