C++ 模板 全方位教程
目录
- 什么是模板?为什么需要它?
- 函数模板
- 基本语法
- 工作原理:模板实例化
- 示例:交换两个变量的值
- 类模板
- 基本语法
- 示例:一个通用的
Stack(栈) 类 - 在类模板外定义成员函数
- 模板参数
- 类型参数
- 非类型参数
- 默认模板参数 (C++11)
- 模板特化
- 全特化
- 偏特化
- 变参模板 (C++11)
- 语法简介
- 示例:打印任意数量、任意类型的参数
- 最佳实践与注意事项
- 模板与编译器错误信息
- 模板代码膨胀
constexpr与模板
什么是模板?为什么需要它?
想象一下,你需要编写一个函数来交换两个整数的值,然后再写一个函数来交换两个双精度浮点数的值,你会发现这两个函数的逻辑几乎完全一样,只是数据类型不同。

// 交换两个 int
void swap_int(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 交换两个 double
void swap_double(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}
为了处理不同的数据类型,你不得不编写几乎相同的代码,这导致了代码重复,违反了 DRY (Don't Repeat Yourself) 原则。
模板C++ 提供的解决方案,它允许你编写与类型无关的代码,可以用于处理多种数据类型,你可以把模板看作是“创建类或函数的蓝图或公式”。
使用模板,我们可以写一个通用的 swap 函数,让它适用于任何数据类型(只要该类型支持赋值操作)。
函数模板
函数模板用于创建一个通用的函数。

基本语法
使用 template 关键字,后面跟着一个模板参数列表(用 <> 括起来)。
template <typename T> // 或者 template <class T>
void my_function(T param) {
// 函数体,这里 T 是一个占位符,代表某种数据类型
}
template: 关键字,告诉编译器这是一个模板。typename T: 声明一个模板参数。T是一个类型参数,它代表一个未知的类型。class和typename在这里几乎可以互换使用,typename更能表达其“类型”的含义。
工作原理:模板实例化
当你在代码中调用一个函数模板时,编译器会根据你传递的实际参数的类型,自动推导出模板参数 T 的具体类型,并生成一个具体的、专用的函数版本,这个过程称为实例化。
如果你调用 my_function(10),编译器会实例化出 my_function(int);如果你调用 my_function(3.14),编译器会实例化出 my_function(double)。
示例:交换两个变量的值
让我们用函数模板来重写 swap 函数。

#include <iostream>
// 函数模板定义
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
std::cout << "Before swap (int): x = " << x << ", y = " << y << std::endl;
swap(x, y); // 编译器实例化 swap(int&, int&)
std::cout << "After swap (int): x = " << x << ", y = " << y << std::endl;
std::cout << "---------------------" << std::endl;
double d1 = 1.1, d2 = 2.2;
std::cout << "Before swap (double): d1 = " << d1 << ", d2 = " << d2 << std::endl;
swap(d1, d2); // 编译器实例化 swap(double&, double&)
std::cout << "After swap (double): d1 = " << d1 << ", d2 = " << d2 << std::endl;
return 0;
}
输出:
Before swap (int): x = 10, y = 20
After swap (int): x = 20, y = 10
---------------------
Before swap (double): d1 = 1.1, d2 = 2.2
After swap (double): d1 = 2.2, d2 = 1.1
注意: 传递给模板函数的参数类型必须匹配,你不能用 swap(x, d1),因为 x 是 int,d1 是 double,编译器无法找到一个能同时匹配 T& 和 U& 的模板(除非我们定义多个模板参数)。
类模板
类模板用于创建通用的类,标准库中的 std::vector, std::map, std::stack 等都是类模板的经典例子。
基本语法
template <typename T>
class MyClass {
public:
MyClass(T val) : value(val) {}
void print() {
std::cout << "Value: " << value << std::endl;
}
private:
T value; // 成员变量可以是模板类型
};
示例:一个通用的 Stack (栈) 类
栈是一种后进先出的数据结构,我们可以用模板来创建一个可以存储任意类型元素的栈。
#include <iostream>
#include <vector>
#include <string>
template <typename T>
class Stack {
private:
std::vector<T> elements; // 使用 std::vector 作为底层容器
public:
// 压入元素
void push(const T& element) {
elements.push_back(element);
}
// 弹出元素
void pop() {
if (elements.empty()) {
throw std::out_of_range("Stack<>::pop() : empty stack");
}
elements.pop_back();
}
// 返回栈顶元素
T top() const {
if (elements.empty()) {
throw std::out_of_range("Stack<>::top() : empty stack");
}
return elements.back();
}
// 检查栈是否为空
bool empty() const {
return elements.empty();
}
// 返回栈的大小
size_t size() const {
return elements.size();
}
};
int main() {
// 创建一个存储 int 的栈
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
std::cout << "Top of intStack: " << intStack.top() << std::endl; // 输出 20
// 创建一个存储 std::string 的栈
Stack<std::string> stringStack;
stringStack.push("Hello");
stringStack.push("World");
std::cout << "Top of stringStack: " << stringStack.top() << std::endl; // 输出 "World"
return 0;
}
在类模板外定义成员函数
成员函数的定义也需要在前面加上模板声明。
template <typename T>
void Stack<T>::push(const T& element) {
elements.push_back(element);
}
template <typename T>
void Stack<T>::pop() {
// ... 同上
}
模板参数
模板参数不仅仅是类型,还可以是具体的值。
类型参数
这是我们最常用的形式。
template <typename T, typename U> // 多个类型参数
void print(T a, U b) {
std::cout << a << ", " << b << std::endl;
}
非类型参数
非类型参数表示一个常量值,它的类型必须是整数、指针、引用或枚举。
// 创建一个固定大小的数组
template <typename T, int N>
class FixedArray {
private:
T data[N]; // 使用非类型参数 N 作为数组大小
public:
T& operator[](int index) {
return data[index];
}
};
int main() {
FixedArray<int, 10> arr; // 创建一个包含10个int的数组
arr[0] = 100;
std::cout << arr[0] << std::endl; // 输出 100
// FixedArray<int, -5> arr2; // 编译错误,N必须是常量正整数
}
默认模板参数 (C++11)
和函数参数一样,模板参数也可以有默认值。
template <typename T, typename U = int>
class MyClass {
// ...
};
int main() {
MyClass<double> m1; // T 是 double, U 是 int (使用默认值)
MyClass<char, float> m2; // T 是 char, U 是 float (覆盖默认值)
}
模板特化
有时,我们希望为特定类型提供与通用模板不同的实现,这时就需要模板特化。
全特化
为所有模板参数提供特定的类型。
假设我们有一个通用的比较函数模板:
template <typename T>
bool is_equal(const T& a, const T& b) {
return a == b;
}
对于 double 类型,由于浮点数精度问题,直接 比较可能不准确,我们可以为 double 类型提供一个特化版本。
// 全特化,针对 T = double
template <>
bool is_equal<double>(const double& a, const double& b) {
// 使用一个很小的误差范围来比较
const double epsilon = 1e-10;
return std::abs(a - b) < epsilon;
}
// 也可以省略 <double>,编译器能推断出来
template <>
bool is_equal(const double& a, const double& b) {
const double epsilon = 1e-10;
return std::abs(a - b) < epsilon;
}
偏特化
当模板有多个参数时,我们可以只特化其中的一部分。
一个模板有两个类型参数 T1 和 T2,我们可以特化 T1 为 int,而 T2 保持通用。
template <typename T1, typename T2>
class MyClass {
public:
void print() {
std::cout << "Generic version" << std::endl;
}
};
// 偏特化:当 T1 是指针时
template <typename T1, typename T2>
class MyClass<T1*, T2> {
public:
void print() {
std::cout << "Specialized version for T1*" << std::endl;
}
};
// 偏特化:当 T2 是 const char* 时
template <typename T1>
class MyClass<T1, const char*> {
public:
void print() {
std::cout << "Specialized version for T2 = const char*" << std::endl;
}
};
变参模板 (C++11)
变参模板可以接受任意数量、任意类型的参数,这在编写元编程或高级通用库时非常有用。
语法简介
使用 来表示可变参数。
typename... Args: 表示一个包,里面是0个或多个类型。Args...: 表示一个包,里面是0个或多个值。
示例:打印任意数量、任意类型的参数
这是一个经典的递归实现。
#include <iostream>
// 基础情况:当参数包为空时,递归结束
void print() {
std::cout << "End of recursion." << std::endl;
}
// 递归情况:打印第一个参数,然后处理剩余参数
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // 递归调用,参数包被“解包”
}
int main() {
print(1, 3.14, "Hello", 'A');
std::cout << std::endl;
print("Only one argument");
std::cout << std::endl;
print(); // 调用基础情况
return 0;
}
输出:
1 3.14 Hello A
Only one argument
End of recursion.
最佳实践与注意事项
模板与编译器错误信息
模板代码的错误信息通常非常冗长和复杂,因为编译器需要在你实例化模板时才能发现错误,如果你在模板函数中调用了一个不存在的方法,错误信息会指向模板调用的地方,而不是模板定义本身,学会阅读和理解这些错误信息是使用模板的关键技能。
模板代码膨胀
为每种类型都实例化一个模板,会导致生成大量的机器码。std::vector<int> 和 std::vector<double> 是两个完全独立的类,有自己的成员函数实现,如果模板被用于很多不同的类型,最终的可执行文件体积会显著增加,对于性能极其敏感的场景,这可能需要考虑。
constexpr 与模板
constexpr 函数可以在编译期求值,而模板实例化也发生在编译期,将两者结合可以创造出强大的编译期计算能力,是现代 C++ 元编程的基础。
// 编译期阶乘计算
template <int N>
constexpr int factorial() {
return N * factorial<N - 1>();
}
template <>
constexpr int factorial<0>() {
return 1;
}
int main() {
// 在编译期计算 5!
constexpr int result = factorial<5>();
static_assert(result == 120, "Compile-time error!");
std::cout << result << std::endl;
}
| 特性 | 描述 |
|---|---|
| 函数模板 | 创建与类型无关的通用函数。 |
| 类模板 | 创建与类型无关的通用类。 |
| 模板参数 | 可以是类型 (typename T) 或非类型值 (int N)。 |
| 模板实例化 | 编译器根据调用时提供的实际类型,生成具体的函数或类。 |
| 模板特化 | 为特定类型或类型组合提供定制化的实现。 |
| 变参模板 | 接受任意数量和任意类型的参数,功能强大。 |
模板是 C++ 的核心特性之一,是实现泛型编程 的基石,掌握模板能让你写出更通用、更优雅、更高效的代码,也是通往 C++ 高级编程和现代 C++ 的必经之路。
