重生之从0开始学习c++之模板初级

张开发
2026/4/21 4:59:39 15 分钟阅读

分享文章

重生之从0开始学习c++之模板初级
1. 泛型编程 —— 为什么需要模板如何实现一个通用的交换函数呢voidSwap(intleft,intright){inttempleft;leftright;righttemp;}voidSwap(doubleleft,doubleright){doubletempleft;leftright;righttemp;}voidSwap(charleft,charright){chartempleft;leftright;righttemp;}因为c支持函数重载所以如果我们想用不同类型的参数是不是可以这么写啊但是这样写是不是有点麻烦和冗余啊因为它们的逻辑完全相同仅仅是类型不同。那能否告诉编译器一个模子让编译器根据不同的类型利用该模子来生成代码呢就像这样┌─────────────────────────────────────────────────────────────┐ │ 模具(模板)│ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ │ │voidSwap(Tleft,Tright)│ │ │ │{│ │ │ │ T templeft;│ │ │ │ leftright;│ │ │ │ righttemp;│ │ │ │}│ │ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ │ │ ▼ 倒入绿色液体 ▼ 倒入蓝色液体 ▼ 倒入红色液体 ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Swap │ │ Swap │ │ Swap │ │int│ │double│ │char│ └─────────┘ └─────────┘ └─────────┘如果在C中也能够存在这样一个模具通过给这个模具中填充不同材料(类型)来获得不同材料的铸件(即生成具体类型的代码那将会节省许多头发。巧的是前人早已将树栽好我们只需在此乘凉。核心思想泛型编程——编写与类型无关的通用代码由编译器根据实际使用时的类型自动生成针对该类型的代码。2. 函数模板函数模板代表了一个函数家族该函数模板与类型无关在使用时被参数化根据实参类型产生函数的特定类型版本。2.1 函数模板的语法templatetypename T1, typename T2,…,typename Tn返回值类型 函数名(参数列表){}templatetypenameT// T 是模板参数可以是 typename 或 classvoidSwap(Tleft,Tright){T templeft;leftright;righttemp;}2.2 函数模板的原理函数模板本身不是函数它只是一个蓝图。编译器遇到函数模板的调用时才会根据实参类型生成一个具体的函数。这个过程叫做模板实例化。流程图解编译器在编译期的推演过程源代码 ─────────────────────────────────────────────────────────────intmain(){doubled12.0,d25.0;Swap(d1,d2);// 调用点1inti110,i220;Swap(i1,i2);// 调用点2chara0,b9;Swap(a,b);// 调用点3}───────────────────────────────────────────────────────────── │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 编译器推演 │ │ 编译器推演 │ │ 编译器推演 │ │ 实参类型double│ │ 实参类型int│ │ 实参类型char│ │ 推导 Tdouble│ │ 推导 Tint│ │ 推导 Tchar│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 生成的函数 │ │ 生成的函数 │ │ 生成的函数 │ │voidSwap(│ │voidSwap(│ │voidSwap(│ │doubleleft,│ │intleft,│ │charleft,│ │doubleright │ │intright)│ │charright)│ │){...}│ │{...}│ │{...}│ └─────────────────┘ └─────────────────┘ └─────────────────┘编译器为每种不同的类型组合生成一份独立的函数代码。最终的可执行文件中包含了Swapdouble、Swapint、Swapchar三个具体的函数就像你手动写了三个重载一样。2.3 函数模板的实例化用具体类型使用函数模板称为实例化。分为两种隐式实例化让编译器自动根据实参类型推导模板参数。templateclassTTAdd(constTleft,constTright){returnleftright;}intmain(){Add(1,2);// 两个实参都是 int推导 T intAdd(1.0,2.0);// 两个实参都是 double推导 T double// Add(1, 2.0); // 编译错误一个 int一个 double编译器推导冲突}错误图解调用Add(1,2.0) │ ├─ 实参1(1)类型为int→ 推导 Tint├─ 实参2(2.0)类型为double→ 推导 Tdouble│ └─ 冲突模板参数列表中只有一个 T编译器无法确定 T 到底是int还是double。 编译器不会进行隐式类型转换因为转换可能丢失数据编译器不背这个锅。解决方案Add(a,(int)d);// 方案1用户手动强转Addint(a,d);// 方案2显式实例化推荐显式实例化在函数名后用 类型 强制指定模板参数。Addint(10,20.0);// 强制 T int20.0 会被隐式转换为 int流程图调用Addint(a,b) │ ▼ ┌─────────────────────────────────────────┐ │ 编译器用户已指定 Tint不用推导了 │ │ 实参a(int)→ 匹配 │ │ 实参b(double)→ 尝试隐式转换为int│ │ 如果能转就编译通过否则报错 │ └─────────────────────────────────────────┘ │ ▼ 生成函数intAdd(constintleft,constintright)2.4 模板参数的匹配原则原则一非模板函数可以和同名模板函数共存// 非模板函数专门处理 intintAdd(intleft,intright){returnleftright;}// 模板函数通用版本templateclassTTAdd(T left,T right){returnleftright;}原则二优先调用非模板函数除非模板能生成更好的匹配Add(1,2);// 调用非模板函数完全匹配且非模板优先Addint(1,2);// 强制调用模板实例化的版本Add(1,2.0);// 非模板不匹配参数类型不同模板可以生成更好的匹配如果模板有两个参数决策流程图遇到函数调用Add(1,2) │ ├─ 查找同名非模板函数 → 找到intAdd(int,int)→ 完全匹配 → 调用 │ └─ 即使模板能生成完全相同的函数也不考虑非模板优先 遇到函数调用Add(1,2.0) │ ├─ 查找同名非模板函数 →intAdd(int,int)不匹配第二个参数类型不对 │ └─ 查找模板 → 若有templateclassT1,classT2版本可生成匹配函数 → 调用模板实例化版本原则三模板函数不允许自动类型转换普通函数可以voidfunc(intx,inty){}// 普通函数templateclassTvoidfunc(T x,T y){}// 模板函数func(1,a);// 调用普通函数a 自动转换为 intASCII 97// 模板函数不会考虑因为 char 和 int 推导冲突3. 类模板3.1 为什么需要类模板以 Stack栈为例我们需要存储 int 的栈也需要存储 double、string 的栈。如果不用模板要么为每种类型写一个类要么用 void* 或继承不类型安全。类模板就是类的模具。3.2 类模板的定义格式templateclassT1,classT2,...,classTnclass类模板名{// 类内成员定义};我们来举一个我们很熟练的栈的例子templateclassTypeclassStack{public:Stack(intcapacity4):_arr(newType[capacity]),_size(0),_capacity(capacity){}~Stack(){delete[]_arr;_arrnullptr;_size_capacity0;}voidPush(constTypex){if(_capacity_size){Type*tmpnewType[_capacity*2];memcpy(tmp,_arr,sizeof(Type)*_size);delete[]_arr;_arrtmp;_capacity_capacity*2;}_arr[_size]x;}voidPrint()const{for(inti0;i_size;i){cout_arr[i] ;}}private:Type*_arr;int_size;int_capacity;};intmain(){Stackintst1;st1.Push(1);st1.Push(2);st1.Push(3);st1.Push(4);st1.Print();return0;}关键点类模板名字是 Stack但真正的类型是Stackint、Stackdouble等。成员函数在类外定义时必须写成templateclass T void StackT::Push(...)。模板的声明和定义通常不分离到 .h 和 .cpp 两个文件否则会导致链接错误原因涉及模板实例化的编译模型后面会深入。3.3 类模板的实例化类模板必须显式实例化不能像函数模板那样隐式推导。类模板实例化与函数模板实例化不同类模板实例化需要在类模板名字后跟然后将实例化的类型放在中即可类模板名字不是真正的类而实例化的结果才是真正的类。Stackintst1;// 实例化出一个存储 int 的栈类Stackdoublest2;// 实例化出一个存储 double 的栈类程序代码段编译后 ┌───────────────────────────────────────────────────────────┐ │ Stackint类编译器生成 │ │-_array:int*│ │-Push(constint)│ ├───────────────────────────────────────────────────────────┤ │ Stackdouble类编译器生成 │ │-_array:double*│ │-Push(constdouble)│ └───────────────────────────────────────────────────────────┘ ↑ ↑ │ │ 使用 Stackintst1 使用 Stackdoublest2

更多文章