📌 相关专栏
-
【C++ 专栏】
📌 相关文章推荐
很高兴你点开这篇文章✨
这里会持续更新我喜欢的内容,关注我,一起慢慢变好呀
👍 点赞 ⭐ 收藏 💬 评论
文章目录
- 前言
- 一、函数模板
-
- [1.1 为什么需要函数模板?](#1.1 为什么需要函数模板?)
- [1.2 函数模板的语法](#1.2 函数模板的语法)
-
- [1.3 模板的使用](#1.3 模板的使用)
- 二、模板参数推导与实例化
-
- [2.1 隐式实例化](#2.1 隐式实例化)
- [2.2 参数不匹配时的处理](#2.2 参数不匹配时的处理)
- [2.3 多类型模板参数](#2.3 多类型模板参数)
- [2.4 显式实例化](#2.4 显式实例化)
- 三、模板重载与匹配规则
-
- [3.1 函数模板与普通函数可以重载](#3.1 函数模板与普通函数可以重载)
- [3.2 匹配优先级](#3.2 匹配优先级)
- 四、类模板
-
- [4.1 类模板的定义](#4.1 类模板的定义)
- [4.2 类模板成员函数的类外定义](#4.2 类模板成员函数的类外定义)
- [4.3 类模板的使用(显式实例化)](#4.3 类模板的使用(显式实例化))
- 五、模板的注意事项
-
- [5.1 声明与定义分离的问题](#5.1 声明与定义分离的问题)
- [5.2 模板的编译原理](#5.2 模板的编译原理)
- [5.3 模板的缺点](#5.3 模板的缺点)
- 六、完整示例:通用Stack类
- 七、知识点汇总
- 八、常见面试题
前言
在C语言中,如果我们要写一个交换两个整数的函数,再写一个交换两个浮点数的函数,我们只能写两个不同名的函数,或者用宏(但宏有很多坑)。
C++提供了模板来解决这个问题。有了模板,我们就可以写出类型相关的通用代码。
🐾 这一篇我们来学习:
- 函数模板:
如何写一个通用的Swap函数- 模板参数推导:
编译器如何自动推断类型- 显式实例化:
强制指定模板参数类型- 类模板:
如何写一个通用的Stack容器
🐶 🐾 ✨ 🐾 🐶
一、函数模板
1.1 为什么需要函数模板?
看看这个例子:我们需要交换两个变量的值,但int、double、char都需要写一个版本。
cpp
// 代码重复严重
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
🐾 函数模板
cpp
// 一个模板搞定所有类型
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
1.2 函数模板的语法
cpp
// template 关键字 + <typename T> 或 <class T>
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
// 多个模板参数
template<typename T1, typename T2>
void func(const T1& x, const T2& y)
{
// ...
}
注意 : typename和class在模板参数中完全等价,没有区别。
cpp
template<typename T> // 推荐(更语义化)
template<class T> // 也可以(C++早期用法)
1.3 模板的使用
cpp
int main()
{
int i = 1, j = 2;
double m = 1.1, n = 2.2;
Swap(i, j); // 编译器推导 T = int
Swap(m, n); // 编译器推导 T = double
// Swap(i, n); // 错误!T被推导成int还是double?矛盾
return 0;
}
🐶 🐾 ✨ 🐾 🐶
二、模板参数推导与实例化
2.1 隐式实例化
编译器会根据你传入的实参类型,自动推导模板参数:
cpp
template<typename T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, a2); // 隐式实例化:T → int
Add(d1, d2); // 隐式实例化:T → double
return 0;
}
2.2 参数不匹配时的处理
编译器根据你传入的实参类型,自动推导模板参数:
cpp
Add(a1, d1); // 错误:T被推导成int还是double?
🐾
解决方法1:强制类型转换
cpp
cout << Add(a1, (int)d1) << endl; // 都转成int
cout << Add((double)a1, d1) << endl; // 都转成double
🐾
解决方法2 :显式实例化(推荐)
cpp
cout << Add<int>(a1, d1) << endl; // 明确指定 T = int
cout << Add<double>(a1, d1) << endl; // 明确指定 T = double
2.3 多类型模板参数
cpp
template<typename T1, typename T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
int a1 = 10;
double d1 = 20.2;
cout << Add(a1, d1) << endl; // T1=int, T2=double,返回int
return 0;
}
2.4 显式实例化
cpp
template<typename T>
T* func1(int n)
{
return new T[n];
}
int main()
{
// 无法推导T,必须显式指定
int* p1 = func1<int>(10); // T → int
double* p2 = func1<double>(10); // T → double
delete[] p1;
delete[] p2;
return 0;
}
🐶 🐾 ✨ 🐾 🐶
三、模板重载与匹配规则
3.1 函数模板与普通函数可以重载
cpp
// 函数模板
template<typename T>
T Add(const T& left, const T& right)
{
cout << "template Add: ";
return left + right;
}
// 普通函数(特化版本)
int Add(const int& x, const int& y)
{
cout << "normal Add: ";
return (x + y) * 10;
}
int main()
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl;
// 输出:normal Add: 300
// 普通函数优先级更高
cout << Add<int>(a1, a2) << endl;
// 输出:template Add: 30
// 显式指定<>,强制调用模板
double d1 = 1.1, d2 = 2.2;
cout << Add(d1, d2) << endl;
// 输出:template Add: 3.3
// 没有匹配的普通函数,调用模板
return 0;
}
3.2 匹配优先级
| 优先级 | 匹配规则 |
|---|---|
1(最高) |
完全匹配的普通函数 |
| 2 | 通过模板实例化得到匹配函数 |
3(最低) |
通过类型转换匹配 |
🐶 🐾 ✨ 🐾 🐶
四、类模板
4.1 类模板的定义
cpp
template<typename T>
class Stack
{
public:
Stack(int n = 4)
: _array(new T[n])
, _size(0)
, _capacity(n)
{}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
void Push(const T& x);
private:
T* _array;
size_t _capacity;
size_t _size;
};
4.2 类模板成员函数的类外定义
关键 : 类外定义时需要加上template<typename T>,并用类名<T>::指定作用域。
cpp
// 类外定义成员函数
template<typename T>
void Stack<T>::Push(const T& x)
{
if (_size == _capacity)
{
// 扩容逻辑
T* tmp = new T[_capacity * 2];
memcpy(tmp, _array, sizeof(T) * _size);
delete[] _array;
_array = tmp;
_capacity *= 2;
}
_array[_size++] = x;
}
4.3 类模板的使用(显式实例化)
注意 : 类模板不支持隐式实例化,必须显式指定模板参数类型。
cpp
int main()
{
// 显式实例化:指定T为int
Stack<int> st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
// 显式实例化:指定T为double
Stack<double> st2;
st2.Push(1.1);
st2.Push(2.2);
st2.Push(3.3);
// 动态分配类模板对象
Stack<double>* pst = new Stack<double>;
pst->Push(10.5);
delete pst;
return 0;
}
🐶 🐾 ✨ 🐾 🐶
五、模板的注意事项
5.1 声明与定义分离的问题
模板的声明和定义通常不能分离到.h和.cpp文件中。
cpp
// Stack.h
template<typename T>
class Stack {
public:
void Push(const T& x);
};
// Stack.cpp 错误!链接时会找不到定义
template<typename T>
void Stack<T>::Push(const T& x) { /* ... */ }
🐾 解决办法:
将定义直接写在.h文件中或者在.cpp文件末尾显式实例化需要的类型
cpp
// Stack.cpp - 显式实例化
template class Stack<int>;
template class Stack<double>;
5.2 模板的编译原理
模板在编译阶段根据使用情况生成具体代码:
编译器看到模板定义时,不会生成代码编译器看到实例化(如Stack<int>)时,才会生成对应的类代码不同的实例化生成不同的类(Stack<int>和Stack<double>是不同类型)
5.3 模板的缺点
| 缺点 | 说明 |
|---|---|
| 编译慢 | 每次实例化都要重新生成代码 |
| 代码膨胀 | 不同类型生成多份代码 |
| 错误信息复杂 | 模板编译错误信息难以阅读 |
| 声明定义难分离 | 通常只能写在头文件 |
🐶 🐾 ✨ 🐾 🐶
六、完整示例:通用Stack类
cpp
#include<iostream>
#include<string>
using namespace std;
template<typename T>
class Stack
{
public:
Stack(int n = 4)
: _array(new T[n])
, _size(0)
, _capacity(n)
{
cout << "Stack()" << endl;
}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
cout << "~Stack()" << endl;
}
void Push(const T& x)
{
if (_size == _capacity)
{
Expand();
}
_array[_size++] = x;
}
void Pop()
{
if (_size > 0)
_size--;
}
T& Top()
{
return _array[_size - 1];
}
bool Empty() const
{
return _size == 0;
}
size_t Size() const
{
return _size;
}
private:
void Expand()
{
T* tmp = new T[_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;
};
int main()
{
// 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; // world
return 0;
}
🐶 🐾 ✨ 🐾 🐶
七、知识点汇总
| 知识点 | 核心要点 |
|---|---|
| 函数模板 | 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等)
- 迭代器的使用
🐶 🐾 ✨ 🐾 🐶

谢谢你看到这里呀
如果喜欢这篇内容,点个关注,下次更新不迷路✨
👍 点赞 ⭐ 收藏 💬 评论
