在编程中,我们经常会遇到这样的情况:写的代码逻辑完全相同,只是处理的数据类型不同。比如,我们想要一个交换两个变量的函数,int类型写一遍,double类型写一遍,char类型再写一遍......这不仅枯燥,还容易出错。
C++ 提供了模板这一强大工具,让编译器帮我们"自动生成"这些重复的代码。今天我们就来聊聊模板的入门知识。
一、泛型编程是什么?
泛型编程,简单来说,就是编写与类型无关的通用代码。它是一种代码复用的手段,而模板正是实现泛型编程的基础。
你可以把模板想象成一个"模具",我们往里面填充不同的类型,它就能生成对应类型的代码。这样,我们只需要维护一份代码,编译器会根据我们使用时的类型,自动生成多个版本。
二、函数模板
1. 函数模板的概念
函数模板代表了一个函数家族,它本身不是函数,而是告诉编译器如何生成函数的一个"蓝图"。
2. 函数模板的格式
cpp
template<typename T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
-
template是定义模板的关键字。 -
typename T表示定义一个类型参数T,你也可以用class T,但不能用struct。 -
T可以任意命名,通常习惯用大写字母。
3. 函数模板的原理
编译器在编译阶段,会根据函数模板的使用情况,推演出具体的类型,并生成对应的函数代码。
比如你调用 Swap(a, b),如果 a 和 b 都是 int 类型,编译器就会生成一个 int 版本的 Swap 函数。如果是 double,就生成 double 版本。这一切都在编译期完成,不会影响运行效率。
4. 函数模板的实例化
使用函数模板时,编译器会帮我们生成具体类型的函数,这个过程叫实例化。它分为两种:
隐式实例化
让编译器根据实参自动推导模板参数类型:
cpp
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2); // T 被推导为 int
Add(d1, d2); // T 被推导为 double
// Add(a1, d1); // 错误!T 无法同时推导为 int 和 double
return 0;
}
当模板参数只有一个 T,但传入不同类型时,编译器无法确定 T 应该是什么类型,会报错。解决方法有两种:
-
强制类型转换:
Add(a1, (int)d1); -
显式实例化
显式实例化
在函数名后加上 <类型>,显式指定模板参数的类型:
cpp
int main() {
int a = 10;
double b = 20.0;
Add<int>(a, b); // 强制使用 int 版本,b 会被隐式转换为 int
return 0;
}
5. 模板参数的匹配原则
-
如果有普通函数 和函数模板同时存在,且普通函数完全匹配,编译器会优先调用普通函数。
-
如果模板能生成一个更匹配的函数,编译器会选择模板。
cpp
#include <iostream>
using namespace std;
// 专门处理int的加法函数
int Add(int left, int right) {
return left + right;
}
// 通用加法函数(两个模板参数)
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
return left + right;
}
void Test() {
cout << Add(1, 2)<<endl; // 调用普通函数(精确匹配)
cout << Add(1.5, 3); // 调用模板,生成 Add(double,int) 版本,更匹配
}
int main() {
Test();
return 0;
}

