🔷 第十二章 模板
📄 一句话概括

函数模板有三个作用:① 消灭重复(一份逻辑适配无数类型);② 比宏安全(有真正的类型检查,而且参数只求值一次);③ 是标准库的地基(sort、vector、max 全靠它)。本节还讲一个"看起来不起眼、其实很实用"的能力——实例01:把"数组长度"也变成模板参数。写法是 template <typename T, int N>,N 由编译器从数组长度自动推出来,你一个数字都不用数。

📝 本节和前面几节的关系

12.1 说"模板消灭重复",12.1.1 讲了语法,本节回答"到底值不值得用、比老办法好在哪"。这里的"老办法"指的是 #define 宏——它在 C 语言时代就是干这个的,用模板替掉宏,是 C++ 最早的进化之一。 另外,实例01 用到的"非类型模板参数",是 12.2.4 和 12.2.5 有界数组模板的铺垫——那里的 int size 就是同一个东西。

一、作用一:消灭重复(代码复用)

这是最直接的作用。要求"每段逻辑只写一遍":

用模板前 用模板后
maxInt、maxDouble、maxChar、maxString 四个函数 一个 maxOf<T>
交换三个类型,写三份 swap 一个 mySwap<T>
数组求和:sumInt、sumDouble…… 一个 sum<T>
哪天逻辑要改 改一处,处处生效

这不只是"少写几个字"。真正值钱的是后面那条:逻辑只有一份 → 不可能出现"四个函数里有一个忘了改"的事故。

判断标准:当你发现自己在写第二份"只换了类型名"的代码时,就该动手抽模板了。

二、作用二:比宏安全

在模板之前,C 语言是这么解决这个问题的:

#define MAX(a, b)  ((a) > (b) ? (a) : (b))

它能用,但埋着两个雷。

雷一:参数可能被求值两次

宏是纯文本替换。MAX(i++, 3) 展开后变成:

((i++) > (3) ? (i++) : (3))

如果 i++ > 3 成立,i++ 会被执行两次——i 凭空多加了一次。这类 bug 极难查,因为代码看起来完全正常。

模板不会:maxOf(i++, 3) 的参数是传值,i++ 只算一次。

雷二:没有类型检查

MAX("apple", 3)        // 展开后是 (("apple") > (3) ? ...) —— 拿字符串和整数比

在 C++ 里这甚至可能编过(指针和整数的比较),跑起来结果荒唐。模板会直接报错——T 推不出唯一答案。

对照表

#define 宏 函数模板
类型检查 ❌ 没有 ✅ 有(实例化时按真实类型检查)
参数求值次数 可能多次(i++ 会被加两次) 一次
调试 ❌ 调试器看不到(它已经不存在了) ✅ 正常函数,能打断点、能单步
作用域 ❌ 从定义处到文件末,全局污染 ✅ 受命名空间/作用域约束
能不能有"局部变量" ❌ 只能写成一整行表达式 ✅ 可以写多行函数体
报错信息 报的是"展开后那一坨" 报的是模板名和参数(虽然啰嗦,但认得出)

