📌 相关专栏
- 【Linux专栏】
- 【C语言专栏】
- 【测试专栏】
- 【MySQL专栏】
- 【C++ 专栏】
📌 相关文章推荐
- 【C++】STL:从零掌握STL容器特性与实战用法
- 【C++】C++类与对象2:C++构造函数、运算符重载与流输入输出全面解析
- 【测试】一文吃透软件测试全分类,入门必懂核心体系
- 【Linux】一文搞懂HTTP协议:概念、报文格式与极简服务器实现
很高兴你点开这篇文章✨
这里会持续更新我喜欢的内容,关注我,一起慢慢变好呀
👍 点赞 ⭐ 收藏 💬 评论
文章目录
- 前言
- 一、函数模板
- 1.1 为什么需要函数模板?
- 1.2 函数模板的语法
- 1.3 模板的使用
- 二、模板参数推导与实例化
- 2.1 隐式实例化
- 2.2 参数不匹配时的处理
- 2.3 多类型模板参数
- 2.4 显式实例化
- 三、模板重载与匹配规则
- 3.1 函数模板与普通函数可以重载
- 3.2 匹配优先级
- 四、类模板
- 4.1 类模板的定义
- 4.2 类模板成员函数的类外定义
- 4.3 类模板的使用(显式实例化)
- 五、模板的注意事项
- 5.1 声明与定义分离的问题
- 5.2 模板的编译原理
- 5.3 模板的缺点
- 六、完整示例:通用Stack类
- 七、知识点汇总
- 八、常见面试题
前言
在C语言中,如果我们要写一个交换两个整数的函数,再写一个交换两个浮点数的函数,我们只能写两个不同名的函数,或者用宏(但宏有很多坑)。
C++提供了模板来解决这个问题。有了模板,我们就可以写出类型相关的通用代码。
🐾这一篇我们来学习:
- 函数模板:
如何写一个通用的Swap函数- 模板参数推导:
编译器如何自动推断类型- 显式实例化:
强制指定模板参数类型- 类模板:
如何写一个通用的Stack容器
🐶 🐾 ✨ 🐾 🐶
一、函数模板
1.1 为什么需要函数模板?
看看这个例子:我们需要交换两个变量的值,但int、double、char都需要写一个版本。
// 代码重复严重voidSwap(int&left,int&right){inttemp=left;left=right;right=temp;}voidSwap(double&left,double&right){doubletemp=left;left=right;right=temp;}voidSwap(char&left,char&right){chartemp=left;left=right;right=temp;}🐾函数模板
// 一个模板搞定所有类型template<typenameT>voidSwap(T&left,T&right){T temp=left;left=right;right=temp;}1.2 函数模板的语法
// template 关键字 + <typename T> 或 <class T>template<typenameT>voidSwap(T&x,T&y){T tmp=x;x=y;y=tmp;}// 多个模板参数template<typenameT1,typenameT2>voidfunc(constT1&x,constT2&y){// ...}注意:typename和class在模板参数中完全等价,没有区别。
template<typenameT>// 推荐(更语义化)template<classT>// 也可以(C++早期用法)1.3 模板的使用
intmain(){inti=1,j=2;doublem=1.1,n=2.2;Swap(i,j);// 编译器推导 T = intSwap(m,n);// 编译器推导 T = double// Swap(i, n); // 错误!T被推导成int还是double?矛盾return0;}🐶 🐾 ✨ 🐾 🐶
二、模板参数推导与实例化
2.1 隐式实例化
编译器会根据你传入的实参类型,自动推导模板参数:
template<typenameT>TAdd(constT&left,constT&right){returnleft+right;}intmain(){inta1=10,a2=20;doubled1=10.1,d2=20.2;Add(a1,a2);// 隐式实例化:T → intAdd(d1,d2);// 隐式实例化:T → doublereturn0;}2.2 参数不匹配时的处理
编译器根据你传入的实参类型,自动推导模板参数:
Add(a1,d1);// 错误:T被推导成int还是double?🐾
解决方法1:强制类型转换
cout<<Add(a1,(int)d1)<<endl;// 都转成intcout<<Add((double)a1,d1)<<endl;// 都转成double🐾
解决方法2:显式实例化(推荐)
cout<<Add<int>(a1,d1)<<endl;// 明确指定 T = intcout<<Add<double>(a1,d1)<<endl;// 明确指定 T = double2.3 多类型模板参数
template<typenameT1,typenameT2>T1Add(constT1&left,constT2&right){returnleft+right;}intmain(){inta1=10;doubled1=20.2;cout<<Add(a1,d1)<<endl;// T1=int, T2=double,返回intreturn0;}2.4 显式实例化
template<typenameT>T*func1(intn){returnnewT[n];}intmain(){// 无法推导T,必须显式指定int*p1=func1<int>(10);// T → intdouble*p2=func1<double>(10);// T → doubledelete[]p1;delete[]p2;return0;}🐶 🐾 ✨ 🐾 🐶
三、模板重载与匹配规则
3.1 函数模板与普通函数可以重载
// 函数模板template<typenameT>TAdd(constT&left,constT&right){cout<<"template Add: ";returnleft+right;}// 普通函数(特化版本)intAdd(constint&x,constint&y){cout<<"normal Add: ";return(x+y)*10;}intmain(){inta1=10,a2=20;cout<<Add(a1,a2)<<endl;// 输出:normal Add: 300// 普通函数优先级更高cout<<Add<int>(a1,a2)<<endl;// 输出:template Add: 30// 显式指定<>,强制调用模板doubled1=1.1,d2=2.2;cout<<Add(d1,d2)<<endl;// 输出:template Add: 3.3// 没有匹配的普通函数,调用模板return0;}3.2 匹配优先级
| 优先级 | 匹配规则 |
|---|---|
1(最高) | 完全匹配的普通函数 |
| 2 | 通过模板实例化得到匹配函数 |
3(最低) | 通过类型转换匹配 |
🐶 🐾 ✨ 🐾 🐶
四、类模板
4.1 类模板的定义
template<typenameT>classStack{public:Stack(intn=4):_array(newT[n]),_size(0),_capacity(n){}~Stack(){delete[]_array;_array=nullptr;_size=_capacity=0;}voidPush(constT&x);private:T*_array;size_t _capacity;size_t _size;};4.2 类模板成员函数的类外定义
关键:类外定义时需要加上template<typename T>,并用类名<T>::指定作用域。
// 类外定义成员函数template<typenameT>voidStack<T>::Push(constT&x){if(_size==_capacity){// 扩容逻辑T*tmp=newT[_capacity*2];memcpy(tmp,_array,sizeof(T)*_size);delete[]_array;_array=tmp;_capacity*=2;}_array[_size++]=x;}4.3 类模板的使用(显式实例化)
注意:类模板不支持隐式实例化,必须显式指定模板参数类型。
intmain(){// 显式实例化:指定T为intStack<int>st1;st1.Push(1);st1.Push(2);st1.Push(3);// 显式实例化:指定T为doubleStack<double>st2;st2.Push(1.1);st2.Push(2.2);st2.Push(3.3);// 动态分配类模板对象Stack<double>*pst=newStack<double>;pst->Push(10.5);deletepst;return0;}🐶 🐾 ✨ 🐾 🐶
五、模板的注意事项
5.1 声明与定义分离的问题
模板的声明和定义通常不能分离到.h和.cpp文件中。
// Stack.htemplate<typenameT>classStack{public:voidPush(constT&x);};// Stack.cpp 错误!链接时会找不到定义template<typenameT>voidStack<T>::Push(constT&x){/* ... */}🐾解决办法:
将定义直接写在.h文件中或者在.cpp文件末尾显式实例化需要的类型
// Stack.cpp - 显式实例化templateclassStack<int>;templateclassStack<double>;5.2 模板的编译原理
模板在编译阶段根据使用情况生成具体代码:
编译器看到模板定义时,不会生成代码编译器看到实例化(如Stack<int>)时,才会生成对应的类代码不同的实例化生成不同的类(Stack<int>和Stack<double>是不同类型)
5.3 模板的缺点
| 缺点 | 说明 |
|---|---|
| 编译慢 | 每次实例化都要重新生成代码 |
| 代码膨胀 | 不同类型生成多份代码 |
| 错误信息复杂 | 模板编译错误信息难以阅读 |
| 声明定义难分离 | 通常只能写在头文件 |
🐶 🐾 ✨ 🐾 🐶
六、完整示例:通用Stack类
#include<iostream>#include<string>usingnamespacestd;template<typenameT>classStack{public:Stack(intn=4):_array(newT[n]),_size(0),_capacity(n){cout<<"Stack()"<<endl;}~Stack(){delete[]_array;_array=nullptr;_size=_capacity=0;cout<<"~Stack()"<<endl;}voidPush(constT&x){if(_size==_capacity){Expand();}_array[_size++]=x;}voidPop(){if(_size>0)_size--;}T&Top(){return_array[_size-1];}boolEmpty()const{return_size==0;}size_tSize()const{return_size;}private:voidExpand(){T*tmp=newT[_capacity*2];for(size_t i=0;i<_size;i++){tmp[i]=_array[i];}delete[]_array;_array=tmp;_capacity*=2;}T*_array;size_t _capacity;size_t _size;};intmain(){// int栈Stack<int>intStack;intStack.Push(10);intStack.Push(20);intStack.Push(30);while(!intStack.Empty()){cout<<intStack.Top()<<" ";intStack.Pop();}cout<<endl;// 30 20 10// string栈Stack<string>strStack;strStack.Push("hello");strStack.Push("world");cout<<strStack.Top()<<endl;// worldreturn0;}🐶 🐾 ✨ 🐾 🐶
七、知识点汇总
| 知识点 | 核心要点 |
|---|---|
| 函数模板 | template + 函数定义 |
| 模板参数 | typename和class完全等价 |
| 隐式实例化 | 编译器自动推导参数类型 |
| 显式实例化 | FuncName(a, b)强制指定 |
| 类模板 | 必须显式实例化,如Stack |
| 类外定义 | 需template + Stack:: |
| 匹配优先级 | 普通函数 > 模板实例化 > 类型转换 |
| 编译特性 | 模板在实例化时才生成代码 |
🐶 🐾 ✨ 🐾 🐶
八、常见面试题
🐾Q1:typename和class在模板中有什么区别?
在模板参数中完全等价。但typename还可以用于嵌套依赖类型,例如typename T::iterator。
🐾Q2:函数模板可以隐式实例化,类模板为什么不行?
函数模板编译器可以实参推导,类模板没有推导依据(构造函数实参可以推导C++17开始支持)。C++17开始类模板也支持部分隐式推导(CTAD)。
🐾Q3:模板声明和定义为什么不能分离?
模板在实例化时才生成代码。如果定义在.cpp文件,其他文件包含.h时看不到定义,无法实例化,导致链接错误。
🐾Q4:模板代码膨胀怎么解决?
将不依赖模板参数的公共代码抽取到基类或单独的函数中。
🐾下一篇我们来学习:
- STL初识(vector、list、map等)
- 迭代器的使用
🐶 🐾 ✨ 🐾 🐶
谢谢你看到这里呀
如果喜欢这篇内容,点个关注,下次更新不迷路✨
👍 点赞 ⭐ 收藏 💬 评论