【C++ 模板初阶】一篇搞懂:函数模板、实例化与类模板
本文是模板系列的第一篇(初阶),只讲最核心、最常用的部分。
后面的进阶内容(模板特化、非类型模板参数、模板的分离编译、可变参数模板等)会单独成篇,敬请期待。
如果你是刚接触模板的小白,跟着本文一步一步走,保证能看懂、能上手。
一、先说两句:为什么要有模板?
很多小伙伴刚学完函数重载,会有一个疑问:我写一个Swap交换函数,int要写一遍,double要写一遍,string又要写一遍……这也太累了,代码全是重复的,看着都头疼。
有没有一种办法,让我只写一遍,就能通吃所有类型呢?
有!这就是泛型编程。
泛型编程:编写与类型无关的通用代码,让代码在使用时再绑定具体类型,从而实现代码复用。
而模板(Template)就是泛型编程的基础。你把它想象成一个"模具"就行:模具本身不是产品,但往里一浇,就能浇出对应类型的产品。
简单来说:模板写一次,类型随便换。
二、函数模板
2.1 概念
函数模板代表的是一个"函数家族",它本身与类型无关,在使用时会被"参数化"——编译器会根据你传入的实参类型,自动生成一个特定类型的函数版本。
2.2 格式
函数模板的格式长这样:
template<typenameT1,typenameT2,...,typenameTn>返回值类型 函数名(参数列表){...}一个经典的交换函数模板:
template<typenameT>voidSwap(T&left,T&right){T temp=left;left=right;right=temp;}这里有几个易错点,先给你打个预防针:
typename是用来定义模板参数的关键字,也可以换成class(两者在模板参数这里等价)。- ⚠️千万不能用
struct代替class,会编译报错,这个坑不少人踩过。
2.3 一句话搞懂原理:模板不是函数
很多人会问:模板到底是不是函数?
记住:函数模板只是一个"蓝图",它本身并不是函数。
在编译阶段,当你真正调用模板时,编译器会根据你传入的实参类型去"推演"(deduce)模板参数T,然后现场生成一个对应类型的真实函数。
举个例子:当你用double类型调用这个模板时,编译器把T推演成double,然后生成一份专门处理double的代码,再去调用它。
所以一句话概括:
编译阶段根据传入模板的类型,编译器现场生成一个函数;模板本身不是函数,只是一个生产函数的"模板机"。
三、函数模板的实例化
用不同类型的参数去使用同一个函数模板,这个过程就叫做模板实例化。
实例化分为两种:隐式实例化和显式实例化。
3.1 隐式实例化
看个加法模板:
template<classT>TAdd(constT&left,constT&right){returnleft+right;}intmain(){inta1=10,a2=20;doubled1=10.0,d2=20.0;Add(a1,a2);// 编译器把 T 推演成 intAdd(d1,d2);// 编译器把 T 推演成 doublereturn0;}这种"不用你管,编译器自己根据实参推导类型"的方式,就是隐式实例化。
⚠️注意下面这个大坑:
Add(a1,d1);// ❌ 编译报错!为什么报错?因为在编译期间,编译器看到这个调用,要通过实参a1把T推演成int,又通过实参d1把T推演成double。可模板参数列表里只有一个T,编译器犯了选择困难症——到底该把T定成int还是double?没法确定,直接报错。
划重点:在模板中,编译器一般不会帮你做隐式类型转换。
这时候你有两种解决办法:
方法一:自己强制类型转换
Add(a1,(int)d1);// 手动把 double 强转成 int方法二:使用显式实例化(下面马上讲)。
另外提醒一句:隐式实例化只有在T作为传入参数时才能推导。如果T压根没出现在参数列表里,编译器就没法"看菜下碟",这时候就必须用显式实例化去规定T的类型。
3.2 显式实例化
显式实例化:在函数名后面的<>中,直接指定模板参数的实际类型。
intmain(){inta=10;doubleb=20.0;Add<int>(a,b);// 显式规定 T 是 intreturn0;}这里把T明确指定成了int。如果传入的类型不匹配,编译器会尝试做一次隐式类型转换;要是转换不了,就直接报错。
简单记:能推导就隐式,推不了就显式。
3.3 模板参数的匹配原则(重要)
当一个非模板函数和同名函数模板同时存在时,编译器会遵循下面这几条原则:
- 一个非模板函数可以和同名函数模板同时存在,而且模板还能被实例化成这个非模板函数。
- 如果其他条件都相同,调用时会优先调用非模板函数,不会从模板再生成一个实例。
- 如果模板能产生一个匹配更好的版本,那么就选模板。
看个例子就明白了:
// 专门处理 int 的加法函数(非模板)intAdd(intleft,intright){returnleft+right;}// 通用加法模板template<classT1,classT2>T1Add(T1 left,T2 right){returnleft+right;}voidTest(){Add(1,2);// 和非模板函数完全匹配 → 调用非模板函数Add(1,2.0);// 模板能生成更匹配的版本 → 选择模板生成新函数}Add(1, 2):两个参数都是int,和现成的非模板函数完全匹配,直接用现成的,没必要再实例化模板。Add(1, 2.0):一个int一个double,非模板函数对不上,但模板可以现场生成一个更合适的版本,于是选模板。
这一条"完全匹配优先用非模板,模板更合适才用模板"的规则,很多面试题会考,务必记牢。
四、类模板(初阶)
函数有模板,类当然也有模板。比如我们要写一个通用的Stack(栈),不想为int、double各写一遍,就可以用类模板。
4.1 定义格式
template<classT1,classT2,...,classTn>class类模板名{// 类内成员定义};一个栈的类模板示例:
template<typenameT>classStack{public:Stack(size_t capacity=4):_array(newT[capacity]),_capacity(capacity),_size(0){}voidPush(constT&data);private:T*_array;size_t _capacity;size_t _size;};4.2 关于声明和定义分离,这里有个大坑!
⚠️ 注意:模板不建议把声明和定义分离到两个文件(.h和.cpp)里,否则会出现链接错误。
因为模板只有在被实例化时才生成代码,而.cpp文件里如果没有具体实例化,编译器就不知道要为它生成啥,链接时自然找不到定义。
正确姿势:声明和定义写在同一个文件里。哪怕要分离,也只在同一文件内分离,并且每个成员函数定义时都要单独带模板参数:
template<classT>voidStack<T>::Push(constT&data){// 这里一般会有扩容逻辑,先省略_array[_size]=data;++_size;}注意定义处Stack<T>这个写法——成员函数属于"类模板Stack<T>",而不是Stack。
4.3 类模板的实例化
类模板的实例化和函数模板不太一样:函数模板很多时候靠推导(隐式),而类模板必须在名字后面跟<>,把具体类型放进去,手动指定。
intmain(){Stack<int>st1;// 存放 int 的栈Stack<double>st2;// 存放 double 的栈return0;}这里再强调一个很关键的概念,很多人一开始会懵:
Stack只是类模板的名字,它本身不是一个真正的类;Stack<int>才是真正的类型。
所以定义对象时,一定要写Stack<int>这种带尖括号的完整形式,而不是只写Stack。
五、小结与进阶预告
来,把今天的内容串一遍:
| 知识点 | 一句话记忆 |
|---|---|
| 泛型编程 | 写一份与类型无关的通用代码,实现代码复用 |
| 模板 | 泛型编程的基础,是"蓝图/模具",本身不是函数也不是类 |
| 函数模板 | 编译时根据实参类型推演T,现场生成对应函数 |
| 隐式实例化 | 编译器根据实参自动推导类型 |
| 显式实例化 | 在<>里手动指定类型,如Add<int>(...) |
| 匹配原则 | 完全匹配优先非模板,模板更合适才用模板 |
| 类模板 | Stack<int>才是真正的类型,Stack只是名字 |
本文属于模板系列初阶篇,我们把最核心的地基打好了。
但是!模板的世界远不止这些。后面的进阶篇还会带你搞定:
- 非类型模板参数(模板参数不只是类型)
- 模板的特化(全特化 & 偏特化)
- 模板的分离编译(
extern template那些事) - 可变参数模板(
...的魔法) - 模板与 STL 的关系
如果觉得这篇对你有帮助,欢迎点赞、收藏、关注,进阶篇正在路上,我们不见不散!🚀
参考:本人 C++ 模板初阶学习笔记整理。