一、从函数重载到泛型编程

想象一下,当你需要一个交换两个变量的函数,它可以处理 int、double、string……如果用函数重载,你要购买的int版、double版、string版各写一份,代码完全相同只是类型不同。这既繁琐又容易出错。

C++ 的模板机制就是让你只写一份代码,然后告诉编译器:“这里的类型先用 T 替代,你根据实际调用时的参数类型自己生成对应的代码”。

这种编程思想称为泛型编程(Generic Programming),即编写与类型无关的通用代码,实现代码复用。模板是泛型编程的基础。

二、函数模板

函数模板代表了一个函数家族,它本身不是函数,而是编译器用于生成具体函数的模具。

2.1 格式与原理

示例 1:函数模板定义与使用

#include <iostream>
#include <string>

template<typename T>
T Max(T a, T b) {
    return a > b ? a : b;
}

int main() {
    // 隐式实例化:编译器根据实参自动推测 T
    std::cout << Max(3, 7) << "\n";          // int
    std::cout << Max(3.14, 2.71) << "\n";    // double
    std::cout << Max(std::string("Cat"),
                     std::string("Dog")) << "\n"; // string
    return 0;
}

在编译阶段,编译器看到 Max(3, 7) 时,会根据实参 3 和 7 的类型将 T 推测为 int,然后生成一份专门处理 int 的 Max 函数。这个过程完全由编译器自动完成,对用户透明。

2.2 隐式实例化与显式实例化

模板的实例化是指编译器根据实际类型生成具体代码的过程,分为两种方式:

  • 隐式实例化:编译器根据实参自动推测类型,最简洁
  • 显式实例化:在函数名后用 <> 指定类型,编译器会尝试进行隐式类型转换

示例 2:隐式与显式实例化对比

#include <iostream>

template<typename T>
T Add(T a, T b) {
    return a + b;
}

int main() {
    int x = 10, y = 20;
    double p = 3.14, q = 2.71;

    // 隐式实例化:类型自动推测
    std::cout << Add(x, y) << "\n";       // T=int
    std::cout << Add(p, q) << "\n";       // T=double

    // Add(x, p);  // 编译错误:无法确定 T 是 int 还是 double

    // 显式实例化:指定类型,编译器会尝试隐式转换
    std::cout << Add<double>(x, p) << "\n"; // T=double, int被转为double
    return 0;
}

注意:模板中编译器一般不会进行类型转换操作。当 Add(x, p) 时,一个是 int 一个是 double,编译器无法确定 T 到底应该是哪个类型,因此会报错。解决办法是用显式实例化或者自己强制转换。

2.3 模板参数的匹配原则

当同时存在普通函数和函数模板时,编译器遵循以下规则:

  • 如果普通函数与实参完全匹配,优先调用普通函数(不会从模板实例化)
  • 如果模板可以生成更好匹配的版本,则优先使用模板
  • 普通函数支持自动类型转换,模板函数不支持

示例 3:模板匹配规则

#include <iostream>

// 普通函数
int Max(int a, int b) {
    std::cout << "Normal Max: ";
    return a > b ? a : b;
}

// 函数模板
template<typename T>
T Max(T a, T b) {
    std::cout << "Template Max: ";
    return a > b ? a : b;
}

int main() {
    std::cout << Max(3, 7) << "\n";        // 调用普通函数
    std::cout << Max(3.14, 2.71) << "\n";  // 调用模板(普通函数不匹配)
    std::cout << Max<int>(3, 7) << "\n";   // 显式指定,调用模板
    return 0;
}

三、类模板

与函数模板类似,类模板让类的成员变量和成员函数也能处理任意类型。这在学习类与对象时的 Vector、Stack 等数据结构中非常常见。

3.1 定义格式

示例 4:类模板——简单的 Stack

#include <iostream>

template<typename T>
class Stack {
public:
    Stack(int cap = 4)
        : _data(new T[cap]), _size(0), _capacity(cap) {}

    ~Stack() { delete[] _data; }

    void push(const T& val) {
        if (_size == _capacity) {
            _capacity *= 2;
            T* tmp = new T[_capacity];
            for (int i = 0; i < _size; ++i) tmp[i] = _data[i];
            delete[] _data;
            _data = tmp;
        }
        _data[_size++] = val;
    }

    T pop() { return _data[--_size]; }
    bool empty() const { return _size == 0; }

private:
    T* _data;
    int _size;
    int _capacity;
};

int main() {
    Stack<int> st;
    st.push(10);
    st.push(20);
    st.push(30);
    while (!st.empty())
        std::cout << st.pop() << " ";
    std::cout << "\n";

    Stack<double> st2;
    st2.push(1.5);
    st2.push(2.5);
    std::cout << st2.pop() << "\n";

    return 0;
}

注意:Stack 是类模板名,Stack<int> 和 Stack<double> 才是真正的类。类模板的实例化必须显式指定类型,不能像函数模板那样隐式推测。

3.2 成员函数的类外定义

如果将类模板的成员函数定义在类外,需要重复模板参数列表,并用类型 Stack<T> 指明所属类。

示例 5:类模板成员函数类外定义

#include <iostream>
#include <string>

template<typename T>
class Wrapper {
public:
    Wrapper(const T& val);
    void show() const;
private:
    T _val;
};

// 类外定义:需要重复 template<typename T>
template<typename T>
Wrapper<T>::Wrapper(const T& val) : _val(val) {}

template<typename T>
void Wrapper<T>::show() const {
    std::cout << "Value: " << _val << "\n";
}

int main() {
    Wrapper<int> w1(42);
    Wrapper<std::string> w2("Hello Templates!");
    w1.show();
    w2.show();
    return 0;
}

四、总结与展望

本篇介绍了模板的基础知识:

  • 泛型编程的思想——让类型成为参数,代码复用
  • 函数模板的定义、隐式与显式实例化、匹配原则
  • 类模板的定义、实例化、成员函数类外定义

模板是 C++ 极其强大的特性,它让数据结构和算法与类型解耦,为后续学习 STL(标准模板库)奠定了基础。在后续的内容中,我们还将探讨模板特化、变参模板、模板元编程等更深入的话题。

更多推荐