ADD(1.5,3)返回值类型是 double的唯一原因:第一个参数 1 是 int 类型。,
这就是模板类型推导的核心规则:函数调用时的实参类型,决定了模板参数的类型,进而决定了返回值类型。
- 普通函数支持自动类型转换,而模板函数一般不会进行隐式类型转换(除非显式实例化或强制转换)。
三、类模板
1. 类模板的定义格式
类模板的定义和函数模板类似,使用 template<class T> 放在类定义前。
cpp
template<typename T>
class Stack {
public:
Stack(size_t capacity = 4) {
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
2. 类模板的成员函数定义
如果成员函数在类外定义,需要加上模板声明,并指定类模板的实例化类型:
cpp
template<class T>
void Stack<T>::Push(const T& data) {
// ======================
// 扩容逻辑(直接写在这里)
// ======================
if (_size == _capacity) {
// 1. 计算新容量(2倍扩容)
size_t newCapacity = _capacity * 2;
// 2. 开辟新空间
T* temp = new T[newCapacity];
// 3. 拷贝旧数据
for (size_t i = 0; i < _size; ++i) {
temp[i] = _array[i];
}
// 4. 释放旧空间
delete[] _array;
// 5. 指向新空间 + 更新容量
_array = temp;
_capacity = newCapacity;
}
注意:模板的声明和定义通常不建议分离到 .h 和 .cpp 文件中,否则容易出现链接错误。一般将模板的实现也放在头文件中。
3. 类模板的实例化
类模板的实例化必须显式指定类型,与函数模板不同:
cpp
#include <iostream>
using namespace std;
template<typename T>
class Stack {
public:
Stack(size_t capacity = 4) {
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
// 析构函数:释放内存
~Stack() {
delete[] _array;
_array = nullptr;
}
// 入栈 + 扩容逻辑 完全写在一起
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
// 入栈 + 扩容 写在一起,不拆分函数
template<class T>
void Stack<T>::Push(const T& data) {
// ======================
// 扩容逻辑(直接写在这里)
// ======================
if (_size == _capacity) {
// 1. 计算新容量(2倍扩容)
size_t newCapacity = _capacity * 2;
// 2. 开辟新空间
T* temp = new T[newCapacity];
// 3. 拷贝旧数据
for (size_t i = 0; i < _size; ++i) {
temp[i] = _array[i];
}
// 4. 释放旧空间
delete[] _array;
// 5. 指向新空间 + 更新容量
_array = temp;
_capacity = newCapacity;
}
// 正常入栈
_array[_size] = data;
++_size;
}
// 测试
int main() {
Stack<int> st;//int 类型的栈
for (int i = 1; i <= 10; ++i) {
st.Push(i);
cout << "入栈:" << i << endl;
}
Stack<double>* pst = new Stack<double>;//double类型的栈
pst->Push(3.14);
cout << "入栈:" << 3.14 << endl;
return 0;
}
4.Stack<int> st; 和 Stack<double>* pst = new Stack<double>不同:
1. 先看两个写法
写法 1:栈上对象(局部变量)
Stack<int> st;
写法 2:堆上对象(动态分配)
Stack<double>* pst = new Stack<double>;
2. 最核心区别(必背)
① Stack<int> st;
- 在栈内存上创建对象
- 生命周期自动管理 :函数结束,对象自动销毁,析构函数自动调用
- 不需要手动 delete
- 变量名
st就是对象本身
② Stack<double>* pst = new Stack<double>;
- 在堆内存上创建对象
- 生命周期手动管理 :必须自己 delete,否则内存泄漏
pst是指针,指向堆上的对象- 访问成员要用
->,不能用.
3.内存泄露不同
1. 先看 Stack<int> st;(栈对象)
- 对象在栈上
- 函数结束 → 自动调用析构函数~Stack ()
那如果析构函数里没写 delete [] 会怎样?
结果:
st这个栈对象本身确实会自动销毁- 但是!
_array指向的堆数组,永远留在堆里没人管 → 内存泄漏
2. 再看 new Stack<double>(堆对象)
Stack<double>* pst = new Stack<double>;
- 对象本体在堆上
- 指针
pst在栈上 - 只有你写
delete pst;才会触发:- 调用析构函数~Stack ()在析构函数里执行:
delete[] _array; - 释放对象本身的堆内存 释放
new出来的Stack本体
- 调用析构函数~Stack ()在析构函数里执行:
如果你不写 delete pst
- 析构函数永远不会执行
- 里面的
delete[] _array根本跑不到 - 双重内存泄漏:
_array指向的数组泄漏- Stack 对象本身也泄漏
4.最终结论(必须记住)
- 栈对象:析构函数自动调用
- 堆对象:析构函数只有 delete 时才调用
- 不管栈还是堆,
_array都是你自己 new 的,必须靠析构里的 delete [] 释放 - 堆对象你不 delete → 析构不跑 → 内部数组直接泄漏