C++ 模板 全方位教程

目录

  1. 什么是模板?为什么需要它?
  2. 函数模板
    • 基本语法
    • 工作原理:模板实例化
    • 示例:交换两个变量的值
  3. 类模板
    • 基本语法
    • 示例:一个通用的 Stack (栈) 类
    • 在类模板外定义成员函数
  4. 模板参数
    • 类型参数
    • 非类型参数
    • 默认模板参数 (C++11)
  5. 模板特化
    • 全特化
    • 偏特化
  6. 变参模板 (C++11)
    • 语法简介
    • 示例:打印任意数量、任意类型的参数
  7. 最佳实践与注意事项
    • 模板与编译器错误信息
    • 模板代码膨胀
    • constexpr 与模板

什么是模板?为什么需要它?

想象一下,你需要编写一个函数来交换两个整数的值,然后再写一个函数来交换两个双精度浮点数的值,你会发现这两个函数的逻辑几乎完全一样,只是数据类型不同。

c template 教程
(图片来源网络,侵删)
// 交换两个 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 函数,让它适用于任何数据类型(只要该类型支持赋值操作)。


函数模板

函数模板用于创建一个通用的函数。

c template 教程
(图片来源网络,侵删)

基本语法

使用 template 关键字,后面跟着一个模板参数列表(用 <> 括起来)。

template <typename T> // 或者 template <class T>
void my_function(T param) {
    // 函数体,这里 T 是一个占位符,代表某种数据类型
}
  • template: 关键字,告诉编译器这是一个模板。
  • typename T: 声明一个模板参数。T 是一个类型参数,它代表一个未知的类型。classtypename 在这里几乎可以互换使用,typename 更能表达其“类型”的含义。

工作原理:模板实例化

当你在代码中调用一个函数模板时,编译器会根据你传递的实际参数的类型,自动推导出模板参数 T 的具体类型,并生成一个具体的、专用的函数版本,这个过程称为实例化

如果你调用 my_function(10),编译器会实例化出 my_function(int);如果你调用 my_function(3.14),编译器会实例化出 my_function(double)

示例:交换两个变量的值

让我们用函数模板来重写 swap 函数。

c template 教程
(图片来源网络,侵删)
#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),因为 xintd1double,编译器无法找到一个能同时匹配 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;
}

偏特化

当模板有多个参数时,我们可以只特化其中的一部分。

一个模板有两个类型参数 T1T2,我们可以特化 T1int,而 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++ 的必经之路。