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

通用模板有个硬伤:它比较 T 时用的是 a > b,而 T 一旦换成 const char *,> 比的是两个"地址"——结果毫无意义,而且错得很隐蔽(有时看着是对的)。解药有两条:① 再写一个"更精确的普通函数"(编译器会优先选它);② 用模板特化(template <>,12.3 正式讲)。这就是实例02:求出字符串的最小值要演示的东西。本节还要说清编译器挑函数的顺序:精确匹配的普通函数 > 特化版 > 通用模板。

📝 本节和前面几节的关系

12.1.2 证明了模板很能打,本节说它也有办不到的事——"通用"不等于"正确"。这个问题和 6.5 重载函数是同一套选择机制(编译器按"匹配程度"挑),只是这次候选里既有普通函数又有模板。 而"字符串不能用 > 比"这件事,其实是 7.4.1 变量与指针埋下的账:数组名/字符串字面量会退化成指针,所以 s1 > s2 比的从来不是内容。

一、问题现场:通用模板对字符串会"办错事"

把 12.1 的 bigger 拿来比字符串:

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

const char *s1 = "apple";
const char *s2 = "banana";

cout << bigger(s1, s2) << endl;      // ★ 你以为会输出 "banana"

实际情况:T 被推成 const char *,于是比较变成了:

return a > b ? a : b;      // 比的是"两个字符串第一个字符的内存地址"

比地址——结果完全取决于编译器把这两个字面量放在哪儿。

可能的结果 什么时候出现
输出 banana(看着对) "banana" 的地址恰好更大——运气好
输出 apple(错的) 地址反过来——运气不好
换个编译器/加一行代码就变了 这才是最可怕的

注意这个 bug 的形态:它不报错、不警告,看起来还很正常。你只有在"结果跟你预期不一致"时才会注意到它——而它偏偏在大多数时候表现得像是对的。

这不是模板的错,是"> 在指针上本来就这个意思"——通用模板只是"照字面执行"。模板不知道你要比内容。

二、解药一:写一个"更精确的普通函数"(函数模板重载)

最省事的办法:再写一个专门处理字符串的普通函数,名字一样。

#include <cstring>       // strcmp

const char *bigger(const char *a, const char *b) {
    return strcmp(a, b) > 0 ? a : b;      // ★ 比内容,不是比地址
}

然后什么都不用改,bigger(s1, s2) 就自动走这个版本了。

为什么? 因为编译器挑函数时有一套优先级(下面第三节展开):"参数类型完全对上的普通函数"比"需要推导 T 的模板"更精确 —— 精确的赢。

这种做法就叫"函数模板重载":同名的模板和普通函数共存,编译器按匹配程度挑。

代价很小:多写十行;好处很大:不动原来那份模板,其他人用 bigger(3, 7) 完全不受影响。

三、选择顺序:普通函数 > 特化 > 模板

这是本节最该背下来的一张表:

优先级 候选 例子
最高 参数完全匹配的普通函数 const char *bigger(const char *, const char *)
中间 模板特化版(为某个具体类型专门写的模板版本,12.3 讲) template <> const char *bigger<const char *>(...)
最低 通用模板(需要推导 T 的那个) template <typename T> T bigger(T, T)

读法:"能精确对上就不用推导,能推导就不报错。"

几个由此推出的推论:

推论 说明
普通函数只要参数类型完全对上,模板就没机会 这是"打补丁"能生效的原因
普通函数写得不精确时,模板照旧会赢 比如你写 bigger(char *, char *),而实参是 const char * → 仍然走模板(类型没完全对上)
模板报错时,编译器会列出所有候选 报错里的 candidate: 一行行都是候选——能看出它为什么没选你写的那个

四、"重载函数模板"的另一层含义

除了"模板 + 普通函数同名",还有一种是几个模板互相重载——参数形态不同:

template <typename T>
T  maxOf(T a, T b)          { return a > b ? a : b; }        // 两个同类型值

template <typename T>
T  maxOf(T a, T b, T c)     { return maxOf(maxOf(a, b), c); } // 三个值

template <typename T>
T *maxOf(T *arr, int n)     {                                 // 一个数组 + 长度
    T *m = arr;
    for (int i = 1; i < n; i++) if (arr[i] > *m) m = arr + i;
    return m;
}

编译器怎么选? 两道筛子:

  1. 先按"参数个数和形态"筛——maxOf(a, b) 只有一个候选(第一个)。
  2. 参数个数相同的,比"谁更精确"——比如 maxOf(a, 5)(数组 + 整数)唯一对上第三个。

