公司动态

C++——模板进阶

📅 2026/7/18 14:03:53
C++——模板进阶
在上一节的priority_queue讲解完成后完成了一些常用C容器的基础使用和底层实现这篇博客开始将继续向后学习C的内容。一.非类型模板参数在之前的模板函数以及模板类中模板的类型代替了具体的参数。这里我们再学一种新的模板参数——非类型模板参数。这个模板参数不代表一个具体的类型而用来指代一个常量如下面代码所示templateclass T , size_t N 10 class arr { public: private: T _a[N]; };这里实现了一个数组并用类对数据实现了封装同时实现为类模板唯一不同的是在模板参数列表中我们添加了一个size_t类型的参数N并设默认值为10。在数组中用N来实现具体实现多大的空间。具体使用方法如下面的代码所示//这样所有的内容都存储在类中可能会出现栈溢出的问题 arrint,10 ar1; arrint, 1000 ar2;这里分别创建了一个大小为10个int的数组ar1和一个大小为1000个int的数组ar2。通过调试列表可以看到ar1数组内部有着10个int类型的数据ar2有着1000个。这样就能通过显示的传递一个常量值来实现对于数组大小的控制。这就叫做非类型模板参数可以传递的类型为整形家族char、int、size_t、long、long long等。注意非类型模板参数不能传递变量只能传递一个常量。因为模板需要在编译时就确定模板参数所以不能是常量。这也就决定了vs编译器下哪怕利用该方法也无法实现变长数组。在这里我们引出一个新的容器——array。1.1 array容器array容器包含在array的头文件中。类模板的方式实现内部有一个类型参数T和一个size_t的非类型模板参数N。其本质是对于数组的封装内部可以理解为就是一个数组然后利用类的方式对其进行了一定的封装[ ]取下标等操作均是调用相应的operator重载函数//一种新的容器arrayC中用来当作数组使用 arrayint, 10 a1; a1[0] 1; a1[9] 100; arrayint, 10::iterator it a1.begin(); while (it ! a1.end()) { cout *it ; it; } cout endl; //从输出结果可以看出array容器并不会对数据进行初始化这里我们简单生成一个能存放10个int数据类型的array。将第一个数据改为1第10个数据改为100。通过迭代器遍历来输出一下可以看出array容器并没有对数组进行初始化。这么来看array容器封装起来的数组似乎和直接数组没有区别。较为明显的差别就在于array容器中的相关操作例如[ ]下标等是通过函数重载来实现而数组数转换为*(arri)来实现。这就导致了两种情况下对于数组越界访问的处理情况不同a1[0] 1; a1[9] 100; arrayint, 10::iterator it a1.begin(); while (it ! a1.end()) { cout *it ; it; } cout endl; //从输出结果可以看出array容器并不会对数据进行初始化 //这样看似乎和直接生成一个数组差别不大 int arr[10] { 1,2,3,4,5,6,7,8,9,10 }; //数组的越界可以读取越界的内容无法进行修改 //数组本质是*(arr10),且编译器对其的检查是抽查检查后两个标志位的数据是否被修改 //如果被修改则报错如果没有修改正常运行所以数组可以越界读取数据 cout arr[10] endl; //arr[10] 10; //如果是array的容器直接就会断言报错 //因为内部本质是调用[]运算符重载函数可以利用传递的非类型模板参数N cout a1[10] endl;这里再生成一个普通数组arr里面存放着10个数据。如果访问下标为10的数据那么可以正常访问但是不能修改。因为数组对于越界访问是抽查行为会在数组后面的两个位置设为标志位如果标志位的数据被改变了就会报错。由于读取数据不会改变标志位的数据所以数组越界可以正常读但不能写。但数组越界不是所有位置都能检查出来。array就不同了直接在重载函数内部利用断言下标和非类型模板参数N的大小即可这样就能实现对于数组边界的严格控制。二. 函数模板特化通常情况下使用模板可以实现一些与类型无关的代码但对于一些特殊类型的可能会得到一些错误的结果需要特殊处理这里用一个比较函数模板来举例讲解templateclass T bool Less(T data1, T data2) { return (data1 data2); } cout Less(1, 2) endl; cout Less(1.1, 2.2) endl;这是一个判断第一个数据是否小于第二个数据的函数模板。正常情况下如果传递两个数据都能进行比较。但不乏一些特殊情况int* p1 new int(5); int* p2 new int(2); cout Less(p1, p2) endl; double* p3 new double(1.1); double* p4 new double(2.2); cout Less(p3, p4) endl;这里分别创建了p1、p2、p3、p4这四个指针。其中p1和p2指针是一个整形数据指针p3和p4是double类型的指针。如果直接利用该Less函数进行比较那么比较的就是指针变量的大小并不是这数据的大小为了解决该问题利用函数特化的新语法templateclass T bool Less(T data1, T data2) { cout bool Less(T data1, T data2) endl; return (data1 data2); } //函数模板特化 template bool Lessint*(int* data1, int* data2) { cout bool Lessint*(int* data1, int* data2) endl; return *data1 *data2; } template bool Lessdouble*(double* data1, double* data2) { cout bool Lessdouble*(double* data1, double* data2) endl; return *data1 *data2; }首先模板特化需要一个具体的模板才能进行特化所以需要在原Less函数的基础上进行特化。写作方式为template内部不再需要加类型T然后在函数名后写出要特化的类型是什么比如是int*double*之后分别指明要传递的数据类型为特化后的类型。这样该函数就实现了特化编译器就知道了传递的参数为一个指针类型然后通过解引用操作符比较指针变量指向的内容即可。通过cout输出一些特定语句就可以辨别出是否调用了特化后的函数模板template bool Lessint*(int* data1, int* data2) { cout bool Lessint*(int* data1, int* data2) endl; return *data1 *data2; } template bool Lessdouble*(double* data1, double* data2) { cout bool Lessdouble*(double* data1, double* data2) endl; return *data1 *data2; } void test2() { cout Less(1, 2) endl; cout Less(1.1, 2.2) endl; int* p1 new int(5); int* p2 new int(2); cout Less(p1, p2) endl; double* p3 new double(1.1); double* p4 new double(2.2); cout Less(p3, p4) endl;可以看出第一个示例比较1和2、1.1和2.2都是调用函数模板进行比较。而两个指针对象调用了特化后的函数模板。并实现了数据之间的比较。再看下面这个函数模板:templateclass T1,class T2 void print_type(const T1 val1,const T2 val2) { //这里的const本质是对val1和val2做的const修饰不能改变的是变量本身 cout typeid(val1).name(): val1 endl; cout typeid(val2).name(): val2 endl; cout endl; }该函数模板有着两个模板参数T1和T2。该函数实现了分别输出第一个参数和第二个参数的类型和数值。通过之前一些容器的学习我们一版不会直接传递模板参数类型的数据T这样的话如果是自定义对象会需要拷贝构造效率较低。所以一般会const T 的方式传参。print_type(1, 2.2); print_type(1.1, 2);正常调用则会正常输出。与刚才的问题相同如果没有函数特化那么对于指针类型输出的将是指针。print_type(1, 2.2); print_type(1.1, 2); int* p1 new int(1); double* p2 new double(1.1); print_type(p1, p2); char* p3 new char(a); double* p4 new double(1.1); print_type(p3, p4);如果仍然像针对其中的数据进行输出就需要进行特化。注意下面模板特化的方式templateclass T1,class T2 void print_type(const T1 val1,const T2 val2) { //这里的const本质是对val1和val2做的const修饰不能改变的是变量本身 cout typeid(val1).name(): val1 endl; cout typeid(val2).name(): val2 endl; cout endl; } template void print_typeint*,double*(int* const val1, double* const val2) { //同理这里也需要达到变量本身不能修改 //所以const应该修饰指针变量本身而非指针指向的内容 cout void print_typeint*,double*(int* val1, double* val2) endl; cout typeid((*val1)).name() : *val1 endl; cout typeid((*val2)).name() : *val2 endl; cout endl; } template void print_typechar*, double*(char* const val1, double* const val2) { cout void print_typechar*, double*(char* val1, double* val2) endl; cout typeid((*val1)).name() : *val1 endl; cout typeid((*val2)).name() : *val2 endl; cout endl; }特化的方式仍然为在函数名后面加上两个模板参数分别是什么然后替换参数的类型即可。唯一需要注意的是const的位置。针对原模板参数const修饰的是val1和val2即该参数本身。而如果传递了指针参数那么const也应该修饰的是该参数本身即——指针不能改变。所以const应该放到*后面用来修饰指针而非指针指向的内容。这是极其需要注意的点。在函数特化之后也就能通过内部的解引用达到输出该数据的效果。并可以看出调用了两个特化后的函数模板。三. 类模板特化3.1 全特化、半特化templateclass T1,class T2 class Data1 { public: Data1() { cout class Data1 endl; } private: T1 _d1; T2 _d2; };这里实现了一个两个模板参数的类模板。其中的成员变量_d1用T1的类型、_d2用T2的类型。全特化就是将T1和T2两个参数均特化为具体的类型//全特化 template class Data1int,double { public: Data1() { cout class Data1int,double endl; } private: int _d1; double _d2; };与函数模板相同template之后在类名中分别加上要特化的具体类型之后将成员变量的类型分别改为这两个具体的特化类型即可之后是半特化/偏特化即针对其中的某些模板类型进行特化处理//半特化/偏特化 templateclass T1 class Data1T1,double { public: Data1() { cout class Data1T1,double endl; } private: T1 _d1; double _d2; }; templateclass T2 class Data1int, T2 { public: Data1() { cout class Data1int, T2 endl; } private: int _d1; T2 _d2; };例如第一个特化只对第二个模板参数T2进行特化为doubleT1保留为正常的模板参数。类名后要保留T1和double。第二个特化将T1特化为int保留第二个模板参数。Data1int, double d1; Data1int, int d2; Data1char, doubled3; Data1int, longd4; cout endl;进行简单调试输出第一个int,double调用了全特化因为全特化恰好是int和double第二个int,int首先全特化不能满足之后去看半特化第二个半特化第一个参数为int就会调用半特化而非原始模板可以考虑为哪怕是半成品也要图省事去找这个半成品第三个char,double调用第二个模板参数为double的半特化。第四个和第二个相同的原理会找到半特化。3.2 对模板参数的进一步限制和函数模板特化进行对比如果传递的类型是指针但是用户的目的又是对指针指向的内容进行相关操作就用到了该语法的内容。//对模板参数的进一步限制 templateclass T1,class T2 class Data2 { public: Data2() { cout class Data2 endl; } private: T1 _d1; T2 _d2; }; templateclass T1, class T2 class Data2T1*,T2* { public: Data2() { cout class Data2T1*,T2* endl; } void f1() { //通过输出也可以看出T1和T2的类型仍然为原类型而非指针类型 cout typeid(_d1).name() : _d1 endl; cout typeid(_d2).name() : _d2 endl; cout typeid(*(_d1)).name() : _d1 endl; cout typeid(*(_d2)).name() : _d2 endl; } private: T1* _d1; T2* _d2; };重新实现了一个类模板Data2内部有两个模板参数。同时实现了一个针对模板参数的进一步限制的函数模板在类名后指定两个模板参数类型为指针类型这样就可以利用该指针也可以利用该指针指向的内容Data2int*, double* dd1; dd1.f1();这里传递的模板参数分别为int*和double*之后调用内部的f1函数。根据输出结果就可以看出这里成功实现了对于指针及指针内部数据的比较。下面将举一些具体的示例进行说明priority_queueDate pqd; pqd.push(Date(2019, 1, 1)); pqd.push(Date(2020, 1, 1)); pqd.push(Date(2021, 1, 1)); pqd.push(Date(2022, 1, 1)); pqd.push(Date(2023, 1, 1)); while (!pqd.empty()) { cout pqd.top() ; pqd.pop(); } cout endl;之前实现过一个日期类这里如果在优先级队列中存放日期类成员那么Less的仿函数进行比较的时候就会自动调用日期类内部的operator的比较函数。那么日期也会排列成一个大根堆那么还是之前常谈的问题如果是一个指针对象呢priority_queueDate* pqpd; Date* pd1 new Date(2019, 1, 1); Date* pd2 new Date(2020, 1, 1); Date* pd3 new Date(2021, 1, 1); Date* pd4 new Date(2022, 1, 1); Date* pd5 new Date(2023, 1, 1); pqpd.push(pd1); pqpd.push(pd2); pqpd.push(pd3); pqpd.push(pd4); pqpd.push(pd5); //这样的话就是比较地址达不到想要的效果。 //如果想要通过地址达到想要的效果就需要传递合适的仿函数 while (!pqpd.empty()) { cout *(pqpd.top()) ; pqpd.pop(); } cout endl;如果传递的是一个指针对象那么会按照指针的大小排列内容就不再是一个大根堆。如果没有本节的模板特化那么就需要显示的传递一个进行指针内容比较的仿函数templateclass T class Less_p { public: bool operator()(const T data1, const T data2) { return (*data1 *data2); } }; priority_queueDate*,vectorDate*,Less_pDate* pqpd; Date* pd1 new Date(2019, 1, 1); Date* pd2 new Date(2020, 1, 1); Date* pd3 new Date(2021, 1, 1); Date* pd4 new Date(2022, 1, 1); Date* pd5 new Date(2023, 1, 1); pqpd.push(pd1); pqpd.push(pd2); pqpd.push(pd3); pqpd.push(pd4); pqpd.push(pd5); //这样的话就是比较地址达不到想要的效果。 //如果想要通过地址达到想要的效果就需要传递合适的仿函数 while (!pqpd.empty()) { cout *(pqpd.top()) ; pqpd.pop(); } cout endl;通过显示调用Less_p的仿函数就能定向实现对于指针对象中内容的排序。此时再运行内容就变成了大根堆如果利用本节模板特化的指示只需要对默认的Less仿函数进行一个针对于指针的特化即可templateclass T class LessT* { public: Less() { cout class LessT* endl; } bool operator()(T* const data1, T* const data2) { return (*data1 *data2); } };这是通过对模板参数的进一步限制来实现的定向识别模板参数为指针类型比较的时候通过解引用针对内容进行比较即可。template class LessDate* { public: Less() { cout class LessDate* endl; } bool operator()( Date* const data1, Date* const data2) { return (*data1 *data2); } };这是通过函数模板的特化来实现特化类型为Date*Practice::priority_queueDate* pqpd; Date* pd1 new Date(2019, 1, 1); Date* pd2 new Date(2020, 1, 1); Date* pd3 new Date(2021, 1, 1); Date* pd4 new Date(2022, 1, 1); Date* pd5 new Date(2023, 1, 1); pqpd.push(pd1); pqpd.push(pd2); pqpd.push(pd3); pqpd.push(pd4); pqpd.push(pd5); while (!pqpd.empty()) { cout *(pqpd.top()) ; pqpd.pop(); } cout endl;可看出利用本篇博客的内容也可针对指针内容进行比较。但如果通过显示传递一个仿函数那就没这些事了。四. 模板的分离编译之前在实现C一些基本容器比如vector list时它们的实现方式为类模板当时说的是如果对模板函数声明和定义进行分离就会出现编译错误在这里详细讲解一下Fun.h头文件进行模板函数的声明Fun.cpp进行函数的定义main中进行调用。此时会有两个编译错误LNK201链接错误。下面对C/C的编译过程进行一次回顾预处理展开头文件宏替换去掉注释条件编译 Fun.a test2.a编译检查语法生成汇编代码 (Fun.s test2.s)汇编生成二进制机器码 (Fun.o test2.o)链接多个文件进行合并符号表合并生成可执行程序 a.out如果是正常函数的分离编译那么每个文件会依次进行预处理-编译-汇编的过程最后在符号表合并的时候将函数定义的位置进行填补即可。而模板会出现链接报错的原因就在于实例化阶段。在main函数中首次调用Fun函数并传递一个参数进行实例化而在test2文件中没有Fun函数的定义。而又由于各个文件在链接之前是分开进行各个步骤这就代表这出现Fun函数定义的Fun.cpp中无法进行实例化就不存在这个函数当然会出现链接错误。而如果一定要分离编译就需要下面这种做法在模板函数定义的下面template 然后参数的位置将类型T替换为想要的类型即可。这样就能正常输出。但这样过于麻烦如果再想调用一个char类型的数据那么就再次需要进行一次相同的声明否则仍然会出现链接错误。一般就不要这样使用了就将函数在声明的位置定义即可。或者不进行两个文件的分离编译都在头文件中编译即可。直接就可正常运行。最后对模板的优缺点进行总结优点1.模板复用了代码节省了程序员的编写成本将复用内容交给编译器自行推导。C的标准模板库也因此诞生。2.模板增强了代码的灵活性。不再需要像C语言一样针对特定的类型特定的方式实现特定的过程。而只需要针对特定的对象即可缺1.编译报错比较难以定位准确错误信息凌乱。2.会导致代码膨胀问题但这个并不是类模板的缺点哪怕人工实现也是需要这么多代码。