文章目录
- 前言
- 一、为什么需要模板?从函数重载的痛点说起
-
- [1. 函数重载的局限](#1. 函数重载的局限)
- [2. 泛型编程与模板](#2. 泛型编程与模板)
- 二、模板初阶:函数模板与类模板
-
- [1. 函数模板](#1. 函数模板)
-
- [1.1 基本语法:](#1.1 基本语法:)
- [1.2 底层原理](#1.2 底层原理)
- [1.3 函数模板的实例化](#1.3 函数模板的实例化)
- [1.4 模板参数匹配原则](#1.4 模板参数匹配原则)
- [2. 类模板](#2. 类模板)
-
- [2.1 语法格式](#2.1 语法格式)
- [2.2 类外定义成员函数](#2.2 类外定义成员函数)
- [2.3 类模板的实例化](#2.3 类模板的实例化)
- 三、模板进阶:核心特性
- 总结
前言
在学习模板之前,想写一个通用交换函数,我们得靠函数重载写 N 个版本:int 版、double 版、char 版...... 逻辑完全重复,加新类型就要手动补代码。
有没有办法写一套通用逻辑,让编译器自动适配所有类型?这就是 C++ 的模板(Template)!
一、为什么需要模板?从函数重载的痛点说起
1. 函数重载的局限
我们想实现通用交换函数,用 C 语言 / 重载的思路得这么写:
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;
}
这就带来了两个问题:
复用率极低 :逻辑一模一样,只是类型不同,每加一种类型就要抄一遍代码;
维护成本高:一个逻辑出 bug,所有重载版本都要挨个修改,漏改就可能出 bug。
2. 泛型编程与模板
泛型编程的核心思想:编写与类型无关的通用代码,让编译器根据传入类型自动生成对应版本,实现代码复用。
生活化比喻:
模板就像月饼模具。模具本身不是月饼,但你倒入绿豆沙就出绿豆月饼,倒入莲蓉就出莲蓉月饼。一套模板,就可以适配所有你想要的口味(类型)。
C++ 模板分为两大类:
函数模板 :针对函数,生成不同类型的函数;
类模板:针对类,生成不同类型的类。
二、模板初阶:函数模板与类模板
1. 函数模板
1.1 基本语法:
cpp
template<typename T1, typename T2, ...>
返回值类型 函数名(参数列表) {
// 函数体
}
template:声明模板的关键字;typename:定义模板类型参数,也可以用class替代,二者完全等价(注意不能用struct);T:虚拟类型名,代表一种待定的类型。.
示例:通用交换函数
cpp
#include <iostream>
using namespace std;
template<typename T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
int main() {
int a = 10, b = 20;
Swap(a, b); // 自动生成int版Swap
double c = 1.1, d = 2.2;
Swap(c, d); // 自动生成double版Swap
char e = 'x', f = 'y';
Swap(e, f); // 自动生成char版Swap
return 0;
}
1.2 底层原理
模板本身不是真正的函数,只是一张 "蓝图"。编译阶段,编译器会根据实参类型推演出 T 的真实类型,然后生成一份对应类型的具体函数。

比如调用Swap(a,b)时,编译器生成void Swap(int&, int&);调用Swap(c,d)时,生成void Swap(double&, double&)。
简单说:我们写一套模板,编译器帮我们生成所有需要的重载。
1.3 函数模板的实例化
用具体类型调用模板、生成对应代码的过程叫实例化,分两种:
隐式实例化(最常用)
编译器根据实参自动推演类型:
cpp
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
Add(10, 20); // T推演为int
Add(1.1, 2.2); // T推演为double
return 0;
}
易错点:类型不一致导致推演失败
比如下面代码
cpp
Add(10, 20.5); // 编译报错!
第一个实参把 T 推成int,第二个推成double,但模板只有一个 T,编译器无法决策。

模板中编译器一般不会自动做类型转换,避免背锅。
两种解决方法:
1、手动强转:Add(10, (int)20.5);
2、显式实例化
显式实例化
手动在<>中指定类型:
cpp
Add<int>(10, 20.5); // 强制T为int,20.5自动转成int
类型不匹配时编译器会尝试隐式转换,转不了才报错。
1.4 模板参数匹配原则
当普通函数和同名模板同时存在时,编译器按三条规则匹配:
- 普通函数优先级更高:类型完全匹配时,优先调用普通函数,不会用模板生成;
- 模板更匹配则选模板:如果模板能生成更精准的版本(比如两个不同类型参数),则调用模板;
- 普通函数支持自动类型转换,模板不支持。
cpp
#include <iostream>
using namespace std;
// 普通int加法
int Add(int left, int right) {
cout << "普通int加法" << endl;
return left + right;
}
// 通用模板
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
cout << "通用模板" << endl;
return left + (T1)right;
}
int main() {
Add(1, 2); // 参数完全匹配普通函数,优先调用
Add(1, 2.0); // 普通函数需要转double,模板能生成更匹配版本 → 调用模板
return 0;
}

2. 类模板
函数模板解决了通用函数的问题,那类呢?比如栈,想支持 int 栈、double 栈、string 栈,总不能写三个类吧?这就需要类模板。
2.1 语法格式
cpp
template<class T1, class T2, ...>
class 类名 {
// 类成员
};
示例:通用栈类模板
cpp
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;
};
2.2 类外定义成员函数
类模板的成员在类外定义时,必须带上模板声明,作用域写类名<T>::
cpp
template<class T>
void Stack<T>::Push(const T& data) { // 类外定义
_array[_size] = data;
++_size;
}
提醒:模板不建议声明和定义分离到 .h 和 .cpp,会出现链接错误,后面进阶会详细讲。
2.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) { // 类外定义
_array[_size] = data;
++_size;
}
int main() {
Stack<int> st1; // int类型的栈
Stack<double> st2; // double类型的栈
st1.Push(10);
st2.Push(3.14);
return 0;
}
Stack:是类模板名 ,不是真正的类;
Stack<int>:才是实例化后的具体类型。
三、模板进阶:核心特性
1. 非类型模板参数
模板参数分两类:
- 类型形参 :用
typename/class修饰,代表一种类型(前面讲的都是); - 非类型形参 :用一个常量作为模板参数,模板内部可以当常量使用。
示例:定义固定大小的静态数组
cpp
template<class T, size_t N = 10>
class array {
public:
T& operator[](size_t index) {
return _array[index];
}
size_t size() const {
return N;
}
private:
T _array[N];
};
int main() {
array<int, 100> arr1; // 大小100的int数组
array<double> arr2; // 默认大小10的double数组
return 0;
}
N 就是非类型模板参数,编译期就能确定数组大小,不用动态开辟。
注意事项:
- 浮点数、类对象、字符串不能作为非类型模板参数;
- 非类型参数必须是编译期就能确定的常量(比如整型、枚举、指针引用等)。
2. 模板特化
2.1 为什么需要特化?
通用模板能处理大多数场景,但遇到特殊类型可能逻辑错误。比如实现一个比较大小的 Less 模板:
cpp
#include <iostream>
using namespace std;
class Date
{
public:
Date(int y = 1, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
// 重载小于运算符,实现日期大小比较
bool operator<(const Date& date) const
{
if (_year != date._year)
return _year < date._year;
if (_month != date._month)
return _month < date._month;
return _day < date._day;
}
private:
int _year, _month, _day;
};
template<class T>
bool Less(T left, T right) {
return left < right;
}
int main() {
cout << Less(1, 2) << endl; // 正确,比较int值
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 正确,比较Date对象
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 错误!比较的是指针地址,不是对象内容
return 0;
}
指针场景下,通用模板不符合预期。这时候就需要模板特化:针对特殊类型,单独写一套特殊实现。
2.2 函数模板特化
特化步骤:
- 先有基础函数模板;
template后跟空尖括号<>;- 模板函数名后
<>中指定要特化的类型; - 形参列表必须和主模板形参类型、个数、顺序完全一致。
cpp
// 基础模板
template<class T>
bool Less(T left, T right) { return left < right; }
// 针对Date*的特化版本
template<>
bool Less<Date*>(Date* left, Date* right) {
return *left < *right; // 解引用比较对象内容
}
调用Less(p1,p2)时,会自动匹配特化 Date* 版本。
函数模板一般不推荐特化,直接写一个对应类型的普通函数更简单直观,可读性更好。
2.3 类模板特化
类模板特化用得更多,分两种:全特化 和偏特化。
全特化
把模板所有参数都确定成具体类型:
cpp
#include <iostream>
using namespace std;
// 基础模板
template<class T1, class T2>
class Data {
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 全特化:两个参数都指定为int、char
template<>
class Data<int, char> {
public:
Data() { cout << "Data<int, char>" << endl; }
private:
int _d1;
char _d2;
};
int main() {
Data<int, int> d1; // 走基础模板
Data<int, char> d2; // 走全特化版本
return 0;
}

偏特化
对模板参数做进一步限制,不是全部指定。分两种表现形式:
形式 1:部分特化
只特化一部分参数:
cpp
// 第二个参数特化为int
template <class T1>
class Data<T1, int> {
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
形式 2:参数进一步限制
对参数类型加约束,比如特化为指针、引用:
cpp
// 两个参数都特化为指针
template <typename T1, typename T2>
class Data <T1*, T2*> {
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 两个参数都特化为引用
template <typename T1, typename T2>
class Data <T1&, T2&> {
public:
Data(const T1& d1, const T2& d2) : _d1(d1), _d2(d2) {
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
2.4 模板的分离编译
什么是分离编译?
一个项目由多个.cpp源文件组成,每个源文件单独编译生成目标文件,最后链接成可执行文件,这就是分离编译模式。
模板分离编译为什么会报错?
如果把模板声明放 .h,定义放 .cpp,调用时会出现链接错误:
cpp
// Add.h
template<class T>
T Add(const T& left, const T& right);
// Add.cpp
#include "Add.h"
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
// test.cpp
#include <iostream>
using namespace std;
#include "Add.h"
int main() {
Add(1, 2); // 链接报错!
Add(1.0, 2.0);// 链接报错!
return 0;
}

原因分析:
- 编译阶段:Add.cpp 里只有模板定义,没有实例化调用,编译器不会生成具体的
Add<int>、Add<double>代码; - 链接阶段:test.cpp 里调用了
Add<int>,但找不到对应函数的地址,直接报链接错误。
解决方法:
- 推荐做法:把模板的声明和定义放到同一个文件里(比如.hpp 或.h),编译时就能实例化生成代码;
- 显式实例化:在定义文件里手动指定实例化类型,不实用,不推荐。
总结
模板是 C++ 泛型编程的核心,也是 STL 模板库的底层基础,后续很多容器模拟都依赖模板实现。