C++模板(初阶+进阶)
阅读须知:每个部分中的示例代码相连
一、模板的基本概念
模板是 C++泛型编程的核心工具,允许编写与类型无关的代码,提高代码复用性。模板分为函数模板和类模板,编译器会根据调用时的具体类型,生成对应的 “实例化代码”。
二、函数模板
1. 定义与语法
用 template<typename T> 或 template<class T>(typename 和 class 在此处等价)声明模板参数列表,随后定义函数逻辑。
示例:
template<class T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
2. 模板实例化
编译器根据调用时的实参类型,自动生成对应类型的函数(即 “实例化”)。分为两种方式:
(1)隐式实例化
编译器根据实参自动推演模板参数 T 的类型,生成函数实例。
示例:
int x = 0, y = 1;
Swap(x, y); // 推演T为int,生成 void Swap(int&, int&)
double d1 = 1.1, d2 = 2.2;
Swap(d1, d2); // 推演T为double,生成 void Swap(double&, double&)
(2)显式实例化
手动指定模板参数,格式为 函数模板名<具体类型>(实参)。
示例:
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
// 显式指定T为int,将double隐式转换为int后相加
cout << Add<int>(10, 20.5) << endl;
// 显式指定T为double,将int隐式转换为double后相加
cout << Add<double>(10, 20.5) << endl;
3. 函数模板与普通函数的重载匹配
- 规则:编译器优先匹配普通函数(有 “现成” 实现时);若普通函数不匹配,再考虑函数模板的实例化。
示例:
// 普通函数
int Add(int left, int right) {
return left + right;
}
// 函数模板
template<class T>
T Add(T left, T right) {
return left + right;
}
int main() {
Add(1, 2); // 优先调用普通函数 int Add(int, int)
Add<int>(1, 2); // 显式指定模板,调用函数模板实例化的 int Add(int, int)
Add(1.1, 2); // 普通函数不匹配(无double+int的普通Add),调用模板(2隐式转为double)
}
三、类模板
1. 定义与语法
用 template<class T1, class T2, ...> 声明模板参数,随后定义类结构。
示例(栈的类模板(栈实际用的是容器适配器,此处栈实现更像是array实现)):
template<class T, size_t N = 10> // N为“非类型模板参数”,默认值10
class Stack {
private:
T _a[N]; // 用模板参数指定数组大小
size_t _top;
public:
Stack() : _top(0) {}
void push(const T& val) { /* 入栈逻辑 */ }
void pop() { /* 出栈逻辑 */ }
// 其他成员函数...
};
2. 类模板的使用
类模板不能隐式推演类型,必须显式指定模板参数。
示例:
Stack<int, 20> s1; // T为int,N为20(覆盖默认值)
Stack<double> s2; // T为double,N使用默认值10
3. 成员函数的实例化
类模板的成员函数在调用时才会实例化(未被调用的成员函数,不会生成对应的二进制代码)。
成员函数可在类内定义,也可在类外定义(类外定义需显式指定模板参数):
template<class T, size_t N>
void Stack<T, N>::push(const T& val) {
if (_top < N) {
_a[_top++] = val;
}
}
进阶部分:
四、模板特化(Specialization)
为模板的特定类型提供 “定制化” 实现,解决通用模板在某些类型下的行为不符合预期的问题(如指针比较时,默认比较 “指针值” 而非 “指向对象的内容”)。
1. 函数模板特化
步骤:
- 先声明通用函数模板。
- 再定义特化版本,格式为
template<> 返回值 函数名<特化类型>(参数列表)。
示例(指针比较:比较指向对象的内容,而非指针本身):
// 通用模板:比较两个对象的大小
template<class T>
bool Less(const T& left, const T& right) {
return left < right;
}
// 特化版本:针对T为Date*的情况
template<>
bool Less<Date*>(Date* const& left, Date* const& right) {
return *left < *right; // 比较指针指向的Date对象内容
}
注意:
特化时,函数参数类型要与通用模板推演后的类型完全匹配(如示例中用 Date* const& 匹配 “引用传递的指针”)。
2. 类模板特化
分为全特化和偏特化(部分特化和限制)。
(1)全特化
对模板参数的所有可能都指定具体类型,格式为 template<> class 类名<具体类型1, 具体类型2>。
示例:
// 通用类模板
template<class T1, class T2>
class Data {
public:
Data() { cout << "Data<T1, T2>" << endl; }
};
// 全特化:T1为int,T2为char
template<>
class Data<int, char> {
public:
Data() { cout << "Data<int, char>" << endl; }
};
(2)偏特化(部分特化和限制)
对模板参数的部分进行限制,或对参数的特性(如指针、引用、常量等)进行限制。
-
示例 1:限制 “部分模板参数类型”(如第二个参数为
char):template<class T1> class Data<T1, char> { public: Data() { cout << "Data<T1, char>" << endl; } }; -
示例 2:限制 “模板参数为指针类型”:
template<class T1, class T2> class Data<T1*, T2*> { public: Data() { cout << "Data<T1*, T2*>" << endl; } }; -
示例 3:限制 “模板参数为引用类型”:
template<class T1, class T2> class Data<T1&, T2&> { public: Data() { cout << "Data<T1&, T2&>" << endl; } }; -
示例 4:混合限制(如第一个为指针,第二个为引用):
template<class T1, class T2> class Data<T1*, T2&> { public: Data() { cout << "Data<T1*, T2&>" << endl; } };
五、非类型模板参数
模板参数可以是常量表达式(而非仅 “类型”),这类参数称为 “非类型模板参数”。
1. 规则与限制
- 非类型模板参数的类型只能是:整数类型(
int、size_t等)、枚举类型、指针(对象 / 函数指针)、引用。 - 浮点数(如
double)、类对象(如std::string)不能作为非类型模板参数(C++20 前不支持;C++20 开始部分支持浮点数,但兼容性需注意)。
2. 示例
用非类型模板参数指定数组大小:
template<class T, size_t N = 10> // size_t是无符号整数类型,N为非类型参数,默认值10
class Stack {
private:
T _a[N]; // 数组大小由非类型参数N决定
size_t _top;
public:
Stack() : _top(0) {}
// ...
};
// 使用示例
Stack<int, 20> s1; // 显式指定N为20
Stack<double> s2; // 使用默认N=10
六、模板的编译机制

