news 2026/7/1 19:21:20

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ 模板初阶:从重复代码到泛型编程

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

快速跳转:前言 为什么需要模板 函数模板 模板原理 实例化 匹配规则 类模板 小结

前言

刚开始写 C++ 时,我们很容易遇到一种尴尬情况:逻辑明明一模一样,只是类型不同,代码却要写好几遍。

比如交换两个变量,int要写一份,double要写一份,char可能还要再写一份。写的时候感觉只是复制粘贴,后面维护时就不一定这么舒服了。万一某个版本里写错了,其他版本还得挨个检查。

模板要解决的就是这类问题:把和类型无关的逻辑抽出来,让编译器根据实际类型去生成对应代码。简单说,模板不是让程序运行时变“万能”,而是让编译器在编译阶段帮我们少写重复代码。

这篇先整理模板入门阶段最核心的几块:

学习点先抓住什么
泛型编程为什么“类型不同但逻辑相同”的代码不适合一直重载
函数模板怎么写出一份通用函数逻辑
模板实例化编译器什么时候把模板变成具体函数
匹配规则普通函数和函数模板同名时,编译器怎么选
类模板怎么让类的数据类型也变得通用

先把这些基础问题理顺,后面再看 STL、容器、迭代器、仿函数这些内容,会轻松很多。

一、先从重复代码说起

如果没有模板,我们想写一个通用的交换函数,最直观的办法是函数重载:

voidSwapValue(int&left,int&right){inttmp=left;left=right;right=tmp;}voidSwapValue(double&left,double&right){doubletmp=left;left=right;right=tmp;}voidSwapValue(char&left,char&right){chartmp=left;left=right;right=tmp;}

这段代码能用,但问题也很明显:

  1. 逻辑高度重复,只是类型不一样。
  2. 新增一个类型,就可能要新增一个函数。
  3. 一旦交换逻辑要改,每个重载版本都要跟着改。

所以这里真正需要的不是“更多重载”,而是一个能描述通用逻辑的模子。

这就是泛型编程的思想:写出和具体类型无关的代码,让同一份逻辑适配不同类型。C++ 里的模板,就是泛型编程最基础也最重要的工具。

二、函数模板:先写一个“函数模子”

函数模板的基本格式是:

