通用模板有个硬伤:它比较 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;
}
编译器怎么选? 两道筛子:
- 先按"参数个数和形态"筛——
maxOf(a, b)只有一个候选(第一个)。 - 参数个数相同的,比"谁更精确"——比如
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 秒实验(一定要做,这是本节的"现场"):
- 把 ② 那个普通函数整段注释掉,重新编译运行。
- 看第 4、5 行输出 —— 它变成
orange和pear(或者在你机器上是别的组合,取决于编译器把字面量排在哪里)。 - 这就是"比地址"的结果:它甚至可能在你机器上恰好输出对的,换台电脑就变了。
- 把 ② 的注释去掉,输出恢复成
apple和banana。 - 结论一句话:通用模板"照字面执行",它不会替你猜语义——该补的地方必须补。
六、常见误区
- "通用模板能处理所有类型"——能编译 ≠ 正确。
const char *、char *、以及任何"比较符号含义不符合直觉"的类型(指针是头号),都要专门处理。 - "普通函数和同名的模板会冲突/重复定义"——不会。它们共存,编译器按匹配程度挑。这正是"打补丁"的标准做法。
- "模板会优先选,因为它更通用"——反了。精确匹配的普通函数优先于模板。选择顺序是:普通函数 > 特化 > 模板。
- "补丁只要名字一样就行"——参数类型必须完全对上。写
minOf(char *, char *),而实参是const char *→ 还是走模板(编译能过,但结果是错的)。差一个const就前功尽弃。 - "
strcmp返回的是两个字符串的长度差"——不是。标准只保证:相等返回 0、前者大返回正数、前者小返回负数。具体值是实现相关的,别拿它做算术。 - "忘了
#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 的知识),于是**和普通函数的参数类型完全匹配**。- ☐ 12.2 类模板(下一节)