所以:宏能干的"类型无关的运算",今天都应该用模板或 constexpr 函数。(宏还剩两个正经用途:条件编译 #ifdef 和 字符串拼接 # / ##——那些是模板做不到的。)

回收旧账:这就是为什么 C++ 里那些"看起来像函数"的东西(std::max、std::swap)都是模板而不是宏。

三、作用三:它是标准库的地基

你在前面十一章里其实一直在用模板的产物(只是没点破):

你写过的 它其实是
string s; basic_string<char, char_traits<char>, allocator<char>>
cout << ... basic_ostream<char, char_traits<char>>
vector<int> v;(还没学,但见过) template <class T, class Alloc = allocator<T>> class vector
sort(a, a + n) template <class Iter> void sort(Iter first, Iter last)

标准库的写法就是"模板 + 默认参数"(12.2.4 会讲后半截)。不会看模板,等于读不懂标准库的头文件。

四、实例01:使用数组作为模板参数

现在讲本节的重头戏。

问题:写一个"打印数组"的函数。难点不在打印,在"知道数组有几个元素"。

void showArray(int arr[], int n) {         // 老办法:长度要"手动传"
    for (int i = 0; i < n; i++) cout << arr[i] << " ";
}

每次调用都要自己记住长度,而且传错一个数就悄悄读越界(7.5.1 讲过"数组作参数会退化成指针"):

int a[] = {1, 2, 3, 4, 5};
showArray(a, 5);      // 对
showArray(a, 4);      // 不报错,只是少打一个 —— 没人提醒你
showArray(a, 50);     // 不报错,只是读到越界 —— 灾难

模板的解法:把长度也变成模板参数。

template <typename T, int N>
void showArray(const T (&arr)[N]) {        // ★ 参数是"数组的引用",长度写在类型里
    for (int i = 0; i < N; i++) cout << arr[i] << " ";
}

关键在于 T (&arr)[N] 这个写法:

写法 结果
T arr[] 退化成 T *——长度信息丢了,N 推不出来 ❌
T (&arr)[N] "引用一个长度为 N 的 T 数组"——长度留在类型里,编译器能推出 N ✅

int N 是什么? 它是非类型模板参数——参数不是类型,而是一个值。所以:

于是:

int    a[] = {1, 2, 3, 4, 5};      // N = 5
double b[] = {1.5, 2.5, 3.0};      // N = 3
char   c[] = {'A', 'B', 'C'};      // N = 3

showArray(a);      // 不用写 5
showArray(b);      // 不用写 3
showArray(c);

"忘了传长度"这件事,从此不可能发生——因为它不再需要人传。

五、动手 Demo:实例01 完整代码

#include <iostream>
using namespace std;

// ① 打印任意类型、任意长度的数组
template <typename T, int N>
void showArray(const T (&arr)[N]) {
    cout << "共 " << N << " 个元素:";
    for (int i = 0; i < N; i++) cout << arr[i] << " ";
    cout << endl;
}

// ② 求任意类型、任意长度数组的和
//    ★ T s = T();  —— 用 T 的"零值"初始化:int→0、double→0.0、string→""
template <typename T, int N>
T sumOf(const T (&arr)[N]) {
    T s = T();
    for (int i = 0; i < N; i++) s += arr[i];
    return s;
}

// ③ 求数组里最大的元素
template <typename T, int N>
T maxOfArray(const T (&arr)[N]) {
    T m = arr[0];
    for (int i = 1; i < N; i++) if (arr[i] > m) m = arr[i];
    return m;
}

int main() {
    int    a[] = { 7, 2, 9, 4, 5 };        // N = 5
    double b[] = { 1.5, 2.5, 3.0 };        // N = 3
    char   c[] = { 'A', 'B', 'C' };        // N = 3

    showArray(a);
    showArray(b);
    showArray(c);

    cout << "整数和 = " << sumOf(a) << endl;
    cout << "小数和 = " << sumOf(b) << endl;
    cout << "最大整数 = " << maxOfArray(a) << endl;
    cout << "最大字符 = " << maxOfArray(c) << endl;
    return 0;
}

看到什么算成功:

共 5 个元素:7 2 9 4 5 
共 3 个元素:1.5 2.5 3 
共 3 个元素:A B C 
整数和 = 27
小数和 = 7
最大整数 = 9
最大字符 = C

读结果:

现象 说明
三次调用的长度都对 长度不是人传的,是从数组类型里"读"出来的
sumOf(b) 得 7(不是 6.5?) 1.5 + 2.5 + 3.0 = 7.0,打印时省略了 .0——数值是对的
maxOfArray(c) 得 C T 推成了 char——字符按 ASCII 码比较,'C' > 'A'
T s = T(); 这是"用 T 的零值初始化"的写法。模板里不能写 T s = 0;(string 没有 0 这个构造)

30 秒实验:

  1. 给 char 那个数组改成 char c[] = "ABC";(用字符串字面量)。
  2. 重新编译运行,看第一行输出:"共 4 个元素"——不是 3!
  3. 因为字符串字面量的真实长度包含结尾的 '\0'(7.3 的老知识):"ABC" 的类型是 char[4]。
  4. 这个实验同时说明两件事:① N 确实是从类型里推出来的;② 字符串数组的长度永远比"看到的字符数"多 1。

六、常见误区

  1. "#define 和模板差不多,用哪个都行"——差别很大:宏没有类型检查、参数可能被求值两次、没法调试。只有"条件编译"和"字符串拼接"这两件事才非宏不可。
  2. "template <int N> 里的 N 是个变量"——不是。它是编译期常量(不能 N++)。不同的 N 会生成不同的函数。
  3. "数组做参数时写 T arr[] 就行"——不行,那会退化成指针(7.5.1),长度信息丢掉,N 推不出来。必须写 const T (&arr)[N](引用)。
  4. "sumOf 里写 T s = 0; 更简单"——不行。T 换成 string 时,string s = 0; 编译错。要写 T s = T();——这句话的意思是"用 T 的默认值初始化"。
  5. "模板一定比手写快"——不一定,但通常至少一样快。实例化出来的代码和手写没有区别;真正的代价在编译期(每多一种类型/长度就多编一份代码)。别拿模板去套 20 种不同长度的小数组,那会让可执行文件白白变大。
  6. "有了模板,宏就彻底没用了"——还剩两件事:#ifdef 条件编译(按平台编不同代码)和 # / ## 字符串化/拼接。别的场合,能用模板就用模板。
自测三问(点开对答案) 1. 为什么 `showArray(const T (&arr)[N])` 能自动知道数组长度,而 `showArray(const T arr[], int n)` 不能?——**因为前者是"数组的引用",长度是类型的一部分**(`int[5]` 里就写着 5),编译器能推出 `N`;**后者会退化成指针** `T *`,**长度信息在类型里已经不存在了**,只能靠人传。 2. 用模板替代 `#define MAX(a, b)` 主要好在哪两点?——**① 有类型检查**(`MAX("apple", 3)` 这种用法会当场报错);**② 参数只求值一次**(`MAX(i++, 3)` 里 `i++` 可能被执行两次,改成 `maxOf(i++, 3)` 就不会)。 3. 模板里为什么写 `T s = T();` 而不是 `T s = 0;`?——因为 **`T` 可能是 `string`**,而 `string s = 0;` 会编译错。**`T()` 的意思是"造一个 `T` 的默认值"**——`int` 得到 0、`double` 得到 0.0、`string` 得到空串。这是"模板里不能随便写具体字面量"的典型例子。

相关笔记