C++ 模板初阶:从重复代码到泛型编程

发布时间:2026/7/1 19:21:33
C++ 模板初阶:从重复代码到泛型编程 C 模板初阶从重复代码到泛型编程快速跳转前言 为什么需要模板 函数模板 模板原理 实例化 匹配规则 类模板 小结前言刚开始写 C 时我们很容易遇到一种尴尬情况逻辑明明一模一样只是类型不同代码却要写好几遍。比如交换两个变量int要写一份double要写一份char可能还要再写一份。写的时候感觉只是复制粘贴后面维护时就不一定这么舒服了。万一某个版本里写错了其他版本还得挨个检查。模板要解决的就是这类问题把和类型无关的逻辑抽出来让编译器根据实际类型去生成对应代码。简单说模板不是让程序运行时变“万能”而是让编译器在编译阶段帮我们少写重复代码。这篇先整理模板入门阶段最核心的几块学习点先抓住什么泛型编程为什么“类型不同但逻辑相同”的代码不适合一直重载函数模板怎么写出一份通用函数逻辑模板实例化编译器什么时候把模板变成具体函数匹配规则普通函数和函数模板同名时编译器怎么选类模板怎么让类的数据类型也变得通用先把这些基础问题理顺后面再看 STL、容器、迭代器、仿函数这些内容会轻松很多。一、先从重复代码说起如果没有模板我们想写一个通用的交换函数最直观的办法是函数重载voidSwapValue(intleft,intright){inttmpleft;leftright;righttmp;}voidSwapValue(doubleleft,doubleright){doubletmpleft;leftright;righttmp;}voidSwapValue(charleft,charright){chartmpleft;leftright;righttmp;}这段代码能用但问题也很明显逻辑高度重复只是类型不一样。新增一个类型就可能要新增一个函数。一旦交换逻辑要改每个重载版本都要跟着改。所以这里真正需要的不是“更多重载”而是一个能描述通用逻辑的模子。这就是泛型编程的思想写出和具体类型无关的代码让同一份逻辑适配不同类型。C 里的模板就是泛型编程最基础也最重要的工具。二、函数模板先写一个“函数模子”函数模板的基本格式是templatetypenameT返回值类型 函数名(参数列表){// 函数体}typename后面的T表示一个模板类型参数。这里的T不是固定类型它更像一个占位符等到真正调用函数时编译器再根据实参类型把它替换成具体类型。用模板改写前面的交换函数templatetypenameTvoidSwapValue(Tleft,Tright){T tmpleft;leftright;righttmp;}调用时可以这样写inta10;intb20;SwapValue(a,b);// T 被推导为 intdoublex1.1;doubley2.2;SwapValue(x,y);// T 被推导为 double这时我们写的只有一份模板代码但编译器会根据实际调用生成对应类型的函数版本。这里补一个小细节定义模板类型参数时typename和class都可以用。templateclassTvoidPrint(constTvalue){coutvalueendl;}在这种场景下class并不是说T必须是类类型内置类型也可以。只是关键字写法不同而已。不过不能把这里的class换成struct。三、模板本身不是函数这一点很重要函数模板本身不是一个真正能被调用的函数。它更像一张图纸。只有当我们用具体类型去调用它时编译器才会根据这张图纸生成一份真正的函数代码。比如templatetypenameTTSum(T lhs,T rhs){returnlhsrhs;}intmain(){Sum(1,2);// 生成 int Sum(int, int)Sum(1.5,2.5);// 生成 double Sum(double, double)}编译器看到Sum(1, 2)会推导出T是int于是生成一份处理int的函数。看到Sum(1.5, 2.5)又会推导出T是double于是生成一份处理double的函数。所以模板帮我们省下的是手动重复写这些函数的工作。真正的代码生成发生在编译阶段。四、函数模板的实例化用具体类型使用函数模板的过程叫做模板实例化。函数模板常见的使用方式有两种让编译器隐式推导类型或者由我们显式指定模板参数。1. 隐式实例化隐式实例化就是让编译器自己根据实参推导类型。templatetypenameTTSum(T lhs,T rhs){returnlhsrhs;}intmain(){inta10;intb20;Sum(a,b);// T 推导为 intdoublex1.1;doubley2.2;Sum(x,y);// T 推导为 double}这种写法最自然也是平时最常见的用法。但隐式推导有一个容易踩的点如果同一个模板参数从不同实参里推导出了不同类型编译器就不知道该听谁的。inta10;doubleb2.5;Sum(a,b);// 这里会出问题a希望T是intb希望T是double。可模板参数列表里只有一个T编译器不能擅自替我们决定所以这类调用通常会编译失败。这不是编译器“不会变通”而是模板推导阶段本来就比较严格。它要先把类型推导清楚不能一边推导一边随便做类型转换。2. 显式指定模板参数如果我们就是想指定模板参数类型可以在函数名后面加。Sumint(a,b);这表示明确告诉编译器这次T就按int处理。于是b会尝试转换成int后再参与调用。如果转换不合法还是会报错。也可以自己先做强制类型转换Sum(a,static_castint(b));这两种方式都能解决“一个T推导出多个类型”的问题。区别在于一个是显式指定模板参数一个是先把实参类型处理一致。很多入门资料会把这种调用方式也放在“显式实例化”里讲复习时知道它想表达的是“模板参数由我们明确给出”就可以了。五、模板参数匹配时的几个规则函数模板和普通函数可以同名存在这也是初学时比较容易绕的地方。先看一段代码intSum(intlhs,intrhs){returnlhsrhs;}templatetypenameTTSum(T lhs,T rhs){returnlhsrhs;}intmain(){Sum(1,2);Sumint(1,2);}第一句Sum(1, 2)会优先调用普通函数因为普通函数已经能完全匹配编译器没必要再用模板生成一份一样的函数。第二句Sumint(1, 2)明确写了模板参数所以会调用模板生成的版本。再看另一种情况intSum(intlhs,intrhs){returnlhsrhs;}templatetypenameT1,typenameT2autoSum(T1 lhs,T2 rhs){returnlhsrhs;}intmain(){Sum(1,2);// 普通函数完全匹配Sum(1,2.5);// 模板可以生成更合适的版本}Sum(1, 2.5)如果调用普通函数就需要把2.5转成int这会发生类型转换。而函数模板可以直接生成类似Sumint, double的版本匹配程度更高所以编译器会选择模板。这里可以简单记成三句话普通函数和函数模板可以同名。如果普通函数和模板实例化出来的函数一样合适优先调用普通函数。如果模板能生成更匹配的版本就会选择模板。还有一个细节也要记住模板参数推导时通常不会主动做普通类型转换。显式指定模板参数以后函数调用阶段才可能发生可行的类型转换。六、类模板让类也能和类型解耦函数可以写模板类当然也可以。类模板适合用在这种场景类的整体逻辑一样只是内部存储的数据类型不同。比如一个简单的栈存int、存double、存自定义对象本质操作都是入栈、出栈、取栈顶。区别只是元素类型。函数模板和类模板可以先这样区分对比点函数模板类模板解决的问题函数逻辑重复类结构和成员操作重复生成结果具体类型的函数具体类型的类常见写法Sum(1, 2)或Sumint(1, 2)Stackint s;初学重点模板参数推导和匹配规则类名后面要带具体类型类模板的基本格式是templatetypenameTclass类名{// 成员变量和成员函数};写一个简化版栈#includecstddeftemplatetypenameTclassStack{public:Stack(std::size_t cap8):_data(newT[cap]),_cap(cap),_size(0){}~Stack(){delete[]_data;}voidPush(constTvalue);private:T*_data;std::size_t _cap;std::size_t _size;};如果成员函数在类外定义写法要注意两点前面仍然要带模板参数列表。类名后面要写上模板参数。templatetypenameTvoidStackT::Push(constTvalue){if(_size_cap){// 这里先省略扩容逻辑重点看模板写法return;}_data[_size]value;_size;}StackT::Push里的StackT不能写成单纯的Stack。因为Stack只是类模板名Stackint、Stackdouble这种实例化结果才是真正的类型。七、类模板的实例化类模板和函数模板有一个明显区别类模板通常不能只靠构造对象时的参数自动推导出来基础写法里需要在类名后面写上具体类型。Stackints1;Stackdoubles2;这里的Stackint才是一个真正的类型表示“存放int的栈”。Stackdouble也是一个真正的类型表示“存放double的栈”。它们来自同一个类模板但实例化之后是两个不同类型。所以不要把Stack和Stackint混成一回事Stackints1;// 正确Stackdoubles2;// 正确模板名只是模子带上具体类型后才得到能创建对象的类。八、模板为什么不建议声明和定义分离普通类的成员函数经常可以声明放在.h定义放在.cpp。但模板不太一样。模板代码需要在编译阶段根据具体类型生成代码。如果编译器在使用模板时只看到了声明看不到定义就没办法完成实例化后面很容易出现链接错误。所以实际写模板时常见做法是函数模板直接写在头文件里。类模板的成员函数定义也放在头文件里。或者使用.hpp这类文件专门放模板实现。入门阶段先记住这个结论就够了模板不是普通函数的简单替代品它依赖编译期实例化因此定义通常要让使用它的编译单元看得见。九、这一部分怎么串起来模板初阶可以按这条线理解重复代码太多 - 提出泛型编程 - 用函数模板描述通用函数逻辑 - 编译器根据实参类型实例化具体函数 - 理解隐式推导和显式指定模板参数 - 搞清楚模板函数和普通函数的匹配规则 - 用类模板描述通用数据结构如果只背语法很容易写着写着就乱了。我的建议是先抓住一句话模板就是把“类型不同但逻辑相同”的代码交给编译器生成。理解了这句话template typename T、Sumint、Stackdouble这些写法就不是孤立语法了它们都是在告诉编译器请按这个类型把模子变成真正能用的代码。小结这篇主要整理了 C 模板入门阶段的几个基础点。函数模板解决的是函数逻辑重复的问题。它本身不是函数而是生成函数的模子。使用时可以让编译器根据实参进行隐式推导也可以通过函数名类型的方式显式指定模板参数。模板匹配时普通函数和函数模板可以同名存在。完全匹配时普通函数优先如果模板能生成更合适的版本编译器也会选择模板。模板参数推导阶段一般不会随便做类型转换这一点在混合类型调用时尤其要注意。类模板解决的是类型不同但类结构和操作相同的问题。Stack是类模板名Stackint才是具体类型。类模板的成员函数如果写在类外需要带上template typename T并且使用StackT::指明作用域。模板刚学的时候看起来有点绕但它的出发点其实很朴素少写重复代码把类型变化交给编译器处理。后面学习 STL 时会发现容器、算法、迭代器这些东西都离不开这个基础。