- 模板是 “蓝图 / 配方”,不是真正的可执行代码,只有在实例化时才会生成具体的函数 / 类代码。
- 模板的声明和定义通常放在同一个头文件中(若分离到
.cpp,其他文件调用时可能因看不到模板定义而无法实例化,导致链接错误)。 
当 func.h 只有模板声明,func.cpp 只有模板定义时,会直接导致链接错误,原因是模板实例化需要 “可见的完整定义”,而分离后编译阶段无法生成所需的实例化代码。
模板声明和定义分离(使得链接报错)的详细分析(结合编译流程):
1. test.cpp 的编译过程
// test.cpp
#include "func.h"
int main() {
vector<int> v = {1,2,3};
Print(v); // 调用 Print<vector<int>>
return 0;
}
func.h中只有Print模板的声明(无定义)。- 编译
test.cpp时,编译器看到Print(v),需要实例化Print<vector<int>>,但缺少模板的完整定义,因此无法生成Print<vector<int>>的具体代码,只能记录 “需要调用Print<vector<int>>,但函数未定义”。
2. func.cpp 的编译过程
// func.cpp
#include "func.h"
// 模板定义(但 func.cpp 中没有调用 Print 的代码)
template<class Container>
void Print(const Container& con) {
// 遍历容器的逻辑...
}
func.cpp中有Print模板的完整定义,但func.cpp自身没有调用Print的代码(无法用具体类型触发模板实例化)。- 编译
func.cpp时,编译器不会主动生成 “所有可能的Print实例化代码”(模板实例化是 “按需生成” 的),因此func.cpp编译后,不会包含Print<vector<int>>的代码。
3. 链接阶段的错误
test.cpp生成的目标文件(test.o):包含main函数,且记录了 “需要调用Print<vector<int>>,但该函数未定义”。func.cpp生成的目标文件(func.o):包含Print模板的通用定义,但没有Print<vector<int>>的具体实例化代码。- 链接时,链接器需要把
test.o和func.o合并,但找不到Print<vector<int>>的实际代码,因此报 “未定义的引用” 错误(如undefined reference to Print<std::vector<int, std::allocator<int>>>)。
极端情况下可以使用模板特化来显示实例化(可行不可取,用起来不现实),如下
(因为特化版本会在编译阶段直接生成具体的代码,链接)

七、标准库中的模板应用
标准库大量使用模板实现泛型:
- 容器(如
vector、list、map)是类模板,支持自定义存储类型。 - 算法(如
std::swap、std::sort)是函数模板,支持不同类型的元素操作。
示例(std::swap的核心逻辑简化):
template<class T>
void swap(T& a, T& b) {
T temp(a);
a = b;
b = temp;
}
(实际标准库的swap结合了移动语义等优化,但核心是模板实现泛型交换)
补充重点:
模板中 typename 的独有应用场景
template<class Container>
void Print(const Container& con)
{
//Container::const_iterator前必须要加typename ,因为编译器并不清楚Container::const_iterator是类型还是静态成员变量\函数
typename Container::const_iterator it = con.begin();
while (it != con.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
在 C++ 模板中,当使用 Container::const_iterator 这类 “嵌套依赖名称” 时,必须在前面加 typename,核心原因是编译器在模板定义阶段无法区分其是 “类型” 还是 “静态成员(函数\变量)”。
(1)核心问题:模板的 “延迟绑定” 特性
模板在定义时只是 “蓝图”,实际的类型(如 Container 具体是什么容器)要到实例化时才确定。对于 Container::const_iterator,编译器在模板定义阶段,不知道 Container 是类还是静态成员,因此无法判断 const_iterator 是:
- 情况 1:类型(比如
vector<int>::const_iterator是迭代器类型); - 情况 2:静态成员(比如
class Container { static int const_iterator; };,此时Container::const_iterator是静态变量)。
(2)typename 的作用
typename 关键字的作用是显式声明 “后续的嵌套名称是一个类”,消除编译器的歧义。如果不加 typename,编译器会 “保守假设” 其是 “静态成员变量 / 函数”,从而导致编译错误(用 “非类型” 定义变量是非法的)。
更多推荐


所有评论(0)