尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【c++学习笔记】【基础】9.模板初阶:从函数模板到类模板

【c++学习笔记】【基础】9.模板初阶:从函数模板到类模板 前言在 C 中我们经常会遇到这样的代码voidSwap(intleft,intright);voidSwap(doubleleft,doubleright);voidSwap(charleft,charright);这些函数处理的数据类型不同但完成的事情完全相同交换两个变量的值。函数重载虽然能够解决问题但如果以后还需要支持float、long等更多类型就要继续编写新的重载。代码逻辑没有变化却因为类型不同被重复实现了很多次。于是就产生了一个很自然的问题能不能只写一份代码把具体的数据类型也抽象出来让同一套逻辑适用于不同类型这就是 C模板Template要解决的问题。模板是 C 实现泛型编程的重要基础。它允许我们先编写一份与具体类型无关的通用代码再由编译器根据实际使用的类型生成对应的具体代码。本文主要围绕模板最基础的两个部分展开函数模板让同一套函数逻辑适用于不同类型类模板让同一套类结构适用于不同类型。在这个过程中还需要理解模板参数、模板实例化、类型推导以及函数模板的匹配规则。整篇文章可以沿着这样一条主线来理解重复代码 ↓ 把类型抽象出来 ↓ 泛型编程 ↓ 模板 ├── 函数模板 └── 类模板 ↓ 模板实例化 ↓ 得到具体代码一、为什么需要模板模板出现的根本原因是为了减少一类特殊的重复代码代码逻辑完全相同只有处理的数据类型不同。还是以交换函数为例voidSwap(intleft,intright){inttempleft;leftright;righttemp;}voidSwap(doubleleft,doubleright){doubletempleft;leftright;righttemp;}仔细观察会发现两份代码真正发生变化的只有类型int → double交换的逻辑并没有发生任何变化。如果能够把这里的类型也抽象成一个参数那么就不需要针对每一种类型重新写一遍函数。于是可以写成templatetypenameTvoidSwap(Tleft,Tright){T templeft;leftright;righttemp;}这里的T不再代表某一种确定的数据类型而是一个模板参数。调用inta10;intb20;Swap(a,b);此时T对应int。如果调用doublex1.1;doubley2.2;Swap(x,y);此时T对应double。因此只需要维护一份交换逻辑就能够适用于不同的数据类型。这种编写与具体类型无关的通用代码的方式就是泛型编程。而模板就是 C 实现泛型编程的重要工具。本节小结函数重载可以让同名函数处理不同类型但当多个函数只有类型不同、逻辑完全相同时会产生大量重复代码。模板进一步把类型本身也参数化不同类型 相同逻辑 ↓ 模板 ↓ 一份通用代码二、函数模板把类型也变成参数函数模板解决的是函数逻辑相同但参与运算的数据类型不同。普通函数中的参数可以让传入的“数据”发生变化而模板进一步把“类型”也抽象成参数。1. 函数模板的基本格式函数模板的基本形式为templatetypenameT返回值类型 函数名(参数列表){// 函数实现}例如templatetypenameTvoidSwap(Tleft,Tright){T templeft;leftright;righttemp;}其中templatetypenameT表示声明一个模板参数T。接下来在函数中出现的T就代表这个暂时还没有确定的类型。模板参数也可以不止一个templatetypenameT1,typenameT2T1Add(T1 left,T2 right){returnleftright;}此时T1 和 T2可以分别表示不同的类型。2.typename和class声明类型模板参数时可以写templatetypenameT也可以写templateclassT例如templateclassTvoidSwap(Tleft,Tright){T templeft;leftright;righttemp;}在这里typename和class的作用相同都表示T是一个类型模板参数。需要注意的是templatestructT不能用来替代这里的typename或class。本节小结函数模板可以简单理解为先用模板参数代替具体类型描述一套通用的函数逻辑。例如templatetypenameTvoidSwap(Tx,Ty);此时T还没有确定它究竟是int、double还是其他类型要等真正使用模板时才能知道。三、函数模板是如何变成具体函数的定义templatetypenameTTAdd(constTleft,constTright){returnleftright;}并不意味着程序中已经存在一个确定类型的Add函数。因为此时T ?还是未知的。真正调用inta10;intb20;Add(a,b);编译器才可以根据实参确定T int然后根据函数模板生成对应类型的函数。从理解原理的角度可以把生成的函数看成intAdd(constintleft,constintright){returnleftright;}如果又调用doublex1.1;doubley2.2;Add(x,y);则T double编译器会生成对应double类型的版本。整个过程可以理解为AddT │ ┌────────────┴────────────┐ ▼ ▼ Add(1, 2) Add(1.1, 2.2) │ │ Tint Tdouble │ │ ▼ ▼ Addint Adddouble因此需要建立一个很重要的认识函数模板本身不是某一个具体类型的函数而是编译器生成具体函数的一份模板。根据模板生成对应具体函数的过程就是模板实例化。本节小结模板帮我们减少的是“重复编写代码”的工作而不是让所有类型真的共用同一个具体函数。可以简单理解为函数模板 ↓ 确定模板参数 ↓ 模板实例化 ↓ 生成具体函数这也是理解后面模板参数推导的基础。四、模板实例化模板参数是怎么确定的模板要生成具体函数就必须先确定模板参数。例如templateclassTTAdd(constTleft,constTright){returnleftright;}这里的T最终必须变成一个具体类型。根据模板参数的确定方式可以分为隐式实例化和显式实例化。1. 隐式实例化隐式实例化就是让编译器根据传入的实参自己推导模板参数。例如inta110;inta220;Add(a1,a2);编译器根据两个参数得到a1 → int a2 → int 因此 T int于是实例化出int类型的函数。同理doubled11.1;doubled22.2;Add(d1,d2);可以得到T double这就是模板参数的自动推导。2. 为什么Add(int, double)会出现问题继续使用templateclassTTAdd(constTleft,constTright){returnleftright;}现在调用inta10;doubled20.0;Add(a,d);编译器推导T时会得到两个结果根据 a T int 根据 d T double但函数模板中只有一个TAdd(constTleft,constTright)同一个模板参数无法在这次推导中同时被确定成int和double因此模板参数推导失败。这里最容易产生一个误区int和double明明可以做加法为什么模板不能调用问题其实不在运算。真正的问题发生得更早编译器首先要确定 T 是什么但两个实参给出了不同的推导结果。因此模板参数推导阶段首先需要解决的是类型是否能够推导一致。3. 显式指定模板参数既然问题出在编译器不知道T应该选择哪个类型那么也可以直接告诉它Addint(a,d);这里int明确指定T int于是编译器不再需要根据两个实参推导T。如果实际参数和指定的模板参数类型不同只要能够完成相应的类型转换就可以继续调用。因此可以把两种实例化方式简单区分成Add(a, b) ↓ 编译器根据实参推导 T ↓ 隐式实例化 Addint(a, b) ↓ 程序员明确指定 T ↓ 显式实例化本节小结模板实例化解决的是如何从通用模板得到具体函数其中隐式实例化 → 编译器根据实参推导模板参数 显式实例化 → 程序员主动指定模板参数特别要注意模板参数推导失败并不一定意味着某个运算本身不能进行也可能只是编译器无法得到唯一一致的模板参数。五、函数模板的匹配到底调用哪一个有了函数模板以后同一个函数名下面可能同时存在普通函数和函数模板。例如intAdd(intleft,intright){returnleftright;}templateclassTTAdd(T left,T right){returnleftright;}这时调用Add(1,2);就产生了一个问题普通函数和函数模板都能处理这两个参数到底应该选择谁1. 匹配程度相同时优先普通函数对于Add(1,2);普通函数intAdd(int,int);可以直接匹配。函数模板同样可以实例化出Addint(int,int);两者都能够很好地匹配。这种情况下编译器会优先使用已经存在的普通函数。如果明确写Addint(1,2);则表示主动使用函数模板生成的版本。2. 模板也可能提供更好的匹配因此不能简单把规则记成“只要普通函数存在就一定调用普通函数。”真正需要考虑的是匹配程度。如果普通函数需要进行类型转换而函数模板能够根据实参生成一个更加合适的版本那么模板也可能被选择。所以更准确的理解是普通函数和函数模板匹配程度相同时优先选择普通函数如果函数模板能够产生更加匹配的版本则选择更合适的版本。本节小结函数模板的匹配规则不需要死记很多情况当前阶段先抓住一个核心编译器会寻找更加合适的匹配。其中普通函数和函数模板匹配程度相同时优先普通函数而不是“任何情况下普通函数都优先”。六、类模板把泛型从函数扩展到类前面解决的都是函数中的重复问题相同函数逻辑 不同数据类型 ↓ 函数模板但相同的问题也会出现在类中。例如实现一个栈如果只能保存intclassStack{private:int*_array;size_t _capacity;size_t _size;};以后又希望保存double如果重新实现一个DoubleStack那么栈的扩容、入栈等逻辑都需要再写一遍。实际上栈的结构和操作逻辑没有变化变化的仍然只是栈中保存的数据类型。因此可以把同样的泛型思想扩展到类相同类结构 不同数据类型 ↓ 类模板1. 类模板的基本格式类模板的基本形式为templateclassT1,classT2,...,classTnclass类模板名{// 成员};例如templatetypenameTclassStack{public:Stack(size_t capacity4){_arraynewT[capacity];_capacitycapacity;_size0;}voidPush(constTdata);private:T*_array;size_t _capacity;size_t _size;};原来固定的int*_array;变成T*_array;也就是说Stack不再提前规定自己只能保存哪一种类型。2. 类模板的成员函数如果成员函数直接定义在类中正常使用模板参数即可。如果在类外定义则需要重新声明模板参数templateclassTvoidStackT::Push(constTdata){_array[_size]data;_size;}这里需要注意StackT::Push而不是Stack::Push因为当前描述的是StackT这个类模板对应的成员函数。3. 类模板的实例化函数模板在调用时可以通过函数实参推导模板参数。类模板则需要在使用时明确给出具体类型Stackintst1;Stackdoublest2;这里Stackint表示使用int实例化Stack而Stackdouble表示使用double实例化Stack。需要特别区分Stack ↓ 类模板 Stackint ↓ 实例化后的具体类型因此Stackintst1;Stackdoublest2;得到的是两个不同的具体类型。整个过程可以理解为StackT │ ┌─────────┴─────────┐ ▼ ▼ Stackint Stackdouble │ │ 保存 int 保存 double本节小结类模板和函数模板的核心思想其实完全一致函数模板 → 把函数中变化的类型抽象出来 类模板 → 把类中变化的类型抽象出来而且需要特别记住Stack是类模板Stackint才是模板实例化后得到的具体类型。七、模板的声明和定义普通函数或者普通类通常可以把声明和定义分开.h → 声明 .cpp → 定义模板在使用时涉及根据具体类型进行实例化因此模板的组织方式和普通函数、普通类存在一定区别。例如templatetypenameTclassStack{public:voidPush(constTdata);};成员函数在类外定义时templatetypenameTvoidStackT::Push(constTdata){// ...}在实际使用模板时通常不建议简单按照普通代码的方式把模板声明放在.h中再把模板定义单独放进.cpp中否则可能出现链接错误。当前阶段先记住这个使用特点即可模板的定义通常需要和模板声明放在一起使模板在实例化时能够看到完整定义。模板为什么会产生这种现象与模板实例化以及 C 的编译、链接过程有关可以在后续继续深入理解。本节小结模板与普通函数、普通类在代码组织上有所不同。当前阶段最重要的是知道普通代码 → 经常声明、定义分离 模板代码 → 通常让模板定义保持可见不要简单照搬普通类.h .cpp的组织方式。八、总结回过头来看模板要解决的核心问题其实非常简单当代码逻辑不变只有数据类型发生变化时不应该为每一种类型重复编写一份代码。C 通过模板把“类型”也抽象成参数从而实现泛型编程。函数模板解决的是函数逻辑的复用函数模板 ↓ 模板参数推导 / 指定 ↓ 模板实例化 ↓ 得到具体函数其中模板参数可以通过两种方式确定隐式实例化 → 编译器根据实参推导 显式实例化 → 程序员主动指定如果普通函数和函数模板同时存在则需要比较匹配程度多个候选函数 ↓ 比较匹配程度 ↓ 选择更加合适的版本普通函数和函数模板匹配程度相同时优先使用普通函数。类模板则把相同的思想从函数扩展到了类类模板 ↓ 指定具体类型 ↓ 模板实例化 ↓ 得到具体类型例如Stack → 类模板 Stackint → 具体类型 Stackdouble → 另一个具体类型因此整个模板初阶的知识可以压缩成下面这张图泛型编程 │ 编写与具体类型无关的代码 │ ▼ 模板 │ ┌────────────┴────────────┐ ▼ ▼ 函数模板 类模板 │ │ 确定模板参数 指定类型 │ │ ┌───────┴───────┐ │ ▼ ▼ │ 隐式推导 显式指定 │ │ │ │ └───────┬───────┘ │ ▼ ▼ 模板实例化 模板实例化 │ │ ▼ ▼ 具体函数 具体类型如果以后回来复习这一部分不需要先去背templatetypename T的语法。先回忆一句话模板的本质是先编写一份与具体类型无关的通用代码再根据实际使用的类型实例化出具体代码。围绕这句话再向下回忆为什么需要模板 ↓ 减少只有类型不同的重复代码 函数模板怎么使用 ↓ 模板参数 模板实例化 T 怎么确定 ↓ 隐式推导 / 显式指定 多个函数都能调用怎么办 ↓ 比较匹配程度 类怎么实现泛型 ↓ 类模板 Stack 和 Stackint 有什么区别 ↓ 一个是类模板一个是具体类型把这条主线建立起来函数模板、模板实例化、参数匹配和类模板就不再是几个零散的语法点而是一套完整的泛型编程逻辑。
返回列表