第16章 模板基础:函数模板与类模板

模板,说白了就是C++里的「代码模具」。你写一次逻辑,编译器帮你生成针对不同类型的版本。我刚开始学的时候觉得这玩意儿挺玄乎,后来在项目里写了一个通用排序函数,才发现——嗯,真香。

16.1 函数模板的定义与实例化

先看一个最简单的例子。假设你要写一个交换两个变量的函数:

template<typename T>
void mySwap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

这里的 template<typename T> 就是模板头,T 是类型参数。你调用的时候,编译器会根据实参自动推导出 T 是什么类型。

实例化方式有两种:

  • 隐式实例化:编译器根据实参自动推导类型
  • 显式实例化:你手动指定类型参数
int a = 1, b = 2;
mySwap(a, b);           // 隐式实例化,T 推导为 int

double x = 1.5, y = 2.5;
mySwap<double>(x, y);   // 显式实例化,T 指定为 double

我个人习惯用隐式实例化,代码更简洁。但有些场景必须显式指定——比如函数参数类型和模板参数类型不一致的时候。

小技巧:如果你想让模板只接受特定类型,可以用 static_assert 做类型检查。我在项目中就遇到过有人传了个自定义类进来,结果交换逻辑全乱套了。

16.2 类模板的定义与实例化

函数模板处理的是函数,类模板处理的就是类了。最常见的例子——栈:

template<typename T>
class Stack {
private:
    std::vector<T> data;
public:
    void push(const T& val) { data.push_back(val); }
    T pop() {
        T val = data.back();
        data.pop_back();
        return val;
    }
    bool empty() const { return data.empty(); }
};

实例化类模板时,必须显式指定类型参数:

Stack<int> intStack;       // 存储 int 的栈
Stack<std::string> strStack; // 存储 string 的栈

你想想看,如果没有模板,你得为每种类型写一个栈类——int栈、double栈、string栈……那代码量,啧啧。

注意:类模板的成员函数只有在被调用时才会实例化。这意味着即使模板中有语法错误,只要你不调用那个函数,编译器就不会报错。我曾经被这个特性坑过一次——写了个模板类,某个成员函数里有个拼写错误,测试没覆盖到,上线后直接崩了。

16.3 模板参数:类型参数与非类型参数

模板参数分两种:类型参数和非类型参数。

参数类型关键字示例说明
类型参数typename / classtemplate<typename T>代表任意类型
非类型参数int / size_t 等template<int N>代表一个编译期常量

非类型参数有什么用?举个例子——固定大小的数组:

template<typename T, int Size>
class FixedArray {
private:
    T arr[Size];
public:
    int size() const { return Size; }
    T& operator[](int index) { return arr[index]; }
};

FixedArray<double, 10> fa;  // 10 个 double 的数组

这里的 Size 就是非类型参数,它必须在编译期确定。你可以传整数、枚举、指针等,但不能传浮点数或字符串字面量。

避坑指南:非类型模板参数必须是编译期常量表达式。我曾经试图传一个变量进去:

int n = 10;
FixedArray<int, n> arr;  // 编译错误!n 不是常量表达式

正确的做法是用 constexprconst int

16.4 模板的特化

有时候,通用模板不能满足所有类型的需求。比如你写了一个通用的比较函数,但遇到 const char* 时,你想比较字符串内容而不是指针地址。这时候就需要特化。

// 通用模板
template<typename T>
bool compare(const T& a, const T& b) {
    return a == b;
}

// 特化版本:针对 const char*
template<>
bool compare<const char*>(const char* const& a, const char* const& b) {
    return strcmp(a, b) == 0;
}

特化用 template<> 开头,后面跟具体的类型。编译器会优先匹配特化版本。

我的经验:特化虽然好用,但别滥用。能用重载解决的问题,就别搞特化。特化多了,代码维护起来跟迷宫似的。

16.5 知识体系图

下面这张图帮你理清本章的核心脉络:

C++ 模板 函数模板 template<typename T> 隐式实例化 / 显式实例化 类模板 template<typename T> 必须显式指定类型 模板参数 类型参数 typename / class 代表任意类型 非类型参数 int / size_t 等 编译期常量 模板特化 template<> 针对特定类型定制 核心:一次编写,多种类型复用

16.6 总结

模板是C++泛型编程的基石。函数模板让你写一次逻辑,适配多种类型;类模板让你定义通用的数据结构;非类型参数则提供了编译期常量的灵活性。特化是最后的杀手锏,用来处理特殊类型的特殊需求。

我个人觉得,模板最难的地方不是语法,而是「什么时候该用,什么时候不该用」。别为了炫技而滥用模板,代码的可读性永远比「看起来很厉害」重要。

一句话记住模板:模板是编译期的「代码生成器」,你写一份逻辑,编译器帮你生成 N 份具体代码。


公众号:蓝海资料掘金营,微信 deep3321