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

用模板分三个层次:① 用现成的(vector<int>、sort、pair——现实中 90% 都是这个)、② 自己写自己用(12.1 / 12.2 学的)、③ 定制(通用模板对某个类型"办错事"时,专门给它换一套实现)。本节讲第三层为什么必须存在:通用模板只保证"能编译",不保证"结果对"——它假设"元素能复制、能用 > 比较、能独立存放",而这些假设在指针、字符串、bool 上全都不成立。解法叫"特化(specialization)",教材叫"定制",分两个粒度:整体换(12.3.1) 和 只换一个成员函数(12.3.2)。

📝 本节和前面几节的关系

12.1.3 已经预演过一次:通用 bigger 对 const char * 比的是地址,只能靠"重载一个更精确的普通函数"打补丁。本节把那个补丁从"函数"升级到"类"——因为类没法"重载",所以需要一套新语法。 十二个节到这里正好各自补上一块:12.1 组解决了"函数通用",12.2 组解决了"类通用",12.3 组解决"通用得不合适的地方怎么办"。

一、用模板的三个层次

层次 做法 例子 难度
① 用现成的 直接用标准库/别人写好的模板 vector<int> v;、sort(a, a + n)、pair<int, string> ⭐
② 自己写自己用 用 12.1 / 12.2 的语法造自己的 maxOf<T>、BoundedArray<T, size> ⭐⭐
③ 定制 为某个具体类型,换一套专门实现 ① 给字符串栈换实现;② 标准库的 vector<bool> ⭐⭐⭐

先说一句实话:日常工作里,90% 的时间在①(用 vector、用 sort);②是"造工具箱"时用的;③是"造工具箱造到一半发现某类型不合适"时用的。

但③必须懂——因为标准库里到处是特化:vector<bool>、char_traits、hash(哈希表对不同类型的哈希算法)……读不懂特化,就看不懂标准库的一半头文件。

二、为什么必须有"定制"

通用模板在写的时候,默默假设了几件事:

通用模板的假设 在哪些类型上不成立 后果
"复制元素就是把值搬过去" 带指针的类型 浅拷贝(10.2.2 的老问题):两个对象共用一块内存,改一个动另一个,还重复释放
"能用 > < 比大小" const char *(比的是地址)、char * 结果随机(12.1.3 的实例02)
"每个元素独立占一份内存" bool 浪费(vector<bool> 特化后按"位"存,一个 bit 一个元素)
"元素能安全地被析构" 元素本身没实现好析构的类型 泄漏

一句话:

通用模板 ≠ 正确模板。它只保证"这段代码在那个类型上能编译" —— 至于"跑出来的结果对不对",那要看"这个类型的语义是否真的符合模板的假设"。

而"定制"就是:当假设不成立时,我亲手给这个类型写一份合适的实现。

三、定制的两个粒度

template <typename T>
class Stack { ... };                    // 通用版(模具)
粒度 写法(预览) 什么时候用
① 整个类都换(12.3.1) template <> class Stack<const char *> { ... }; 大部分成员都要改(字符串栈就是这样:存、取、打印、比较全不一样)
② 只换一个成员函数(12.3.2) template <> void Stack<int *>::push(int *v) { ... } 只有个别成员需要特殊处理(最典型:指针成员的深拷贝)

两个粒度对应两种"改造成本":改得多就整体换,改得少就局部换。

四、本节路线图

节 讲什么
12.3.1 定制类模板 全特化:template <> class Stack<const char *> { ... }; —— 语法、四条规矩、完整 Demo(一个"字符串栈")
12.3.2 定制类模板成员函数 成员特化:template <> void Stack<int *>::push(int *v) { ... } —— 只换一个函数,代价最小

五、动手 Demo:先看看"不定制会怎样"

#include <iostream>
using namespace std;

template <typename T>
class Holder {                    // 一个最简单的"盒子"
private:
    T value;
public:
    Holder(T v) : value(v) { }
    T get() const { return value; }
};

int main() {
    Holder<int> h1(5);
    cout << h1.get() << endl;         // 5 —— 正常

    int x = 5;
    Holder<int *> h2(&x);             // ★ T 换成 int*(保存的是"地址")
    cout << *h2.get() << endl;        // 5 —— 解引用才拿到值

    x = 99;                           // ★ 把源变量改掉
    cout << *h2.get() << endl;        // ★ 变成 99 了!
    return 0;
}

看到什么算成功:

5
5
99

读结果:

现象 说明
第 3 行输出 99 h2 里存的只是 x 的地址——x 一变,h2 看到的就变了
这就是"浅拷贝" 和 10.2.2 复制构造函数讲的完全同一个问题:指针成员"复制"的只是地址
那要怎么样? 让"存进去的那一刻"把值复制一份——这就叫"深拷贝"。通用模板做不到(它不知道 T 是指针),所以要"定制"

30 秒实验:

  1. 仔细想一件事:如果 x 在 main 结束前就死了呢?(比如 x 定义在一个内层 {} 里)
  2. h2 里存的就是"野指针",*h2.get() 读到的是已经不属于你的内存。
  3. 这就是"通用模板对指针类型办错事"的完整危害链:浅拷贝 → 共用内存 → 源销毁后悬空 → 悄悄读错。
  4. 记住这个场景——12.3.1 和 12.3.2 都在解决它。

六、常见误区

  1. "模板已经足够通用了,不需要定制"——通用 ≠ 正确。只要类型不满足模板的隐含假设(指针、字符串、bool),就必须定制。
  2. "定制 = 重载"——不是。重载是"另写一个函数/模板",特化是"给已有的模板指定一个更专门的版本"。语法、机制都不同(虽然效果有时相似)。
  3. "vector<bool> 和别的 vector 一样"——它是个特化版(按位压缩存储,一个元素一个 bit)。所以有些操作行为和别的 vector 不同——这是标准库里最有名的一处特化。
  4. "定制会带来性能代价"——不会。定制的部分就是"你亲手写的代码",和通用部分一样快。代价是维护成本(你多写了一份实现,改的时候要记得两处都改)。
  5. "自己写模板是 C++ 程序员的主流工作"——不是。先是"会用",再是"会定制",最后才是"会写"。顺序别倒过来。
自测三问(点开对答案) 1. 通用模板为什么"不一定对"?——因为它**默默假设了元素的行为**(能复制、能用 `>` 比大小、能独立占内存)。**这些假设在指针、`const char *`、`bool` 上都不成立**。**它只保证"能编译",不保证"结果对"。** 2. 定制的两个粒度是什么?分别用在什么场合?——**① 全特化(整个类都换)**:大部分成员都要改时用(如字符串栈);**② 成员特化(只换一个函数)**:只有个别成员需要特殊处理时用(如指针成员的深拷贝)。 3. 12.3 的 Demo 里,`h2.get()` 为什么在 `x = 99` 之后也变成 99?——因为 **`Holder` 里存的是 `x` 的地址**,不是值。**"复制指针"=浅拷贝**(10.2.2)。要让它保住"存进去那一刻的值",就得**定制 `push/构造` 去做深拷贝**。

相关笔记