ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

泛型编程的诞生(C++篇)

2026/8/7 7:25:24 拓冰建站 浏览量
泛型编程的诞生(C++篇) 模板的深入理解前言模板非常的重要它就是STL的根基。STL容器vector/list/set/map、算法sort全都是基于模板实现我将从函数模板→类模板→模板实例化匹配→模板特化→分离编译层层拆解贯穿底层一、为什么需要模板泛型编程的诞生1. 传统重载的痛点相信大部分从C语言学过来的人都大概率实现过计算器的基本功能。写整数的加法、减法我们会写一个Plus的函数但是计算器的场景不可能只停留在整数还可能涉及到小数也就是我们的浮点数。而C语言完全不支持函数重载C语言标准规定死了同名函数只能有一个那就非常的麻烦。你得写个Plus1Plus2…这类不同名的函数只是为了实现一个简单的加法。非常难受。进入C就好很多你可以在类里封装一个Plus函数对它进行重载传不同的参数但是只是可以名字相同你的工作量还是没有下来你还是得人为的去写一大堆重载函数。比如你还想要一个通用交换函数处理int、double、charvoidSwap(intleft,intright){inttempleft;leftright;righttemp;}voidSwap(doubleleft,doubleright){doubletempleft;leftright;righttemp;}voidSwap(charleft,charright){chartempleft;leftright;righttemp;}重载存在两大致命缺陷代码复用率极低每新增一种类型必须手写一份完全重复逻辑的函数维护成本极高逻辑修改时所有重载函数都要同步改动一处出错全部出错。2. 泛型编程思想但是呢我们祖师爷本.贾尼博士整了个模板也就是从模板开始C有了质的变化。祖师爷呢就希望告诉编译器一套通用逻辑让编译器根据传入的类型自动生成对应代码不用人工重复写。C通过模板实现泛型编程分为两大分支函数模板通用函数类模板通用类stack/vector底层全是类模板二、函数模板2.1 函数模板基础语法templatetypenameT1,typenameT2,...,Tn返回值类型 函数名(参数列表){//通用逻辑和类型无关}关键字说明template模板声明标识必须写在模板函数/类最前面typename定义模板参数代表通用类型占位符等价替换class T二者完全通用不能用struct TT类型占位符编译阶段由编译器替换成真实类型。通用交换函数模板示例templatetypenameTvoidSwap(Tleft,Tright){T templeft;leftright;righttemp;}2.2 函数模板底层原理模板本身不是可执行函数只是一套代码模具。编译阶段编译器会做两件事读取函数调用时传入的实参推导真实类型根据推导出来的类型实例化一份专属该类型的函数代码。示例inta1,b2;doublec1.1,d2.2;Swap(a,b);// 推导Tint生成Swapint版本Swap(c,d);// 推导Tdouble生成Swapdouble版本模具模板 实参类型 → 编译器生成对应类型函数。2.3 函数模板两种实例化方式1隐式实例化最常用自动推导编译器根据传入实参自动推导T不用手动指定类型templatetypenameTTAdd(constTleft,constTright){returnleftright;}intmain(){inta110,a220;doubled110.0,d220.0;Add(a1,a2);// Tint 隐式实例化Add(d1,d2);// Tdouble 隐式实例化return0;}隐式实例化限制两边参数类型必须完全一致Add(a1,d1);// 报错第一个参数推导Tint第二个推导Tdouble冲突无法确定T2显式实例化手动指定模板参数调用函数时用强制写明真实类型解决类型冲突问题Addint(a1,d1);// 强制Tintdouble会隐式转换为intAdddouble(a1,d1);// 强制Tdoubleint转double2.4 模板与普通函数的匹配规则普通函数优先级高于模板存在同名普通函数模板时优先调用普通函数不会实例化模板。人想偷懒所以搞了一套模板逻辑把原本人要做的事情交给编译器来做当然编译器发现人写了现有的函数它也会想着去偷懒自己就不用生成了直接用人写的函数这是一个优先级的问题。// 普通int加法函数intAdd(intleft,intright){returnleftright;}// 通用模板templatetypenameTTAdd(T left,T right){returnleftright;}voidTest(){Add(1,2);// 完全匹配普通int函数优先调用普通函数Addint(1,2);// 显式指定模板强制调用模板版本Add(1.0,2.0);// 无匹配普通函数实例化模板double版本}模板不支持自动类型转换普通函数支持自动转换。普通函数int Add(int,int)传double会自动截断模板必须类型完全匹配或手动显式实例化。三、类模板STL容器底层核心vector、stack、list、string底层全部是类模板。类模板模具template… class Data未实例化不是真实类模板类成品Dataint, int编译器根据模具生成的、可以定义对象的真实类。3.1 类模板定义语法templateclassT1,classT2...class类名{//成员变量、成员函数可用T代表通用类型};栈类模板实战templatetypenameTclassStack{public:Stack(size_t capacity4){_arraynewT[capacity];_capacitycapacity;_size0;}voidPush(constTdata);private:T*_array;size_t _capacity;size_t _size;};3.2 类模板实例化规则和函数模板巨大区别类模板无法隐式推导必须显式指定模板参数类型intmain(){Stackintst1;// Tint实例化int栈Stackdoublest2;// Tdouble实例化double栈// Stack st3; 直接报错必须写明内类型return0;}类模板外部定义成员函数标准写法当我们在类模板外去实现其成员函数时必须重写template模板声明同时类名带上模板参数templatetypenameTvoidStackT::Push(constTdata){//扩容逻辑..._array[_size]data;}3.3 类模板分离编译大坑重中之重问题描述如果把模板声明放在.h头文件实现放在.cpp源文件// a.htemplatetypenameTTAdd(constTl,constTr);// a.cpptemplatetypenameTTAdd(constTl,constTr){returnlr;}// main.cpp#includea.hintmain(){Add(1,2);// 链接报错LNK2019}报错根本原因C编译是分文件独立编译编译a.cpp文件内没有任何Addint调用编译器不会实例化任何版本模板不生成函数实体编译main.cpp只看见函数声明看不到模板完整实现无法实例化代码链接阶段找不到Addint实体报未解析外部符号。两种标准解决方案推荐方案将模板声明实现全部放入.hpp头文件所有使用文件直接include编译时完整看到模板定义正常实例化不推荐在.cpp手动提前实例化需要的类型拓展性极差。四、模板进阶模板特化解决特殊类型逻辑差异4.1 什么是特化基础模板提供通用逻辑部分特殊类型需要单独定制实现我们针对该类型重写一套模板逻辑这个过程叫模板特化。分为两类函数模板特化、类模板特化全特化、偏特化。4.2 函数模板全特化场景通用比较模板Less普通内置类型正常比较但比较模板对于自定义Date类会直接比较地址但是我们想的时是去比较哪个日期大逻辑明显错误所以要单独特化这种场景。步骤先存在基础通用函数模板template空模板参数列表代表全特化函数名后指定特化类型函数参数全部替换为目标类型。//基础通用模板templatetypenameTboolLess(constTleft,constTright){returnleftright;}//Date类函数模板全特化templateboolLessDate(constDateleft,constDateright){//自定义日期比较逻辑不再比较地址returnleft.GetYear()right.GetYear()||(left.GetYear()right.GetYear()left.GetMonth()right.GetMonth())||(left.GetYear()right.GetYear()left.GetMonth()right.GetMonth()left.GetDay()right.GetDay());}调用规则传入Date对象时编译器优先匹配特化版本不会使用通用模板。4.3 类模板特化全特化 偏特化1全特化所有模板参数全部固定原模板templateclass T1, class T2 class Data;全特化固定所有参数为int、chartemplateclassDataint,char{//专属intchar的单独实现};2偏特化只固定一部分模板参数剩余仍为通用场景1部分参数固定templatetypenameT1classDataT1,int{//第二个参数固定为int第一个任意类型};场景2参数类型限制指针/引用偏特化//仅针对指针类型偏特化templatetypenameTclassDataT*,T*{//专门处理指针的逻辑比如解引用比较};逻辑脉络首先你通用模板不能写死解引用普通类型会崩溃所以单独写指针偏特化版本只要实例化时传入指针编译器自动切换专属逻辑内部完成解引用比较普通类型走通用逻辑直接对比原值互不冲突。#includeiostreamusingnamespacestd;templateclassT1,classT2classData{public:T1 _d1;T2 _d2;boolIsLess(){// 错误不分类型全部解引用return*_d1*_d2;}};intmain(){Dataint,intd1(1,2);d1.IsLess();// 编译报错int无法解引用return0;}#includeiostreamusingnamespacestd;// 通用模板处理普通非指针类型templateclassT1,classT2classData{public:T1 _d1;T2 _d2;// 直接比较原值不解引用boolIsLess(){return_d1_d2;}};// 指针偏特化仅匹配两个同类型指针 T*, T*templatetypenameTclassDataT*,T*{private:T*_p1;T*_p2;public:Data(T*a,T*b):_p1(a),_p2(b){}// 内部封装解引用对比真实存储数据boolIsLess(){return*_p1*_p2;}};模板版本适用类型是否内部解引用用户是否手动写*通用基础模板int/double/string普通类型不解引用直接操作原值不需要指针偏特化DataT*,T*两个同类型指针内部封装*解引用完全不需要4.4 特化实战意义STL算法底层支撑以std::sortvectorDate*举例容器存日期指针时通用模板只会对比指针地址排序错乱对指针类型偏特化后算法会解引用对比真实日期排序逻辑正常。五、非类型模板参数5.1 概念模板参数不只是类型typename T还可以传入常量整数/字符等编译期常量称为非类型模板参数。templatesize_t N//N是编译期常量非类型参数classArray{private:char_arr[N];//编译期固定大小数组};//调用Array10arr1;//N10Array100arr2;//N100限制只能是整型、字符常量浮点数、自定义类不能作为非类型参数参数必须是编译期可确定常量不能传变量。六、模板优缺点总结优点极致代码复用一套逻辑适配所有类型消除重复重载提升代码灵活性STL整套容器/算法基于模板搭建编译期多份类型代码运行无性能损耗。缺陷代码膨胀代码爆炸每种实例化类型都会生成一份独立代码可执行文件体积变大编译时间变长报错晦涩难懂模板推导失败时编译器报错极长新手难以定位错误最重要的分离编译限制必须头文件包含全部实现。七、问题总结函数模板隐式实例化为什么不能两边不同类型答编译器需要统一推导T两个参数类型冲突推导失败需要显式实例化强制转换。(或者你去写两个模板参数typename T1typename T2)普通函数和同名模板同时存在调用优先级答完全匹配普通函数时优先普通函数无普通函数匹配才实例化模板。类模板和函数模板实例化区别答函数模板支持隐式推导类模板必须手动指定类型无法自动推导。模板分离编译报错原因与解决方案答分文件编译不会主动实例化模板链接找不到实体解决方案.h头文件放完整实现(也就是不对类模板进行声明定义分离)或者在.cpp里对其手动实例化。(不推荐)什么是模板特化全特化和偏特化区别答针对特殊类型定制模板逻辑全特化固定所有模板参数偏特化仅固定部分参数。模板代码膨胀是什么答每一种实例化类型都会生成独立函数/类代码程序体积增大。补充模板是编译期特性所有推导、实例化全部发生在编译阶段运行时没有模板相关逻辑看不懂的时候可以多看编译器生成的对应类型代码理解模具生成逻辑。