所以"重载"在这里的意义是:同一件事,按"你手上有什么"提供不同的调用方式。这和 6.5 讲的普通函数重载是同一个思路,只是候选换成了模板。

五、实例02:求出字符串的最小值

把上面所有东西合起来,写一个"什么类型都能求最小,但字符串按内容比"的函数。

#include <iostream>
#include <cstring>          // ★ strcmp 在这里(别忘)
using namespace std;

// ───── ① 通用模板:处理所有"能用 < 比较"的类型 ─────
template <typename T>
T minOf(T a, T b) {
    return a < b ? a : b;
}

// ───── ② 为字符串"重载"一个普通函数:比内容,不比地址 ─────
const char *minOf(const char *a, const char *b) {
    return strcmp(a, b) < 0 ? a : b;
}

int main() {
    cout << minOf(3, 7) << endl;                 // 走 ①
    cout << minOf(2.5, 1.5) << endl;             // 走 ①
    cout << minOf('z', 'a') << endl;             // 走 ①

    const char *s1 = "orange";
    const char *s2 = "apple";
    cout << minOf(s1, s2) << endl;               // ★ 走 ② → apple

    cout << minOf("pear", "banana") << endl;     // ★ 字面量也走 ② → banana
    return 0;
}

看到什么算成功:

3
1.5
a
apple
banana

逐行读结果:

调用 走了哪个 为什么
minOf(3, 7) ① 模板 普通函数参数是 const char *,对不上 int
minOf(2.5, 1.5) ① 模板 同上
minOf('z', 'a') ① 模板 注意:char 能转成 const char * 吗?不能(这是数值转指针,非法)→ 只能走模板 ✅
minOf(s1, s2) ② 普通函数 参数完全匹配,比模板精确 → 赢
minOf("pear", "banana") ② 普通函数 字面量类型是 const char[5] / const char[7] → 退化成 const char *(7.3)→ 匹配

30 秒实验(一定要做,这是本节的"现场"):

  1. 把 ② 那个普通函数整段注释掉,重新编译运行。
  2. 看第 4、5 行输出 —— 它变成 orange 和 pear(或者在你机器上是别的组合,取决于编译器把字面量排在哪里)。
  3. 这就是"比地址"的结果:它甚至可能在你机器上恰好输出对的,换台电脑就变了。
  4. 把 ② 的注释去掉,输出恢复成 apple 和 banana。
  5. 结论一句话:通用模板"照字面执行",它不会替你猜语义——该补的地方必须补。

六、常见误区

  1. "通用模板能处理所有类型"——能编译 ≠ 正确。const char *、char *、以及任何"比较符号含义不符合直觉"的类型(指针是头号),都要专门处理。
  2. "普通函数和同名的模板会冲突/重复定义"——不会。它们共存,编译器按匹配程度挑。这正是"打补丁"的标准做法。
  3. "模板会优先选,因为它更通用"——反了。精确匹配的普通函数优先于模板。选择顺序是:普通函数 > 特化 > 模板。
  4. "补丁只要名字一样就行"——参数类型必须完全对上。写 minOf(char *, char *),而实参是 const char * → 还是走模板(编译能过,但结果是错的)。差一个 const 就前功尽弃。
  5. "strcmp 返回的是两个字符串的长度差"——不是。标准只保证:相等返回 0、前者大返回正数、前者小返回负数。具体值是实现相关的,别拿它做算术。
  6. "忘了 #include <cstring> 也无所谓"——不行。strcmp 声明在 <cstring>(C 版是 <string.h>)里,没包含会报 'strcmp' was not declared。
自测三问(点开对答案) 1. `bigger(const char *a, const char *b)` 用通用模板写,为什么结果是错的?——因为 `T` 推成 `const char *` 后,`a > b` **比较的是两个指针的地址**,而不是字符串内容。**结果取决于编译器把字符串放在哪儿——有时看着对,换个环境就变。** 2. 编译器挑函数时的优先级顺序是什么?——**① 参数完全匹配的普通函数 > ② 模板特化版 > ③ 通用模板**。所以"写个更精确的普通函数"是给模板打补丁最省事的办法。 3. `minOf("pear", "banana")` 为什么也能走普通函数版本?——因为**字符串字面量的类型是 `const char[N]`**,作为函数实参时会**退化成 `const char *`**(7.3 的知识),于是**和普通函数的参数类型完全匹配**。

相关笔记