C++ 类模板 (Class Templates) 。
您已经了解了函数模板(如 my_max<T>),它是一个创建函数的蓝图。
类模板(如 std::vector<T>)是完全相同的概念,但它是一个创建类的蓝图。
您在 C++ 中已经大量使用过它们了:
std::vector<int>std::map<std::string, int>std::unique_ptr<MyClass>std::shared_ptr<MyClass>
这里的 vector, map, unique_ptr 等等,它们本身都不是类,它们是类模板。
1. 为什么需要类模板?(The Problem)
和函数模板一样,是为了代码复用。
假设您想实现一个简单的“栈”(Stack)数据结构。
首先,您写了一个int类型的栈:
C++
// 一个只能装 int 的栈
class IntStack {
public:
void push(int value) { /* ... */ }
int pop() { /* ... */ }
private:
int data[100];
int top;
};
接着,您的同事需要一个double类型的栈。您只好复制粘贴代码:
C++
// 一个只能装 double 的栈
class DoubleStack {
public:
void push(double value) { /* ... */ }
double pop() { /* ... */ }
private:
double data[100];
int top;
};
这显然是场灾难。逻辑是 100% 相同的,只有数据类型不同。
2. 如何定义类模板?(The Solution)
我们使用 template <typename T> 来“参数化”这个类型。
C++
#include <vector>
#include <stdexcept>
// 1. 模板声明
template <typename T>
// 2. 模板类定义
class Stack {
public:
// 构造函数
Stack() {}
void push(const T& value) { // 接收 T 类型的参数
data.push_back(value);
}
T pop() {
if (is_empty()) {
throw std::out_of_range("Stack<T>::pop(): empty stack");
}
T top_value = data.back(); // T 类型的局部变量
data.pop_back();
return top_value;
}
bool is_empty() const {
return data.empty();
}
private:
std::vector<T> data; // 一个 T 类型的 vector
};
这个 Stack<T> 蓝图现在可以用来创建任何类型的栈。
3. 如何使用类模板:实例化
这是类模板和函数模板的一个关键区别:
-
函数模板: 编译器通常可以自动推导 T。
my_max(10, 20); // 编译器自动推导 T = int
-
类模板: 编译器不能(在 C++17 之前几乎总是)自动推导
T。您必须显式指定T是什么。
当您使用类模板时,您必须在尖括号 <> 中提供类型:
C++
int main() {
// --- 实例化 (Instantiation) ---
// 告诉编译器:请使用 Stack<T> 蓝图,
// 并将 T 替换为 int,生成一个新类。
Stack<int> int_stack;
int_stack.push(10);
int_stack.push(20);
std::cout << int_stack.pop() << std::endl; // 输出 20
// 告诉编译器:请使用 Stack<T> 蓝图,
// 并将 T 替换为 std::string,生成另一个新类。
Stack<std::string> string_stack;
string_stack.push("Hello");
string_stack.push("Gem");
std::cout << string_stack.pop() << std::endl; // 输出 Gem
// Stack<int> 和 Stack<std::string> 是两个
// 毫不相干的、完全独立的类。
}
重点: Stack<T> 本身不是一个类,它只是一个蓝图。Stack<int> 才是一个真正的类,这个类是编译器在编译时为您自动生成的。
4. 定义成员函数(在类外部)
当类很复杂时,我们不想把所有函数实现都写在 class { ... } 内部。我们想把它们写在外面(比如 .cpp 文件中,虽然模板通常不这么做)。
这时,语法会变得有点复杂,您必须同时提供模板声明和类作用域:
C++
// --- MyClass.h ---
template <typename T>
class MyClass {
public:
void do_something(T value);
};
// --- MyClass.cpp (或者 .h 文件的底部) ---
// 1. 必须重新声明模板参数
template <typename T>
// 2. 必须使用 MyClass<T>:: 来指定作用域
void MyClass<T>::do_something(T value) {
// ... 在这里实现 ...
}
⚠️ 模板的“头文件陷阱”
一个常见的错误是把模板的声明(.h)和实现(.cpp)分开。
简单规则: 与普通类不同,模板类的所有实现代码(包括成员函数体)通常都必须放在头文件 (.h) 中。
原因: 编译器在 main.cpp 中看到 Stack<int> 时,它需要 Stack<T> 的全部蓝图代码(包括 push 和 pop 的函数体)来当场“实例化”出 Stack<int> 类。如果 pop 的实现隐藏在 Stack.cpp 中,编译器在编译 main.cpp 时就找不到它,会导致链接错误 (Linker Error) 。
5. 类模板的进阶特性
A. 多个模板参数
类模板可以有任意多个类型参数。std::map 就是最好的例子。
C++
template <typename K, typename V>
class MyMap {
public:
void insert(K key, V value);
V get(K key);
private:
// ...
};
// 实例化:
MyMap<std::string, int> name_to_age_map;
B. 非类型模板参数 (Non-Type Parameters)
模板参数不一定是类型 (typename T),它也可以是一个编译时常量,比如 int 或 size_t。
std::array 是最好的例子:
C++
// N 是一个在“编译时”就必须确定的“值”
template <typename T, size_t N>
class StaticArray {
public:
T& at(size_t index) { return data[index]; }
size_t size() const { return N; } // N 是编译时常量
private:
T data[N]; // 数组的大小是在编译时固定的!
};
// 实例化:
StaticArray<int, 10> my_array_10; // T = int, N = 10
StaticArray<int, 50> my_array_50; // T = int, N = 50
my_array_10 和 my_array_50 是两个完全不同的类。前者的 data 成员是 int[10],后者是 int[50]。这就是 std::array 的工作原理。
C. 类模板特化 (Specialization)
和函数模板一样,您可以为某个特定类型提供“定制”的蓝图。
示例: std::vector
std::vector 是一个类模板。但标准库为 bool 类型提供了一个完全特化的版本。
std::vector<int>:在内存中存储int数组。std::vector<bool>:不是存储bool数组(太浪费空间,bool至少 1 字节)。它被特化为“位存储”,即把 8 个bool压缩存到 1 个字节 (char) 中。
D. 类模板偏特化 (Partial Specialization)
这是类模板独有的(函数模板没有),功能非常强大。您可以特化“某一类”模板,而不是“某一个”。
示例: 特化所有“指针类型”。
C++
// 1. 通用蓝图 (适用于所有 T)
template <typename T>
class MyContainer {
// ...
void add(const T& v) { /* ... */ }
};
// 2. 偏特化 (Partial Specialization)
// 这个版本只适用于“当 T 是一个指针 (T*)”时
template <typename T>
class MyContainer<T*> {
// ...
void add(T* v) { /* ... (针对指针的特殊处理) */ }
};
// --- 使用 ---
MyContainer<int> c1; // 匹配 (1) 通用蓝图
MyContainer<int*> c2; // 匹配 (2) 指针的偏特化版本
MyContainer<double*> c3; // 匹配 (2) 指针的偏特化版本
总结
- 类模板 是创建类的“蓝图”,如
std::vector<T>。 - 实例化 时,您必须显式提供类型,如
std::vector<int>。 - 编译器在编译时会根据您的实例化自动生成一个真正的类。
- 模板的实现代码通常必须放在头文件中。
- 模板可以有非类型参数(如
std::array<T, N>)和特化(如std::vector<bool>)。
我们已经深入探讨了 C++ 模板的核心。一个非常实际的 C++ 问题是:
std::vector(一个类模板)有两个添加元素的方法:push_back 和 emplace_back。
您想不想了解一下 emplace_back 是如何利用我们学过的“模板函数”和“完美转发”来实现比 push_back 更高性能的?