C++模板进阶(非类型模板参数,模板特化,模板的分离编译)
文章目录
零.前言
本节主要介绍三点:非类型模板参数,函数模板和类模板的特化,模板不能进行分离编译。
1.非类型模板参数
(1)概念
模板参数分为类型形参和非类型形参。
类型形参:出现在模板列表中,跟在class和typename之类的参数类型名称。
非类型形参:就是用一个常量作为类模板的一个参数,在类(模板)中可以将参数当成常量来使用。
这两者可以在模板参数列表中单独存在也可以共同存在。
注意:非类型形参只能是整型,不能是浮点数等。
(2)举例
非类型模板参数的使用
template<class T,size_t N>
class A
{
public: A()
{
cout<<"N";
}
private:int top;
T a[N];
};
int main()
{
A<int,10> a;
}
其中T是一个类型参数,而N就是一个非类型参数。N是一个常量,因此可以给数组传值。
通过调试我们可以看到对象a中已经有一个十个元素的数组了。
非类型模板参数的缺省使用
这个和函数的缺省使用一样,只需要加上N=10即可:
template<class T,size_t N=10>
注意对于类型模板参数无法进行缺省操作。
库里面也有一些带缺省模板参数的类,比如array它表示一个数组,它和普通数组的区别在于它的[]是一个重载函数,对越界的检测为断言检测,更加严格。
array<int,10> aa1 建立一个十个int的数组。
2.模板的特化
(1)概念
在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化和类模板特化。
(2)模板的特化步骤
1.首先要有一个基础的模板。
2.关键字template带一个<>
3.函数名或类名后跟一个<>其中是需要特化的类型。
(3)函数模板的特化
template<class T,class N>
void print(T& a, N& b)//普通模板
{
cout << "nomal template" << endl;
}
template<>//特化模板
void print<int,char>(int& a, char& c)
{
cout << "int and char" << endl;
}
int main()
{
int a, b;
char c,d;
print(a, b);
print(a, d);
return 0;
}
在以上的例子中,当向模板中传入的是两种不同类型(非int,char)时调用原来的模板,当传入的是int和char时,调用的是特化的模板。
注意特化模板定义时<>中int,char的顺序要与形参的顺序一致,不然会报错。
(4)类模板的特化
全特化
template<class T1,class T2>
class Data
{
public:
Data()
{
cout << "<T1,T2>" << endl;
}
private:T1 a;
T2 b;
};
template<>
class Data<int,char>
{
public:
Data()
{
cout << "<int,char>" << endl;
}
private:int d;
char f;
};
int main()
{
Data<int, int> d1;
Data<int, char> d2;
return 0;
}
全特化指的是将模板参数的T1和T2均进行特化操作,上述例子中特化成了int和char。
偏特化
template<class T2>
class Data<T2, char>
{
public:
Data()
{
cout << "<T2,char>" << endl;
}
private:int d;
char f;
};
这里进行的就是偏特化操作,当后一个是char的时候,调用的就是这个模板来创建类,当与全特化矛盾时,会先进行全特化,比如传入的是int和char时,调用的是上一个例子中全特化的模板。
下面把三种形式即打印结果全部进行列出:
template<class T1,class T2>
class Data
{
public:
Data()
{
cout << "<T1,T2>" << endl;
}
private:T1 a;
T2 b;
};
template<>
class Data<int,char>
{
public:
Data()
{
cout << "<int,char>" << endl;
}
private:int d;
char f;
};
template<class T2>
class Data<T2, char>
{
public:
Data()
{
cout << "<T2,char>" << endl;
}
private:int d;
char f;
};
int main()
{
Data<int, int> d1;//调用根模板
Data<int, char> d2;//调用全特化模板
Data<double, char> d3;//调用偏特化模板
return 0;
}
指针与引用的特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << "*" << endl;
}
private:T1* a;
T2* b;
};
我们使用T*来进行特化。
引用同理。
注意:非类型参数也可以进行特化。
3.模板参数的分离编译
这里先建立stack.h,stack.cpp,test.cpp以函数模板方便进行举例:
//stack.h
template<class T>
T Add(const T& left, const T& right);
//stack.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
//test.cpp
Add(1, 2);
(1)分离编译
模板不能像函数一样进行分离编译。
分离编译的意思是,定义与实现不在同一个文件。
比如我们通常定义.h文件和.cpp文件:
.h文件:结构定义和函数声明,了解框架设计和基本功能。
.cpp文件:函数的定义,了解具体的实现。
(2)模板不能分离编译的原因
我们按照程序的编译顺序来分析这个问题,一下后缀均为Linux系统下的后缀:
默认的三个文件为:stack.h,stack.cpp,test.cpp
预处理
头文件展开,条件编译,宏替换,去掉注释。
操作:.h文件被展开,.cpp文件变成.i文件。
剩余文件:stack.i test.i
编译
检测语法问题,生成汇编代码。
操作:对于函数来说,可以生成汇编代码,但是对于模板来说在stack.i中我们不知道T是什么类型,因此无法进行空间的开辟。因此无法生成对应的汇编代码,无法产生函数的地址。在汇编的过程中没有对该模板进行操作。
剩余文件:stack.s,test.s
汇编
将汇编代码转化成二进制的机器码。
生成文件:stack.o,test.o
链接
链接失败,因为没有函数地址。
(3)解决措施
显示实例化
我们只需要在stack.cpp中将T显示实例化即可。
template
int Add<int>(const int& left, const int& right);
在stack.cpp中加上这样一段,就是将T实例化成了int类型,可以开辟空间产生地址,使最终链接成功。
声明和定义不分离
显示实例化并不是常用的方法,我们一般采取定义和实例化不分离的方式。
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
即在.h中声明并定义函数。这样头文件展开之后在编译阶段就能确定函数的地址了。
而对于普通函数来说,在链接的时候确定地址,两者是有区别的。
不过这样做也失去了保密性,使得可维护性变差。
4.模板总结
(1)优点
1.模板复用了代码,节省资源,可以进行更快的迭代开发,C++标准模板STL因此而诞生。
2.增加了代码的灵活性。
(2)缺点
1.模板会使编译时间变长(多了一个实例化的过程)。
2.出现模板编译错误时,错误信息很凌乱,不容易定位。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)