template<typenameT>返回值类型 函数名(参数列表){// 函数体}

typename后面的T表示一个模板类型参数。这里的T不是固定类型,它更像一个占位符,等到真正调用函数时,编译器再根据实参类型把它替换成具体类型。

用模板改写前面的交换函数:

template<typenameT>voidSwapValue(T&left,T&right){T tmp=left;left=right;right=tmp;}

调用时可以这样写:

inta=10;intb=20;SwapValue(a,b);// T 被推导为 intdoublex=1.1;doubley=2.2;SwapValue(x,y);// T 被推导为 double

这时我们写的只有一份模板代码,但编译器会根据实际调用生成对应类型的函数版本。

这里补一个小细节:定义模板类型参数时,typenameclass都可以用。

template<classT>voidPrint(constT&value){cout<<value<<endl;}

在这种场景下,class并不是说T必须是类类型,内置类型也可以。只是关键字写法不同而已。不过不能把这里的class换成struct

三、模板本身不是函数

这一点很重要:函数模板本身不是一个真正能被调用的函数。

它更像一张图纸。只有当我们用具体类型去调用它时,编译器才会根据这张图纸生成一份真正的函数代码。

比如:

template<typenameT>TSum(T lhs,T rhs){returnlhs+rhs;}intmain(){Sum(1,2);// 生成 int Sum(int, int)Sum(1.5,2.5);// 生成 double Sum(double, double)}

编译器看到Sum(1, 2),会推导出Tint,于是生成一份处理int的函数。

看到Sum(1.5, 2.5),又会推导出Tdouble,于是生成一份处理double的函数。

所以模板帮我们省下的,是手动重复写这些函数的工作。真正的代码生成,发生在编译阶段。

四、函数模板的实例化

用具体类型使用函数模板的过程,叫做模板实例化。

函数模板常见的使用方式有两种:让编译器隐式推导类型,或者由我们显式指定模板参数。

1. 隐式实例化

隐式实例化就是让编译器自己根据实参推导类型。

template<typenameT>TSum(T lhs,T rhs){returnlhs+rhs;}intmain(){inta=10;intb=20;Sum(a,b);// T 推导为 intdoublex=1.1;doubley=2.2;Sum(x,y);// T 推导为 double}

这种写法最自然,也是平时最常见的用法。

但隐式推导有一个容易踩的点:如果同一个模板参数从不同实参里推导出了不同类型,编译器就不知道该听谁的。

inta=10;doubleb=2.5;Sum(a,b);// 这里会出问题

a希望Tintb希望Tdouble。可模板参数列表里只有一个T,编译器不能擅自替我们决定,所以这类调用通常会编译失败。

这不是编译器“不会变通”,而是模板推导阶段本来就比较严格。它要先把类型推导清楚,不能一边推导一边随便做类型转换。

2. 显式指定模板参数

如果我们就是想指定模板参数类型,可以在函数名后面加<>

Sum<int>(a,b);

这表示明确告诉编译器:这次T就按int处理。

于是b会尝试转换成int后再参与调用。如果转换不合法,还是会报错。

也可以自己先做强制类型转换:

Sum(a,static_cast<int>(b));

这两种方式都能解决“一个T推导出多个类型”的问题。区别在于,一个是显式指定模板参数,一个是先把实参类型处理一致。

很多入门资料会把这种调用方式也放在“显式实例化”里讲,复习时知道它想表达的是“模板参数由我们明确给出”就可以了。

五、模板参数匹配时的几个规则

函数模板和普通函数可以同名存在,这也是初学时比较容易绕的地方。

先看一段代码:

intSum(intlhs,intrhs){returnlhs+rhs;}template<typenameT>TSum(T lhs,T rhs){returnlhs+rhs;}intmain(){Sum(1,2);Sum<int>(1,2);}

第一句Sum(1, 2)会优先调用普通函数,因为普通函数已经能完全匹配,编译器没必要再用模板生成一份一样的函数。

第二句Sum<int>(1, 2)明确写了模板参数,所以会调用模板生成的版本。

再看另一种情况:

intSum(intlhs,intrhs){returnlhs+rhs;}template<typenameT1,typenameT2>autoSum(T1 lhs,T2 rhs){returnlhs+rhs;}intmain(){Sum(1,2);// 普通函数完全匹配Sum(1,2.5);// 模板可以生成更合适的版本}

Sum(1, 2.5)如果调用普通函数,就需要把2.5转成int,这会发生类型转换。

而函数模板可以直接生成类似Sum<int, double>的版本,匹配程度更高,所以编译器会选择模板。

这里可以简单记成三句话:

  1. 普通函数和函数模板可以同名。
  2. 如果普通函数和模板实例化出来的函数一样合适,优先调用普通函数。
  3. 如果模板能生成更匹配的版本,就会选择模板。

还有一个细节也要记住:模板参数推导时通常不会主动做普通类型转换。显式指定模板参数以后,函数调用阶段才可能发生可行的类型转换。

六、类模板:让类也能和类型解耦

函数可以写模板,类当然也可以。

类模板适合用在这种场景:类的整体逻辑一样,只是内部存储的数据类型不同。

比如一个简单的栈,存int、存double、存自定义对象,本质操作都是入栈、出栈、取栈顶。区别只是元素类型。

函数模板和类模板可以先这样区分:

对比点函数模板类模板
解决的问题函数逻辑重复类结构和成员操作重复
生成结果具体类型的函数具体类型的类
常见写法Sum(1, 2)Sum<int>(1, 2)Stack<int> s;
初学重点模板参数推导和匹配规则类名后面要带具体类型

类模板的基本格式是:

template<typenameT>class类名{// 成员变量和成员函数};

写一个简化版栈:

#include<cstddef>template<typenameT>classStack{public:Stack(std::size_t cap=8):_data(newT[cap]),_cap(cap),_size(0){}~Stack(){delete[]_data;}voidPush(constT&value);private:T*_data;std::size_t _cap;std::size_t _size;};

如果成员函数在类外定义,写法要注意两点:

  1. 前面仍然要带模板参数列表。
  2. 类名后面要写上模板参数。
template<typenameT>voidStack<T>::Push(constT&value){if(_size==_cap){// 这里先省略扩容逻辑,重点看模板写法return;}_data[_size]=value;++_size;}

Stack<T>::Push里的Stack<T>不能写成单纯的Stack。因为Stack只是类模板名,Stack<int>Stack<double>这种实例化结果才是真正的类型。

七、类模板的实例化

类模板和函数模板有一个明显区别:类模板通常不能只靠构造对象时的参数自动推导出来,基础写法里需要在类名后面写上具体类型。

Stack<int>s1;Stack<double>s2;

这里的Stack<int>才是一个真正的类型,表示“存放int的栈”。

Stack<double>也是一个真正的类型,表示“存放double的栈”。

它们来自同一个类模板,但实例化之后是两个不同类型。

所以不要把StackStack<int>混成一回事:

Stack<int>s1;// 正确Stack<double>s2;// 正确

模板名只是模子,带上具体类型后,才得到能创建对象的类。

八、模板为什么不建议声明和定义分离

普通类的成员函数经常可以声明放在.h,定义放在.cpp

但模板不太一样。模板代码需要在编译阶段根据具体类型生成代码。如果编译器在使用模板时只看到了声明,看不到定义,就没办法完成实例化,后面很容易出现链接错误。

所以实际写模板时,常见做法是:

  1. 函数模板直接写在头文件里。
  2. 类模板的成员函数定义也放在头文件里。
  3. 或者使用.hpp这类文件专门放模板实现。

入门阶段先记住这个结论就够了:模板不是普通函数的简单替代品,它依赖编译期实例化,因此定义通常要让使用它的编译单元看得见。

九、这一部分怎么串起来

模板初阶可以按这条线理解:

重复代码太多 -> 提出泛型编程 -> 用函数模板描述通用函数逻辑 -> 编译器根据实参类型实例化具体函数 -> 理解隐式推导和显式指定模板参数 -> 搞清楚模板函数和普通函数的匹配规则 -> 用类模板描述通用数据结构

如果只背语法,很容易写着写着就乱了。我的建议是先抓住一句话:模板就是把“类型不同但逻辑相同”的代码交给编译器生成。

理解了这句话,template <typename T>Sum<int>Stack<double>这些写法就不是孤立语法了,它们都是在告诉编译器:请按这个类型,把模子变成真正能用的代码。

小结

这篇主要整理了 C++ 模板入门阶段的几个基础点。

函数模板解决的是函数逻辑重复的问题。它本身不是函数,而是生成函数的模子。使用时可以让编译器根据实参进行隐式推导,也可以通过函数名<类型>的方式显式指定模板参数。

模板匹配时,普通函数和函数模板可以同名存在。完全匹配时普通函数优先;如果模板能生成更合适的版本,编译器也会选择模板。模板参数推导阶段一般不会随便做类型转换,这一点在混合类型调用时尤其要注意。

类模板解决的是类型不同但类结构和操作相同的问题。Stack是类模板名,Stack<int>才是具体类型。类模板的成员函数如果写在类外,需要带上template <typename T>,并且使用Stack<T>::指明作用域。

模板刚学的时候看起来有点绕,但它的出发点其实很朴素:少写重复代码,把类型变化交给编译器处理。后面学习 STL 时,会发现容器、算法、迭代器这些东西都离不开这个基础。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/1 19:17:33

如何用WiFi热图工具快速定位家庭网络盲区

如何用WiFi热图工具快速定位家庭网络盲区 【免费下载链接】wifi-heat-mapper whm also known as wifi-heat-mapper is a Python library for benchmarking Wi-Fi networks and gather useful metrics that can be converted into meaningful easy-to-understand heatmaps. 项…

作者头像 李华
网站建设 2026/7/1 19:09:31

最大似然估计(MLE)

最大似然估计&#xff08;Maximum Likelihood Estimation&#xff0c;简称 MLE&#xff09; 是统计学和机器学习中最核心的参数估计方法。 如果说 “均方误差&#xff08;MSE&#xff09;” 是为了衡量预测得准不准&#xff0c;那么“最大似然估计”就是为了解决一个更根本的问…

作者头像 李华
网站建设 2026/7/1 19:03:15

抖音评论数据采集神器:3分钟零代码获取完整评论分析

抖音评论数据采集神器&#xff1a;3分钟零代码获取完整评论分析 【免费下载链接】TikTokCommentScraper 项目地址: https://gitcode.com/gh_mirrors/ti/TikTokCommentScraper 还在为手动复制抖音评论而烦恼吗&#xff1f;TikTokCommentScraper是你的终极解决方案&#…

作者头像 李华
网站建设 2026/7/1 19:03:03

终极指南:用Mac Mouse Fix让普通鼠标在macOS上超越触控板体验

终极指南&#xff1a;用Mac Mouse Fix让普通鼠标在macOS上超越触控板体验 【免费下载链接】mac-mouse-fix Mac Mouse Fix - Make Your $10 Mouse Better Than an Apple Trackpad! 项目地址: https://gitcode.com/GitHub_Trending/ma/mac-mouse-fix 你是否厌倦了在macOS上…

作者头像 李华
网站建设 2026/7/1 19:02:52

欧盟下月将公布针对谷歌新法规,谷歌担忧引发安全隐私问题

欧洲加大对大型科技公司监管力度&#xff0c;欧盟委员会计划下月公布针对谷歌的新法规。谷歌对此表示担忧&#xff0c;认为可能引发安全和隐私问题。监管新动态欧盟委员会下月将公布针对谷歌的新法规&#xff0c;规则或迫使谷歌在欧盟市场与对手更友好相处。潜在变化有两种形式…

作者头像 李华
网站建设 2026/7/1 19:02:27

观远数据发布AI决策智能平台,开启企业决策智能新世代

6月26日&#xff0c;观远数据「决策智能新世代&#xff5c;2026观远数据AI战略暨新品全球首发」在杭州圆满举办。来自消费零售、制造业、电商跨境、金融等各行各业的先进企业代表、行业专家与资方媒体伙伴齐聚杭州&#xff0c;共同探讨企业AI应用的下一阶段。 在头部企业服务赛…

作者头像 李华