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.出现模板编译错误时,错误信息很凌乱,不容易定位。

posted @   卖寂寞的小男孩  阅读(307)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示