ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++ 模板初阶:函数模板与类模板详解

C++ 模板初阶:函数模板与类模板详解 C 模板初阶函数模板与类模板详解1. 泛型编程在实际编程中经常会遇到这样的情况算法完全一样只是处理的数据类型不同例如实现交换函数voidSwap(intleft,intright){inttempleft;leftright;righttemp;}voidSwap(doubleleft,doubleright){doubletempleft;leftright;righttemp;}voidSwap(charleft,charright){chartempleft;leftright;righttemp;}这几个函数的逻辑完全一样唯一的区别就是数据类型不同这种写法存在两个明显问题代码重复出现一种新的类型就需要重新写一个函数维护成本高大量重复代码容易出现修改不一致的问题因此C提供了模板泛型编程泛型编程就是编写与具体类型无关的通用代码它的核心思想是把类型作为参数让同一套代码能够处理不同的数据类型模板就是实现泛型编程的重要工具可以把模板理解成一个“代码模具”模板 ↓ 填入具体类型 ↓ 生成对应类型的代码例如Swap模板 ↓ 填入 int ↓ Swap(int, int) Swap模板 ↓ 填入 double ↓ Swap(double, double) Swap模板 ↓ 填入 char ↓ Swap(char, char)这样原本需要程序员重复编写的代码可以交给编译器完成2. 函数模板2.1 函数模板的概念函数模板可以理解为一个函数家族它本身并不是一个真正用于执行的具体函数而是一个生成具体函数的模板例如templateclassTvoidSwap(Tleft,Tright){T templeft;leftright;righttemp;}这里的T就是一个模板参数当我们使用inta10;intb20;Swap(a,b);编译器可以根据a、b的类型推导T → int于是可以生成一个针对int的Swap如果doublea1.1;doubleb2.2;Swap(a,b);那么T → double从而生成针对double的版本2.2 函数模板的基本格式templatetypenameT1,typenameT2,...,typenameTn返回值类型 函数名(参数列表){// 函数体}最常见的形式templatetypenameTvoidSwap(Tleft,Tright){T templeft;leftright;righttemp;}其中templatetypenameT表示下面定义的是一个模板T是模板参数2.3typename和class定义模板参数时templatetypenameT也可以写成templateclassT两者在这里含义相同例如templatetypenameTvoidSwap(Ta,Tb){T tmpa;ab;btmp;}等价于templateclassTvoidSwap(Ta,Tb){T tmpa;ab;btmp;}但是不能写成templatestructT模板类型参数这里使用的是typename或class3. 函数模板的原理理解这一部分非常重要函数模板templateclassTTAdd(constTleft,constTright){returnleftright;}它本身不是一个具体函数可以把它看成一份“函数生成规则”例如inta10;intb20;Add(a,b);编译器看到实参以后会推导T int相当于产生一个处理int的具体版本如果doublea10.0;doubleb20.0;Add(a,b);则T double产生处理double的具体版本所以模板的核心思想就是程序员只负责写一份通用代码编译器根据实际使用情况生成具体版本这个过程发生在编译阶段4. 函数模板的实例化使用不同类型的参数调用函数模板时就会发生函数模板实例化函数模板实例化主要分为隐式实例化显式实例化4.1 隐式实例化隐式实例化就是由编译器根据函数实参自动推导模板参数例如templateclassTTAdd(constTleft,constTright){returnleftright;}调用inta110;inta220;doubled110.0;doubled220.0;Add(a1,a2);Add(d1,d2);第一次Add(a1,a2);根据a1 → int a2 → int推导T → int第二次Add(d1,d2);推导T → double所以可以分别生成Addint Adddouble4.2 一个容易出错的情况例如inta10;doubled20.0;Add(a,d);如果模板只有一个参数templateclassTTAdd(constTleft,constTright){returnleftright;}那么编译器会遇到第一个参数 a → int 第二个参数 d → double此时同一个T到底应该是什么T int T double无法确定因此无法正常推导所以这种调用不能依靠这种形式的模板参数推导4.3 解决方法一强制类型转换可以让两个参数变成相同类型Add(a,(int)d);此时a → int (int)d → int于是T → int模板就可以实例化4.4 解决方法二显式实例化也可以直接告诉编译器T 就是int写法Addint(a,d);这里的int就是明确指定模板参数编译器知道T int然后再处理double→int如果能够进行转换就可以完成调用如果无法转换则编译报错5. 模板参数的匹配原则函数模板和普通函数可以同时存在例如intAdd(intleft,intright){returnleftright;}templateclassTTAdd(T left,T right){returnleftright;}此时Add(1,2);既可以匹配普通函数intAdd(int,int);也可以让模板实例化出Addint(int,int);但是在其他条件相同的情况下Add(1,2);会优先匹配普通函数也就是说普通函数和模板都能完全匹配时优先使用普通函数如果模板能够产生一个匹配程度更高的版本则可能选择模板5.1 显式指定模板参数如果明确写Addint(1,2);那么就是明确要求使用模板并指定T int因此Add(1,2);和Addint(1,2);含义并不完全相同前者会参与普通函数与模板之间的匹配后者明确指定使用模板6. 模板中的类型转换模板参数推导时编译器一般不会为了让模板匹配而随意进行类型转换例如templateclassTTAdd(T left,T right){returnleftright;}然后inta10;doubleb20.0;Add(a,b);此时T int和T double产生冲突不能简单地认为int 自动转换成 double然后就可以完成模板参数推导如果确实希望使用某种类型可以明确指定Adddouble(a,b);或者Add(a,(int)b);核心要区分普通函数调用 ↓ 可以发生常规的类型转换 模板参数推导 ↓ 不会依靠这种类型转换来解决模板参数推导冲突7. 多模板参数模板不一定只能有一个类型参数例如templateclassT1,classT2T1Add(T1 left,T2 right){returnleftright;}这里有两个模板参数T1 T2调用Add(1,2.0);编译器可以推导T1 → int T2 → double因此这种情况下就不会出现前面“两个实参争夺同一个T”的问题8. 类模板函数模板解决的是函数通用化问题而类模板解决的是类通用化问题例如实现一个栈栈的基本结构无论存储什么数据整体逻辑都可以保持一致可能存int double char如果每种类型都重新写一个Stack会产生大量重复代码因此可以使用类模板8.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;};这里T*_array;表示栈底层数组中的数据类型由T决定如果T int那么相当于int*_array;如果T double那么相当于double*_array;因此同一个类模板可以生成不同类型的栈9. 类模板成员函数的定义如果类模板的成员函数在类外定义需要注意模板写法例如类内声明templateclassTclassStack{public:voidPush(constTdata);private:T*_array;size_t _capacity;size_t _size;};类外定义templateclassTvoidStackT::Push(constTdata){_array[_size]data;_size;}这里有两个非常容易混淆的地方templateclassT表示这是一个模板而StackT表示当前定义的是StackT这个类模板的成员函数所以完整结构templateclassTvoidStackT::Push(constTdata){// ...}10. 类模板为什么通常不建议声明和定义分离类模板的成员函数通常不建议像普通类那样Stack.h Stack.cpp把声明和定义完全分开例如// Stack.htemplateclassTclassStack{public:voidPush(constTdata);};然后// Stack.cpptemplateclassTvoidStackT::Push(constTdata){// ...}这种写法容易产生链接错误原因在于模板真正需要的是使用某个具体类型实例化模板时编译器需要看到模板的完整定义例如Stackintst;st.Push(10);编译器需要根据T int生成Stackint::Push(...)如果当前编译单元只看到了模板声明却看不到完整的模板定义就无法正常完成对应的实例化因此实际编程中通常直接把模板的声明和定义放在一起或者放在.h/.hpp中11. 类模板的实例化类模板和函数模板的实例化方式有一个重要区别函数模板可以通过函数参数进行类型推导Swap(a,b);而类模板实例化时必须明确指定类型例如Stackintst1;Stackdoublest2;这里Stack只是类模板名并不是一个真正的具体类型真正的类型是Stackint和Stackdouble可以理解为Stack ↓ 类模板 Stackint ↓ 具体的 int 类型栈 Stackdouble ↓ 具体的 double 类型栈因此Stackintst1;表示使用 Stack 模板 T int 生成 Stackint 创建对象 st1而Stackdoublest2;表示使用 Stack 模板 T double 生成 Stackdouble 创建对象 st212. 函数模板与类模板的核心区别项目函数模板类模板作用生成通用函数生成通用类类型来源通常可以根据实参推导通常需要显式指定使用方式Add(a, b)Stackint st模板本身不是具体函数不是具体类实例化结果具体函数具体类常见用途通用算法、操作通用数据结构最需要记住的是templateclassTTAdd(T a,T b);这是函数模板而templateclassTclassStack{};这是类模板模板本身都是“模具”真正参与程序工作的是根据具体类型实例化出来的具体版本
返回列表