公司动态
C++_模板进阶
目录1.引言2.非类型模板参数拓展array3.模板的特化3.1.概念3.2.函数模板特化3.3.类模板特化3.3.1.全特化3.3.2.偏特化也可以叫半特化对部分模板参数特化特化还能对参数进一步的限制3.3.3.类模板特化应用示例类模板特化拓展4.模板分离编译4.1.什么是分离编译4.2.模板的分离编译4.3.解决方法5.模板总结6.结语1.引言前置知识C_模板初阶-CSDN博客在模板初阶的部分我们只讲了模板最基础的语法还有挺多语法没有讲放这里讲主要是为了先通过几个STL的底层实现来熟悉模板不然一口气太多东西容易乱话不多说直接进入正文——————2.非类型模板参数模板参数分为类型模板参数和非类型模板参数先前我们所讲的那些都是类型模板参数类型模板参数类型形参——出现在模板参数列表中跟在class或者typename之类的参数类型名称非类型模板参数非类型形参——用一个常量作为类(函数)模板的一个参数在类(函数)模板中可将该参数当成常量来使用举个例子如下T就是类型模板参数N就是非类型模板参数templateclass T,size_t N 10非类型模板参数可以解决比如我们想要定义静态的数组或者静态的栈之类的问题因为他这里定义的是常量举个例子如下templateclass T,size_t N 10 class stack { private: int a[N]; int _top; };那么他相比我们之前宏的好处是什么呢宏的数据是定死的但是这不是我们通过举个例子来讲解宏的情况#define _CRT_SECURE_NO_WARNINGS #includeiostream using namespace std; #define N 5 //templateclass T,size_t N 10 class Stack { private: int a[N]; int _top; }; int main() { Stack st1; Stack st2; return 0; }这种情况下我们用Stack实例化对象实例化出来的对象中的a数组是定死了5个大小非类型模板参数的情况#define _CRT_SECURE_NO_WARNINGS #includeiostream using namespace std; templateclass T,size_t N 10 class Stack { private: int a[N]; int _top; }; int main() { Stackint, 5 st1; Stackint, 15 st2; Stackint st3; return 0; }这样我们就可以通过非类型模板参数来调整静态空间开辟的空间大小其实原理还是和模板一样底层来说依旧是生成了多个类注意非类型模板参数只能用于整形char其实也算整形因为底层其实就是ASCII码映射所以char也可以当然bool也可以其他类型都不可以浮点数也不行——支持浮点型要到C20才行非类型模板参数在编译期就能确认结果因为非类型模板参数是编译器常量非类型模板参数可以给缺省值非类型模板参数最常用的应用就是用来调整开辟静态空间的大小如果类模板参数里面所有模板参数都带有缺省值的时候我们要加上不加的话会报错举个例子如下#define _CRT_SECURE_NO_WARNINGS #includeiostream using namespace std; templatesize_t N 10 class Stack { private: int a[N]; int _top; }; int main() { Stack st3;//对 #if 0 Stack st1; 错(C20开始才支持这种写法) #endif return 0; }拓展在库里面也有用到非类型模板参数的这里我简单提一下array一个是array这个容器是C11才提供的这是个静态数组他也支持迭代器随机迭代器其实就是原生指针和[]运算符重载因为它是静态数组所以它不支持扩容自然也就没有push_backinsert这类的说法了C搞这个出来是想替代静态数组举个例子如下arrayint, 10 a1; arrayint, 100 a2; int a3[10];a1和a2其实对标的就是a3他底层的实现其实就类似这种定义一个静态数组然后再实现[]运算符重载其次再实现迭代器什么的namespace bite { // 定义一个模板类型的静态数组 templateclass T, size_t N 10 class array { public: T operator[](size_t index){return _array[index];} const T operator[](size_t index)const{return _array[index];} size_t size()const{return _size;} bool empty()const{return 0 _size;} private: T _array[N]; size_t _size; } }array相比我们之前开辟静态数组方式的好处越界检查的问题很有优势静态数组检查我们先前的方式在读的时候对于越界有时候是不会被发现的就比如如下代码int a3[10]; cout a3[10] endl;在写的时候数组后面会有几个标志位如果修改了标志位会被检查出来这就是所谓的抽查机制如图数组对越界的检查一般就是对标志位的检查所以我们如果这么写,是会被检查到的因为改了标志位cina[10];但是标志位并不多所以我们如果这么写虽然越界了 但是检查不到cina[16];总结静态数组对越界的检查是用抽查的方式越界读不检查越界写检查标志位不多设置点标志位的原因是栈区内存是由高到低分配这个数组他占这一块空间他前面也是可能有别的元素的如果影响到前面的元素就出问题这个具体可以在函数栈帧那边了解函数栈帧的创建与销毁详解C语言拓展版_函数栈帧的创建与销毁鹏哥-CSDN博客array对越界的检查越界读写都能检查因为array有[]运算符重载可以在[]重载函数里进行assert断言检查不管是读还是写都会调用这个断言那自然就能检查出来了assert(indexN);而上面静态数组的底层其实就是指针偏移量的解引用检查不了所以array相比上面静态数组的方式对越界的检查更有优势但其实array也有点鸡肋因为有vector做替代vector相比array不仅能实现array的功能还能初始化还能扩容。array和vector也有本质区别array是存在栈上vector是存在堆上所以如果频繁开辟空间的话array的效率比vector高一点因为栈上开空间效率比堆上高3.模板的特化3.1.概念模板的特化指的就是对某些类型进行特殊化处理通常情况下使用模板可以实现一些与类型无关的代码然后实现同一个效果但是对于一些特殊类型可能会得到一些错误的结果需要特殊处理我举个比大小的例子#define _CRT_SECURE_NO_WARNINGS #includeiostream #includearray using namespace std; templateclass T class Less { public: bool operator()(T x, T y) { return x y; } }; int main() { int a 2; int b 1; cout Lessint()(a, b) endl; int* a1 a; int* b1 b; cout Lessint*()(a1,b1) endl; return 0; }这上面的代码我们预期而言不管是正常比较还是通过指针比较都应该是输出0但是输出结果在指针时候却出问题了如下图很明显我们在用指针的时候他进行的比较是对指针进行比较而不是对指针所指向的数据进行比较在上述代码中Less在多数情况下都可以正常比较但是在特殊场景下就得到错误的结果。此时就需要对模板进行特化即在原模板类的基础上针对特殊类型所进行特殊化的实现方式模板特化分为俩种函数模板特化和类模板特化3.2.函数模板特化函数模板特化步骤必须要有一个基础的函数模板关键字template后面接一对空的尖括号函数名后跟一对尖括号尖括号中指定需要特化的类型函数形参表必须要和函数模板的基础参数类型完全相同如果不同编译器可能会报一些奇怪的错误具体特化起来其实很简单举个例子如下templateclass T bool LESS(T x, T y) { return x y; } #if 0 函数模板 特化 #endif template bool LESSint*(int* x, int* y) { return *x *y; }bool LESSint*(int* x, int* y)就是对函数模板进行了特化如果是其他类型 就走上面的函数模板如果是int*类型就走这个特化模板但其实对于函数而言我们想要处理这种情况也可以用另一种方式——函数重载templateclass T bool LESS(T x, T y) { return x y; } #if 0 函数重载 #endif bool LESS(int* x, int* y) { return *x *y; }函数模板模板特化和函数重载是可以的一起存在的优先顺序是函数重载模板特化函数模板因为函数重载和模板特化同时存在的时候模板特化永远不会被调用这也是C不推荐函数模板特化的原因因为很容易就被重载覆盖了注意函数模板特化不推荐使用不仅是因为函数重载会影响它还有一个大因素就是他有个很大的坑举个例子我们用这种比较的时候一般而言函数模板参数一般不是T而是const T 用来防止内部修改以及减少自定义类型传参的拷贝也就是这样那我们模板特化也就对应更改下变成如下代码templateclass T bool LESS(const T x,const T y) { return x y; } template bool LESSint*(const int* x, const int* y) { return *x *y; }这个时候编不过了const T中const修饰的是x和y但是const int*就不一样了const在*之前修饰的是*x和*y那么就和函数模板不匹配了自然就编译报错了要解决这个问题很简单const放到后面就可以了也就是这样templateclass T bool LESS(const T x,const T y) { return x y; } template bool LESSint*(int* const x, int* const y) { return *x *y; }这样的话相比于函数重载而言太麻烦了而且从观感看也很别扭所以一般都不用函数模板特化基本都用函数重载这边还有个很严重的问题有下面俩种调用方式int* a1 a; int* b1 b; cout LESS(a1, b1) endl; const int* a2 a; const int* b2 b; cout LESS(a2, b2) endl;第一种的情况下是会进入到函数模板的特化里面去的但是第二种就又出问题了因为const iint*是不能传给int*的这个时候又会去调用函数模板了也就又出问题了为了应对这种情况我们得这样template bool LESSconst int*(const int* const x,const int* const y) { return *x *y; }这种类似的神奇问题太多了所以一般而言函数模板的特化基本都是不用的用的都是函数重载函数重载的话就会简单很多只需要这样就可以了bool LESS(const int* x, const int* y) { return *x *y; } bool LESS(int* x,int* y) { return *x *y; }附这边提一点const修饰普通类型没有指针的情况下放前面放后面都可以,效果一样的const int i 0; int const i 0;所以也就可以这么写但是第二种怎么看怎么别扭const int rx 1; int const ry 1;3.3.类模板特化这个相比函数模板特化就有用很多了类模板特化的要求和函数模板特化的要求和函数模板特化不同具体怎么用看下面例子即可 需要注意的是类模板特化后特化的类模板内部的成员函数可以不和原类模板的成员函数一致3.3.1.全特化特化模板参数列表所有参数我举个全特化例子templateclass T1,class T2 class Date { public: void Print() { cout DateT1,T2 endl; } private: T1 a; T2 b; }; #if 0 全特化 #endif template class Dateint,char { public: void Print() { cout Dateint,char endl; } private: int a; char b; };3.3.2.偏特化也可以叫半特化对部分模板参数特化我举个偏特化例子templateclass T1,class T2 class Date { public: void Print() { cout DateT1,T2 endl; } private: T1 a; T2 b; }; #if 0 偏特化 #endif templateclass T1 class DateT1, char { public: void Print() { cout DateT1,char endl; } private: T1 a; char b; };在全特化和偏特化都满足的情况下会走全特化特化还能对参数进一步的限制偏特化并不仅仅是指特化部分参数而是针对模板参数更进一步的条件限制所设计出来的一个特化版本我举个例子下面这俩种也叫偏特化这里关键字classtypename都可以用templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 endl; } private: T1 _d1; T2 _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; };这个偏特化就是如果传的类型是指针走的就是这个版本另一个同理传的如果是引用走的就是这个版本这种就不是针对某个类型了是针对一片当然可以这样特化template typename T1, typename T2 class Data T1*, T2 { public: Data(const T1 d1, const T2 d2) : _d2(d2) { cout DataT1*, T2 endl; } private: const T1* _d1; const T2 _d2; };这个主要是为了解决很多场景下要传引用和指针的情况就会开个特化出来3.3.3.类模板特化应用示例首先先看下面这个代码如果是普通比大小就没问题指针比大小就会出问题templateclass T struct Less { bool operator()(const T x, const T y) const { return x y; } }; int main() { Date d1(2022, 7, 7); Date d2(2022, 7, 6); Date d3(2022, 7, 8); vectorDate v1; v1.push_back(d1); v1.push_back(d2); v1.push_back(d3); // 可以直接排序结果是日期升序 sort(v1.begin(), v1.end(), LessDate()); vectorDate* v2; v2.push_back(d1); v2.push_back(d2); v2.push_back(d3); // 可以直接排序结果错误日期还不是升序而v2中放的地址是升序 // 此处需要在排序过程中让sort比较v2中存放地址指向的日期对象 // 但是走Less模板sort在排序时实际比较的是v2中指针的地址因此无法达到预期 sort(v2.begin(), v2.end(), LessDate*()); return 0; }这个时候我们就可以加个全特化如下template struct LessDate* { bool operator()(Date* x, Date* y) const { return *x *y; } };那么为了可以对更多类型的数据进行排序我们就可以再优化成为偏特化如下templateclass T struct LessT* { bool operator()(T* x, T* y) const { return *x *y; } };类模板特化拓展类模板特化后我们特化的类模板里面具体是什么样可以我们自己定义不用和原类模板一样那么我们就可以在特化的类模板中使用特化的参数如下#define _CRT_SECURE_NO_WARNINGS #includeiostream #includearray using namespace std; templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 endl; } private: T1 _d1; T2 _d2; }; template typename T1, typename T2 class Data T1*, T2* { public: Data() { T1 x; cout DataT1*, T2* endl; cout typeid(x).name() endl; } private: const T1* _d1; const T2* _d2; }; void test2() { Dataint, double d2; // 调用基础的模板 Dataint*, int* d3; } int main() { test2(); return 0; }我们d3走的是类模板的特化我们输出T1的类型原理而言应该是int*类型但其实输出的是int类型这就是类模板偏特化中成片特化的一个坑这种成片特化的时候T1*T1这样才算是完整的类型正确使用是这样的#define _CRT_SECURE_NO_WARNINGS #includeiostream #includearray using namespace std; templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 endl; } private: T1 _d1; T2 _d2; }; template typename T1, typename T2 class Data T1*, T2 { public: Data() { int a 10; T1* x1 a; T2 y1 a; cout DataT1*, T2* endl; cout typeid(x1).name() endl; cout typeid(y1).name() endl; } private: const T1* _d1; const T2 _d2; }; void test2() { Dataint, double d2; // 调用基础的模板 Dataint*, int d3; } int main() { test2(); return 0; }这里的特化我们不能把指针和引用和之前一样当做class T1,class T2这的类型因为这样的话下面的T1*和T1就对不上了我们要把指针和引用当成T1*和T1为什么这么设计呢因为这样我们就可以用指针和引用 也能用对应的原始类型。这样内部模板会更灵活一点可以避免比如如下这种情况Push(const T1 x) { ... }假设不按照上面那种方式来这个T1就是int*的话那么这个const修饰的是什么修饰的是x那么我们想要修饰*xconst加在哪根本没法加这样我们用的时候就会很难受4.模板分离编译我们先前讲过模板的 声明和定义不能分离在不同文件中这里就来讲讲原因4.1.什么是分离编译一个程序项目由若干个源文件共同实现而每个源文件单独编译生成目标文件最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。这么做的好处在我们之前实现的一些小项目和模拟实现一些功能的时候就用过那就是可以先通过.h大致了解里面的东西比如成员变量是什么成员函数有哪些效果是什么然后再去.cpp里去具体看4.2.模板的分离编译接下来我来讲讲为什么模板不能声明定义分离这里先说原因分离会导致链接链不上我通过一个样例来讲我们先回顾一下编译和链接相关的知识不太了解的可以去看这俩篇博客C语言编译和链接详解_详细讲解下c的连接-CSDN博客C语言预处理详解C语言知识完结篇-CSDN博客我这里就浅浅讲了我用代码举个例子func.h#pragma once #include iostream using namespace std; templateclass T T add(const T left, const T right);func.cpp#define _CRT_SECURE_NO_WARNINGS #include func.h templateclass T T add(const T left, const T right) { return left right; }test.cpp#define _CRT_SECURE_NO_WARNINGS #include func.h int main() { Add(1, 2); return 0; }预处理阶段主要就是头文件展开/宏替换/条件编译/去掉注释......预处理后.cpp就变成了.i头文件就没有了那上面的头文件被展开就相当于是这样了#define _CRT_SECURE_NO_WARNINGS #if 0 #include iostream using namespace std; templateclass T T add(const T left, const T right); #endif int main() { Add(1, 2); return 0; }接下来就是编译编译就是对语言特性进行检查但是这个时候其实就是对工程的语法什么进行检查无误后生成汇编代码。注意头文件不参与编译编译器对工程中的多个源文件是分离开单独编译的随后汇编这步骤就不讲了接下来最后就是链接了链接主要解决的是还没有解决的地址问题这个时候其实就已经很明朗了我们函数能声明和定义分离是因为我们函数是有固定地址的但是模板就不一样了模板的存在其实并没有地址存在对模板实例化才会有对应地址那么这个时候链接就会因为找不到对应地址而出问题4.3.解决方法解决上述模板分离编译的问题有俩种 方法将声明和定义放到一个文件中推荐用这种模板定义的位置显式实例化。不推荐不实用第一种我们先前已经讲过了接下来我来讲讲第二种方法#define _CRT_SECURE_NO_WARNINGS #include func.h templateclass T T add(const T left, const T right) { return left right; } template int add(const int left, const int right);这就是第二种方法为了和模板特化区分这里template后面不用加但是这个相当麻烦我们用什么类型就得用什么显式实例化一下代码极其冗余观感也不好所以非常不推荐使用注意这里显式实例化要放在.cpp里5.模板总结优点模板复用代码节省资源更快的迭代开发C标准模板库STL也是因此而产生代码灵活性显著增强缺点模板会导致代码膨胀问题也会导致编译时间变长如果出现模板编译错误时错误信息非常凌乱很难定位错误出在哪6.结语那么C模板进阶部分的内容就全部讲解完毕啦希望以上内容对你有所帮助感谢观看若觉得写的还可以可以分享给朋友一起来看哦毕竟一起进步更有动力嘛当然能关注一下就更好啦。