《C++初阶之STL》【模板参数 + 模板特化 + 分离编译】
【模板参数 + 模板特化 + 分离编译】
- 前言:
- ------------模板参数------------
- ------------模板特化------------
- 1. 什么是模板特化?
- 2. 为什么要使用模板特化?
- 3. 模板特化有哪些?
- ------------分离编译------------
往期《C++初阶》回顾:
/------------ 入门基础 ------------/
【C++的前世今生】
【命名空间 + 输入&输出 + 缺省参数 + 函数重载】
【普通引用 + 常量引用 + 内联函数 + nullptr】
/------------ 类和对象 ------------/
【类 + 类域 + 访问限定符 + 对象的大小 + this指针】
【类的六大默认成员函数】
【初始化列表 + 自定义类型转换 + static成员】
【友元 + 内部类 + 匿名对象】
【经典案例:日期类】
/------------ 内存管理 ------------/
【内存分布 + operator new/delete + 定位new】
/------------ STL ------------/
【泛型编程 + STL简介】
【auto关键字 + 范围for循环 + 迭代器】
【string类:详解 + 实现】
【vector容器:详解 + 实现】
【list容器:详解 + 实现】
【stack/queue/priority_queue容器适配器:详解 + 实现】
前言:
hi~ 小伙伴们大家好呀!(≧∇≦)ノ 后天就是立秋了,夏天即将结束,但炎热的天气还没过去,可能还要持续两周左右呢~(>_<)
随着夏天的结束,《C++ 初阶》也迎来了最后一篇博客:【模板参数 + 模板特化 + 分离编译】 ✨。(๑¯∀¯๑))~这时可能会有小伙伴吐槽:看博主之前的博客,全是 “C 语言、算法、数据结构、C++” 初阶的这类内容,啥时候才能学到进阶的知识呀,哈哈~( ̄▽ ̄)ゞ 让大家久等了,但是这次关于C++的故事还并没结束,下期《C++ 进阶》敬请期待哦!(ノ◕ヮ◕)ノ:・゚✧
------------模板参数------------
C++的模板参数有哪些?
模板(Template):是泛型编程的核心机制,允许在编写代码时使用参数化的类型或值,从而实现代码的复用。
模板的参数分为两大类:类型参数和非类型参数,此外还有模板模板参数(较少见)注:博主可没有打错字哦~,名字真的就叫作模板模板参数啊!
一、类型参数
类型参数(Type Parameters):表示模板中使用的数据类型可以是内置类型(如:int、double)或者是自定义类型(如:类、结构体)
1. 通用类型参数
2. 默认类型参数
可以为类型参数指定默认类型,调用时若未指定则使用默认值
template<typenameT=int>// 默认类型为 intclassStack{// ...}; Stack<> s;// 使用默认类型 int Stack<double> d;// 显式指定类型为 double示例调用:
swap<int>(3,5);// 显式指定类型为 int swap<double>(3.14,2.71);// 显式指定类型为 double// 也可隐式推导类型:swap(3, 5);(编译器自动推导为 int)使用class或typename声明,两者含义相同(推荐用typename,更清晰)
template<classT>// 类型参数 Tvoidswap(T& a, T& b){ T temp = a; a = b; b = temp;}template<typenameU>// 等价于 class UclassVector{/* ... */};二、非类型参数
非类型参数(Non-Type Parameters):表示模板中使用的常量值通常为整型、枚举值或指针/引用C++11 后支持std::nullptr_t、constexpr变量等
1. 基本语法
2. 限制条件
对于指针/引用类型的非类型参数,要求其指向的对象具有静态存储期(如:全局变量、static变量)
int global_var =0;template<int* ptr>voidfunc(){/* ... */} func<&global_var>();// 合法,指向全局变量非类型参数必须是编译期可确定的常量,不能是变量或运行时计算的值
Array<int,10> arr;// 合法,10 是编译期常量int n =10;// Array<int, n> arr; // 非法,n 是变量,非编译期常量代码案例:非类型参数的使用小案例
namespace mySpace {//任务:定义的一个“静态数组”的模板类,同时要使用“非类型参数”template<classT, size_t N =10>classarray{private: T _array[N];//1.存储数据的静态数组 注意:在编译期确定数组大小 size_t _size;//2.记录数组中有效元素数量的变量public://1.实现:“普通版本的下标运算符[]的重载函数” T&operator[](size_t index)//注意:支持对数组元素的读写操作{return _array[index];}//2.实现:“const版本的下标运算符[]的重载函数”const T&operator[](size_t index)const//注意:保证在只读场景下也能通过下标访问元素,返回的是 const 引用,确保元素不会被修改{return _array[index];}//3.实现:“获取数组中有效元素的数量的操作” size_t size()const{return _size;}//4.实现:“判断数组是否为空的操作”boolempty()const{return _size ==0;}};}三、模板模板参数
模板模板参数(Template Template Parameters):是指模板本身作为参数,用于将另一个模板传递给当前模板。
(注:暂时先了解一下概念即可)
------------模板特化------------
1. 什么是模板特化?
模板特化(Template Specialization):是模板机制的一个重要特性,允许针对特定的模板参数类型,提供模板的定制化实现。它允许针对特定类型或值,定制模板的行为,解决通用模板在特殊场景下的 “水土不服” 问题。当模板在某些特定类型下需要不同的行为或更高效的实现时,特化可以让代码更灵活、更贴合需求。
2. 为什么要使用模板特化?
在介绍的模板特化的时候,我们说模板特化是多么的厉害,但是口说无凭,模板特化真的有那么好吗?
下面的我们就来看一看模板特化的重要性。
#include<iostream>usingnamespace std;//任务1:定义一个日期类classDate{public:/*--------------成员变量--------------*/int _year;int _month;int _day;/*--------------成员函数--------------*///1.实现:“默认构造函数”Date(int y,int m,int d):_year(y),_month(m),_day(d){}//2.实现:“<运算符重载函数”booloperator<(const Date& other)const//注意:用于比较两个日期的先后顺序{if(_year != other._year)return _year < other._year;// 优先比较年份if(_month != other._month)return _month < other._month;// 年份相同则比较月份return _day < other._day;// 年份和月份都相同则比较日期}};//任务2:定义比较函数模板template<classT>boolLess(T x, T y){return x < y;//注意:依赖 T 类型的 operator< 实现}intmain(){// ---------------- 基础类型比较(正确行为)----------------// 1. 实例化 Less<int>(int, int)// 调用内置的 int 类型的 < 运算符 cout <<Less(1,2)<< endl;// ---------------- 对象类型比较(正确行为)----------------// 2. 实例化 Less<Date>(Date, Date)// 调用 Date 类重载的 operator< Date d1(2022,7,7); Date d2(2022,7,8); cout <<Less(d1, d2)<< endl;// ---------------- 指针类型比较(潜在问题)----------------// 3. 实例化 Less<Date*>(Date*, Date*)// 调用指针类型的 < 运算符(比较内存地址) Date* p1 =&d1;// p1 指向 d1 的内存地址 Date* p2 =&d2;// p2 指向 d2 的内存地址 cout <<Less(p1, p2)<< endl;// 可能输出 0 或 1(取决于内存地址的随机分配)//注意:此处本意是比较对象内容,但实际比较的是指针地址return0;}可以看到,Less函数在大多数情况下都能正常比较,但在特殊场景中会得出错误结果。
在上述示例里:p1指向的d1显然小于p2指向的d2对象然而Less内部并没有比较p1和p2所指向对象的内容而是比较了p1和p2指针的地址,这就无法达成预期,进而出现错误(哈哈,虽然博主在自己的VS上演示的结果没有出错,但是并不代表它没有问题呦)
这时,就需要对模板进行特化处理。也就是在原模板的基础上,针对特定类型,进行专门化的实现。
3. 模板特化有哪些?
模板特化的分类:
C++ 的模板特化可分为:函数模板特化和类模板特化两大类。
一、函数模板特化
函数模板特化的步骤
1. 先定义基础函数模板要特化函数模板,得先有一个通用的基础函数模板,它为各类型提供默认的泛型逻辑。这个模板能处理int、double、自定义类(若重载了<运算符 )等类型的比较,但遇到指针类型时会因比较地址而非内容出问题,这就需要特化。
2. 添加特化声明与实现特化标识:用template<>表明这是一个模板特化(空尖括号表示不再推导模板参数 )明确特化类型:在函数名后的尖括号里,写上要专门处理的特定类型(如:Date*类型,就写Less<Date*>)保持形参匹配:特化函数的形参列表,必须和基础函数模板的形参类型严格一致,否则编译器可能报错或匹配异常
比如:我们想实现 “比较两个值大小,返回 bool 结果” 的功能,先写通用模板:
template<classT>boolLess(T left, T right){return left < right;}前面我们提到,Less函数在比较p1和p2时,内部并未比较它们所指向对象的内容,而是直接比较了指针的地址,这与预期不符,会导致错误。
此时,需要通过模板特化来解决这一问题。
既然已经了解了模板特化的方法,接下来我们就按照上面的步骤对代码进行特化处理吧!
#include<iostream>usingnamespace std;//任务1:定义一个日期类classDate{public:/*--------------成员变量--------------*/int _year;int _month;int _day;/*--------------成员函数--------------*///1.实现:“默认构造函数”Date(int y,int m,int d):_year(y),_month(m),_day(d){}//2.实现:“<运算符重载函数”booloperator<(const Date& other)const//注意:用于比较两个日期的先后顺序{if(_year != other._year)return _year < other._year;// 优先比较年份if(_month != other._month)return _month < other._month;// 年份相同则比较月份return _day < other._day;// 年份和月份都相同则比较日期}};//任务2:定义比较函数模板Lesstemplate<classT>boolLess(T left, T right){return left < right;//注意:依赖 T 类型的 operator< 实现}//任务3:对Less函数模板进行特化template<>bool Less<Date*>(Date* left, Date* right){return*left <*right;}intmain(){// ---------------- 基础类型比较(正确行为)----------------// 1. 实例化 Less<int>(int, int)// 调用内置的 int 类型的 < 运算符 cout <<Less(1,2)<< endl;// ---------------- 对象类型比较(正确行为)----------------// 2. 实例化 Less<Date>(Date, Date)// 调用 Date 类重载的 operator< Date d1(2022,7,7); Date d2(2022,7,8); cout <<Less(d1, d2)<< endl;// ---------------- 指针类型比较(潜在问题)----------------// 3. 实例化 Less<Date*>(Date*, Date*)// 调用指针类型的 < 运算符(比较内存地址) Date* p1 =&d1;// p1 指向 d1 的内存地址 Date* p2 =&d2;// p2 指向 d2 的内存地址 cout <<Less(p1, p2)<< endl;//注意:调用特化之后的版本了,而不是走通用模板了return0;}函数模板全特化
函数模板全特化:为函数模板的所有参数显式指定类型,完全覆盖通用逻辑。示例:通用函数模板用于比较两个值的大小,但对const char*(C 风格字符串),默认会比较指针地址而非内容,所以需要特化:
语法:
template<>//函数模板特化 返回类型 模板函数名<特化类型>(参数列表){...}#include<iostream>#include<string>usingnamespace std;/*--------------------- 通用模板(求最大值)---------------------*/template<typenameT> T max_val(T a, T b){return a > b ? a : b;}/*------------------ 针对const char*类型的全特化 ------------------*/template<>// 函数模板全特化constchar* max_val<constchar*>(constchar* a,constchar* b)//按字符串字典序比较{returnstrcmp(a, b)>0? a : b;// 使用 C 风格字符串比较//注意:strcmp 返回值:a > b 则 >0,a < b 则 <0,相等则 0}intmain(){/*----------------调用通用模板:比较int值----------------*/ cout <<"-----调用通用模板:比较int值-----"<< endl; cout <<max_val(10,20)<< endl;/*---------------------- 调用全特化版本:比较字符串的内容 ----------------------*/ cout <<"-----调用全特化版本:比较字符串的内容-----"<< endl;constchar* s1 ="apple";constchar* s2 ="banana"; cout <<max_val(s1, s2)<< endl;//注意:自动匹配特化版本return0;}函数模板偏特化
注意:C++不直接支持函数模板偏特化(语法会报错),但可通过函数重载模拟类似效果。示例:让max_val对指针类型,比较指针指向的值,而非指针地址,通过重载实现:指针类型的 “偏特化”
#include<iostream>usingnamespace std;// 通用函数模板:比较值template<classT> T max_val(T a, T b){return a > b ? a : b;}// 重载版本:针对指针类型(模拟偏特化)template<classT> T*max_val(T* a, T* b){return*a >*b ? a : b;}intmain(){int x =10, y =20;// 调用通用模板:比较 int 值 cout <<max_val(5,3)<< endl;// 调用重载的指针版本:比较 *x 和 *yint* result =max_val(&x,&y); cout <<*result << endl;return0;}原理:重载的max_val(T* a, T* b)并非严格意义的 “偏特化”,但利用函数重载决议,优先匹配指针类型的调用,达到 “针对部分类型定制” 的效果。若直接写函数模板偏特化语法(如:template <class T> T max_val<T*>(T* a, T* b) { ... }),编译器会报错,因此实际开发常用重载替代。
注意:虽然这种方式严格一点说并不能算作是函数模板特化,但是其实现简单、可读性高且易于书写。对于参数类型复杂的函数模板,使用特化反而可能会变得繁琐且容易出错,因此,不建议对函数模板进行特化,而应优先考虑使用函数重载或类模板特化来替代。
二、类模板特化
类模板特化:为类模板的特定参数,定制类的实现,又分为:全特化和偏特化类模板全特化
类模板全特化:为类模板的所有参数显式指定类型,完全替换通用类的实现。示例:假设有通用类模板MyContainer,为T=int, N=10全特化:
语法:
template<>//空模板参数列表,表示特化class 模板类名<特化类型>//类模板特化{...};#include<iostream>usingnamespace std;// 通用类模板:存储 T 类型,容量为 Ntemplate<classT, size_t N =10>classMyContainer{public:MyContainer(){ cout <<"通用类模板构造"<< endl;}voidprint(){ cout <<"通用容器:存储类型 T,容量 "<< N << endl;}};// 类模板全特化:针对 T=int,N=10template<>classMyContainer<int,10>{public:MyContainer(){ cout <<"全特化类构造(int, 10)"<< endl;}voidprint(){ cout <<"特化容器:专门存储 int,容量 10(定制逻辑)"<< endl;}};intmain(){ cout <<"-----------调用通用类模板-----------"<< endl; MyContainer<double,5> c1; c1.print(); cout <<"-----------调用全特化类-----------"<< endl; MyContainer<int,10> c2; c2.print();return0;}关键说明:全特化类的实现与通用类完全独立,可自定义构造函数、成员函数等。当实例化 MyContainer<int, 10> 时,编译器优先选择全特化版本。类模板偏特化
类模板偏特化:对模板参数的部分类型或特定条件进行特化,而非全部参数。
偏特化适用场景:参数数量特化:如模板有两个参数,特化其中一个。参数范围特化:如特化指针类型、引用类型、const 类型等。
第一种场景:针对模板参数数量的偏特化
/*------------------------通用模板(两个参数)------------------------*/template<typenameT1,typenameT2>classMyClass{...};/*----------------------偏特化为第一个参数固定为int---------------------*/template<typenameT2>// 保留第二个参数 T2classMyClass<int, T2>{...};// 特化第一个参数为 int第二种场景:针对模板参数范围的偏特化/*----------------------------通用模板----------------------------*/template<typenameT>classMyClass{public:voidprint(T value){ cout <<"通用类型: "<< value << endl;}};/*------------------------针对指针类型的偏特化-------------------------*/template<typenameT>// 仍保留一个模板参数 T,表示指针指向的类型classMyClass<T*>// 特化指针类型{public:voidprint(T* ptr){ cout <<"指针地址: "<< ptr << endl;}};/*---------------------- 针对const类型的偏特化-----------------------*/template<typenameT>classMyClass<const T>// 特化 const T 类型{// 处理 const 类型的逻辑};/*----------------------------- 调用 -------------------------------*/ MyClass<int> obj1;// 通用类型,int obj1.print(10);// 输出:通用类型: 10 MyClass<int*> obj2;// 特化指针类型,int*int x =20; obj2.print(&x);// 输出:指针地址: 0x7fff...示例:指针类型的偏特化:让MyContainer对指针类型T*,定制存储逻辑(如:打印指针地址而非值)
#include<iostream>usingnamespace std;// 通用类模板:存储 T 类型,容量为 Ntemplate<classT, size_t N =10>classMyContainer{public:MyContainer(){ cout <<"通用类模板构造"<< endl;}voidprint(){ cout <<"通用容器:存储类型 T,容量 "<< N << endl;}};// 类模板偏特化:针对 T 是指针类型(T*)template<classT, size_t N>classMyContainer<T*, N>{public:MyContainer(T* data):_data(data){ cout <<"偏特化类构造(指针类型)"<< endl;}voidprint(){ cout <<"存储指针:地址 = "<< _data << endl;}private: T* _data;};intmain(){ cout <<"-----------调用通用类模板-----------"<< endl; MyContainer<double,5> c1; c1.print(); cout <<"-----------调用指针类型的偏特化-----------"<< endl;int x =100; MyContainer<int*>c2(&x);// 调用偏特化类(T=int*, N=默认 10) c2.print();return0;}说明:偏特化后,MyContainer<T*, N>中的T仍为泛型(如:int),N也保留默认值,但约束了T必须是指针类型。实例化MyContainer<int*>时,自动匹配偏特化版本。
回顾与总结:Less函数在比较p1和p2(指针)时,内部没有比较它们指向对象的实际内容,而是直接比较了指针的地址,这与我们想要比较对象内容的预期不符,会引发错误。这时,我们可以用模板特化解决问题,所以之前我们尝试过用函数模板全特化处理但是后来我们提到过:对于参数类型复杂的函数模板,特化过程容易变得繁琐、易错。所以,不建议直接对函数模板特化,更推荐优先用函数重载或类模板特化替代。
前面我们已经演示了用函数重载解决指针比较问题,接下来就用类模板特化的方式,处理这个场景,让比较逻辑符合预期。
#include<vector>#include<algorithm>usingnamespace std;//任务1:定义一个日期类classDate{public:/*--------------成员变量--------------*/int _year;int _month;int _day;/*--------------成员函数--------------*///1.实现:“默认构造函数”Date(int y,int m,int d):_year(y),_month(m),_day(d){}//2.实现:“<运算符重载函数”booloperator<(const Date& other)const//注意:用于比较两个日期的先后顺序{if(_year != other._year)return _year < other._year;// 优先比较年份if(_month != other._month)return _month < other._month;// 年份相同则比较月份return _day < other._day;// 年份和月份都相同则比较日期}};//任务2:定义比较类模板Less(仿函数)template<classT>structLess{//1.重载()运算符,使该结构体可像函数一样调用booloperator()(const T& x,const T& y)const{return x < y;//注意:依赖 T 类型的 operator< 实现}};//任务3:比较类模板Less按照指针方式特化template<>structLess<Date*>{booloperator()(Date* x, Date* y)const{return*x <*y;}};intmain(){// 创建三个日期对象(d2 < d1 < d3) Date d1(2022,7,7); Date d2(2022,7,6); Date d3(2022,7,8);// ---------------- 存储对象的 vector 排序(正确行为)---------------- vector<Date> v1; v1.push_back(d1); v1.push_back(d2); v1.push_back(d3);// 使用 Less<Date> 作为比较器// 实例化 Less<Date>,调用 Date::operator< 比较对象内容sort(v1.begin(), v1.end(), Less<Date>());// ---------------- 存储指针的 vector 排序(潜在问题)---------------- vector<Date*> v2; v2.push_back(&d1); v2.push_back(&d2); v2.push_back(&d3);// 使用特化模板 Less<Date*> 作为比较器// 比较的是指针指向的对象内容(*x < *y),而非指针地址sort(v2.begin(), v2.end(), Less<Date*>());return0;}错误情况:若未进行类模板Less的指针方式特化处理
正确情况:若进行了类模板Less的指针方式特化处理
------------分离编译------------
什么是分离编译?
分离编译:是一种软件开发中的编译策略,指将程序的不同部分(如:不同的源文件)分别编译为目标代码(.obj或.o文件),最终再通过链接器将这些目标文件和依赖的库文件合并成可执行程序或库文件的过程。
核心思想:
将大型程序拆解为多个独立编译单元(源文件),每个单元单独编译,减少重复编译的开销,提高开发效率。
关键机制:声明与定义分离:头文件(.h)放声明,源文件(.cpp)放实现编译单元:每个.cpp文件及其包含的头文件构成独立编译单元符号决议:链接器负责解决跨文件的函数/变量引用
模板的分离编译要注意什么事情?
模板的分离编译是 C++ 中一个容易引发错误的复杂问题,主要源于模板实例化机制与传统分离编译模型的不兼容。
传统分离编译流程:编译器独立处理每个.cpp文件,生成对应的.obj文件链接器将所有.obj文件合并,解析未定义的符号(如:函数调用)
模板实例化机制:模板代码(如:template <class T> void func(T x))本身不是完整代码需要在使用时根据实参类型(如:int)实例化出具体代码(如:void func(int x))
矛盾点:若模板定义(如:func的实现)放在.cpp文件中,编译器编译该文件时无法得知未来会被哪些类型实例化,因此不会生成具体代码当其他文件(如:main.cpp)使用该模板时,编译器只能看到模板声明,无法找到对应实例化的定义,导致链接错误
常见错误示例:错误的分离编译结构
/*--------------------------func.h(声明模板)--------------------------*/template<classT>voidfunc(T x);/*--------------------------func.cpp(定义模板)--------------------------*/#include"func.h"template<classT>voidfunc(T x){/* ... */}/*--------------------------main.cpp(使用模板)--------------------------*/#include"func.h"intmain(){func(42);// 编译时找不到 func<int> 的定义,链接错误!return0;}怎么解决模板分离编译时带来的问题?
解决模板分离编译问题的方法主要有两种:将模板定义放在头文件中使用显式实例化
最推荐第一种方法。毕竟,解决模板分离编译问题的核心就是:让编译器在实例化模板时能同时看到声明与定义。将声明和定义写在同一个头文件中,从根源上避免分离编译带来的符号解析问题,是最简单直接且兼容性最好的方案。
1. 将模板定义放在头文件中原理:让编译器在使用模板的编译单元(如:main.cpp)时同时看到声明和定义,直接实例化代码。优点:简单直接,无需额外操作。缺点:头文件包含实现细节,可能导致代码膨胀。修改实现需重新编译所有包含该头文件的源文件。
2. 使用显式实例化原理:在模板定义文件中显式指定需要实例化的类型,强制编译器生成对应代码。优点:保持分离编译结构,避免头文件包含实现。缺点:需预先知道所有会被使用的实例化类型,不灵活。新增类型需修改.cpp文件并重新编译。
示例:
/*---------------------------func.h(声明模板)---------------------------*/template<classT>voidfunc(T x);/*---------------------------func.cpp(定义模板并显式实例化)---------------------------*/#include"func.h"//1.定义模板template<classT>voidfunc(T x){/* ... */}//2.显式实例化 int 类型templatevoid func<int>(int);/*---------------------------main.cpp(使用模板)---------------------------*/#include"func.h"intmain(){func(42);// 使用已显式实例化的 func<int>return0;}示例:
/*---------------------------func.h---------------------------*/template<classT>voidfunc(T x){/* 直接在头文件中定义 */}/*---------------------------main.cpp---------------------------*/#include"func.h"intmain(){func(42);// 编译器在此处实例化 func<int>return0;}