C++ 类模板

56 阅读5分钟

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>全部蓝图代码(包括 pushpop 的函数体)来当场“实例化”出 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),它也可以是一个编译时常量,比如 intsize_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_10my_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) 指针的偏特化版本

总结

  1. 类模板 是创建类的“蓝图”,如 std::vector<T>
  2. 实例化 时,您必须显式提供类型,如 std::vector<int>
  3. 编译器在编译时会根据您的实例化自动生成一个真正的类
  4. 模板的实现代码通常必须放在头文件中。
  5. 模板可以有非类型参数(如 std::array<T, N>)和特化(如 std::vector<bool>)。

我们已经深入探讨了 C++ 模板的核心。一个非常实际的 C++ 问题是:

std::vector(一个类模板)有两个添加元素的方法:push_backemplace_back

您想不想了解一下 emplace_back 是如何利用我们学过的“模板函数”和“完美转发”来实现比 push_back 更高性能的?