研学网站平台建设方案,五华区网站,有需要网站建设的没,广州工商注册查询网文章目录一、非类型模板参数1、非类型模板参数2、C11 中的 array 类二、模板的特化1、模板特化的概念2、函数模板特化3、类模板特化3.1 全特化3.2 偏特化三、模板的分离编译四、模板总结一、非类型模板参数
1、非类型模板参数
模板参数分为类型形参与非类型形参#xff0c;类…
文章目录一、非类型模板参数1、非类型模板参数2、C11 中的 array 类二、模板的特化1、模板特化的概念2、函数模板特化3、类模板特化3.1 全特化3.2 偏特化三、模板的分离编译四、模板总结一、非类型模板参数
1、非类型模板参数
模板参数分为类型形参与非类型形参类型形参即出现在模板参数列表中跟在 class 或者 typename 关键字之后的参数类型名称我们前面使用的所有模板参数都是类型形参而非类型形参则是用一个常量作为类模板/函数模板的一个参数在类模板/函数模板中可将该参数当成常量来使用。
我们以静态数组为例在没有非类型模板参数时我们采用如下方式来定义一个静态数组
#define N 10
templateclass T
class arr {
public://...
private:T _a[N];
};void test1() {arrint arr;
}但是这样有一个缺陷因为 N 的大小是固定的所以我们只能定义出 N 个元素大小的 arr对象那么此时如果我们想要同时定义两个大小为 10 和 100 的数组显然是做不到的。
C 中设计了非类型模板参数来解决了这个问题如下我们可以通过传递不同的非类型形参来定义不同的类非类型模板参数在函数模板中的使用也是如此
templateclass T, size_t N
class arr {
public://...
private:T _a[N];
};void test1() {arrint, 10 arr1;arrdouble, 100 arr2;
}注意事项 非类型模板参数也可以给定缺省值非类型模板参数只适用于整形不适用于浮点数、类对象以及字符串非类型的模板参数必须在编译期间就能确认结果即非类型模板参数的实参只能是常量。 2、C11 中的 array 类
C 11 中引入了一个新类 – arrayarray 使用非类型形参作为模板参数其底层其实就相当于静态数组
由于 array 底层是静态数组所以 array 的特性和静态数组的特性一样 – 数据存放在栈上、不要求数据连续存储 (使用 [] 给指定位置的数据赋值)、也不允许 push_back (不能扩容)array 使用文档
有的同学可能有疑问 – 既然 array 的底层就是静态数组那为什么不直接使用C语言中的静态数组而要将它封装成为一个新类呢答案是为了更严格的进行越界检查。
C语言静态数组对越界的检查规则是 – 越界读不检查、越界写抽查 而 array 里面重载了 [] 运算符在 operator[]() 函数里面对数组边界进行了判断所以不管是越界读还是越界写都会检查出来 所以其实 C 11 设计出 array 类是为了让 array 替代掉C语言的静态数组以此来帮助人们更早的发现并解决程序中可能出现的越界问题但是由于人们数组已经用习惯了所以 array 其实被使用的并不多。
注array 除了具有安全性还有可读性、抽象性、兼容性等优点。 二、模板的特化
1、模板特化的概念
通常情况下使用模板可以实现一些与类型无关的代码但对于一些特殊类型的可能会得到一些错误的结果需要特殊处理比如实现了一个专门用来进行小于比较的函数模板
// 函数模板 -- 参数匹配
templateclass T
bool Less(T left, T right)
{return left right;
}void test4()
{cout Less(1, 2) endl; // 可以比较结果正确Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout Less(d1, d2) endl; // 可以比较结果正确Date* p1 d1;Date* p2 d2;cout Less(p1, p2) endl; // 可以比较结果错误
}可以看到Less 绝对多数情况下都可以正常比较但是在特殊场景下就得到错误的结果上述示例中p1 指向的 d1 显然小于 p2 指向的d2 对象但是 Less 内部并没有比较 p1 和 p2 指向的对象内容而比较的是 p1 和 p2 指针的地址这就无法达到预期而发生错误。
此时就需要对模板进行特化 – 即在原模板类的基础上针对特殊类型进行特殊化的处理模板特化中分为函数模板特化与类模板特化。
2、函数模板特化
函数模板特化的步骤如下 必须要先有一个基础的函数模板关键字 template 后面接一对空的尖括号函数名后跟一对尖括号尖括号中指定需要特化的类型函数形参表必须要和模板函数的基础参数类型完全相同如果不同编译器可能会报一些奇怪的错误。 如下
// 函数模板 -- 参数匹配
templateclass T
bool Less(T left, T right)
{return left right;
}//对Date*类型进行模板特化
template
bool LessDate*(Date* left, Date* right)
{return *left *right; //特殊处理比较left和right指向的内容而不是left和right本身
}有的同学可能会说我直接重载一个参数类型为 Date* 的函数即可为什么要费这么大劲搞成模板的特化呢-- 确实由于函数支持重载所以我们完全可以将重载一个/多个特殊类型的形参所以一般情况下如果函数模板遇到不能处理或者处理有误的类型为了实现简单通常都是将该函数直接给出 (函数重载)。
// 函数模板 -- 参数匹配
templateclass T
bool Less(T left, T right)
{return left right;
}//Date*类型的函数重载
bool Less(Date* left, Date* right)
{return *left *right; //特殊处理比较left和right指向的内容而不是left和right本身
}如上对于一些参数类型复杂的函数模板直接给出即实现为函数重载这种方法该种实现简单明了代码的可读性高容易书写因此函数模板不建议特化。
3、类模板特化
类模板特化又分为全特化与偏特化。
3.1 全特化
全特化即是将模板参数列表中所有的参数都确定化如下
templateclass T1, class T2
class Data {
public:Data() { cout DataT1, T2 endl; }
private:T1 _d1;T2 _d2;
};template
class Dataint, char { //类模板全特化
public:Data() { cout Dataint, char endl; }
private:int _d1;char _d2;
};3.2 偏特化
偏特化任何针对模版参数进一步进行条件限制设计的特化版本。偏特化有以下两种表现方式 部分特化 – 将模板参数类表中的一部分参数特化参数更进一步的限制 – 偏特化并不仅仅是指特化部分参数而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。 我们以如下类模板为例
templateclass T1, class T2
class Data {
public:Data() { cout DataT1, T2 endl; }
private:T1 _d1;T2 _d2;
};部分特化
//部分特化--将第二个参数特化为int
template class T1
class DataT1, int {
public:Data() { cout DataT1, int endl; }
private:T1 _d1;int _d2;
};void test6() {Dataint, char d1; //使用普通模板实例化Dataint, int d2; //第二个参数与模板特化中的特化参数相同优先使用特化模板进行实例化
}可以看到我们可以将模板中的部分参数显示指定为某种具体类型这样模板参数在进行匹配时会优先匹配。
参数更进一步限制
//参数更进一步限制--两个参数偏特化为指针类型
template typename T1, typename T2
class Data T1*, T2* {
public:Data() { cout DataT1*, T2* endl; }
private:T1 _d1;T2 _d2;
};//两个参数偏特化为引用类型
template typename T1, typename T2
class Data T1, T2 {
public:Data(const T1 d1, const T2 d2): _d1(d1), _d2(d2){cout DataT1, T2 endl;}
private:const T1 _d1;const T2 _d2;
};void test7() {Datadouble, int d1; // 调用特化的int版本Dataint, double d2; // 调用基础的模板Dataint*, int* d3; // 调用特化的指针版本Dataint, int d4(1, 2); // 调用特化的指针版本
}可以看到我们可以通过偏特化对模板参数进行进一步的限制比如模板参数定义为 T*, T*这样只要实参为指针类型不管是 int*、double*、还是 vectorint*都会调用此特化模板又比如将模板参数定义为为 T, T这样只要实参是引用类型就会调用此特化模板
从而实现了在限制参数类型的同时又不会将参数局限为某一种具体类型。 三、模板的分离编译
阅读我博客的同学会发现自从学习了模板以后凡是要用到模板的类我们成员函数的声明和定义都是放在一起的或者是直接在类中给出函数的定义而不提供函数的声明比如我们模拟实现的 vector、list、stack、queue、priority_queue 等容器
那为什么我们不像C语言或者非模板类那样将类成员函数的声明和定义进行分离呢函数声明和定义分离不是即能够方便别人阅读我们的代码还能够保护源码不被泄露吗-- 这是因为模板不支持分离编译。
我们以简易版的 stack 为例
//Stack.h
#includeiostream
using namespace std;templateclass T
class Stack
{
public:Stack(int capacity);~Stack();void push(const T x);private:T* _a;int _top;int _capacity;
};//Stack.cpp
#include stack.htemplateclass T
StackT::Stack(int capacity)
{cout Stack(int capacity ) capacity endl;_a (T*)malloc(sizeof(T) * capacity);if (_a nullptr){perror(malloc fail);exit(-1);}_top 0;_capacity capacity;
}templateclass T
StackT::~Stack()
{cout ~Stack() endl;free(_a);_a nullptr;_top _capacity 0;
}templateclass T
void StackT::push(const T x)
{// ....//_a[_top] x;
}如上我们将模板类 stack 进行声明和定义分离注意 1、类模板的外部成员定义不得具有默认参数即类模板声明与定义分离时不能成员函数不能使用缺省参数 2、类模板的成员函数在分离定义时必须指明该函数是属于那个类的而 stack 是类名stackT 才是类型所以我们需要在每个成员函数前面指明类类型 stackT。 但是当我们编译运行的时候我们发现分离定义的所有成员函数都出现了链接性错误
造成这种错误的原因如下
在C语言 程序环境和预处理 那一节我们学习了一个 .c/.cpp 程序要变成 .exe 可执行程序一共要经历四个步骤分别是预处理、编译、汇编和链接这期间它们的工作分别为 预处理注释的删除#define 定义的符号、宏的替换以及删除各种条件编译的处理头文件的展开编译进行语法分析、词法分析、语义分析和符号汇总汇编生成符号表链接合并段表、符号表的合并和重定位。 同时预处理、编译、汇编这几个阶段每个源文件 (.c 文件) 都是独立进行的只有在链接时才会将这几个目标文件合并到一起形成可执行程序。
在了解了这些知识以后我们就可以得出程序报错的原因了 1、预处理时Stack.h 头文件分别展开到 Stack.cpp 和 Test.cpp 源文件中 2、经过编译Stack.cpp 和 Test.cpp 分别转变成汇编代码 3、在汇编时由于 Test.cpp 里面只有 Stack、~Stack、push 等函数的声明而没有其定义所以 Test.cpp 生成的符号表会给这些函数对应一个无效地址同时由于 Stack.cpp 里面并没有对模板实例化的代码即没有 Stackint也就没有生成具体的代码所以 Stack.cpp 的符号表里面函数对应的也是无效地址 4、在链接时需要将 Test.cpp 和 Stack.cpp 符号表中的内容进行合并与重定位但是由于它们符号表中的都是无效地址所以发生链接错误。 在找出错误原因后有的同学可能会说这简单在 Stack.cpp 中对模板进行显式实例化即可如下
//Stack.cpp 中增加显式实例化的代码
template
class Stackint;这样做确实可以但是如果我们此时还要定义一个 double 类型的对象呢那么我们又需要将 Stack.cpp 中的显式实例化类型改为 double也就是说在同一份代码中我们只能定义同一种类型的对象那么这样也就失去了模板原本的意义了。
所以模板不支持分离编译我们一般采用其他的解决办法如下 1、模板函数不进行声明直接在类里面给出函数的定义如果类很大时这种方法不方便别人阅读我们的代码不推荐使用当类较小时可以这样做比如我们之前模拟实现的 STL 容器 2、将模板函数的声明和定义放到同一个文件 “xxx.hpp” 中 (hpp.h .cpp) 。(这种方式使用于类较大时方便别人快速了解我们的类) 3、注这两种方法都有一个缺点 – 会暴露源码因为函数的声明和定义是在一个文件中的我们将类提供给别人使用时不得不将源码也暴露给别人这也是模板的一个缺点。 四、模板总结
模板的优点 模板复用了代码节省资源更快的迭代开发C的标准模板库 (STL) 因此而产生模板增强了代码的灵活性。 模板的缺点 模板会导致代码膨胀问题也会导致编译时间变长出现模板编译错误时错误信息非常凌乱不易定位错误