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

12.3.1 的办法是"整个类重写"——很重。但现实里更常见的是:通用模板只有"一两个成员"对某个类型不合适(头号例子:指针成员的深拷贝)。这时用 "成员函数特化":只把那一个成员函数为特定类型换掉,其余照旧用通用版。语法:template <> 照样要写,类名写全 Stack<int *>::,函数名后面不再写 <...>——template <> void Stack<int *>::push(int *v) { ... }。它是代价最小的定制手段,也是工程里最常被用上的那一个。

📝 本节和前面几节的关系

12.3.1 是"大改"(整体换),本节是"小改"(点一下)——两节合起来,你就有了"按改动量选手段"的判断力。 而这里要治的病,是 10.2.2 复制构造函数埋下的"浅拷贝之雷":编译器默认的复制是逐成员复制,指针成员只复制地址。同一个雷,在模板里炸了第二次。 还要注意 new 出来的内存没人释放——这是 10.3 析构函数 / 三法则的账,本节末尾会诚实交代这个 Demo 的局限。

一、为什么需要"只改一个成员"

回到 12.3 那个"盒子装指针"的现场:

template <typename T>
class Holder { T value; ... };

int x = 5;
Holder<int *> h(&x);      // ★ 只保存了地址
x = 99;                   // h 里的内容跟着变成 99

这里真正出问题的只有一个动作:"把元素存进去"那一下没有复制内容。

别的成员呢?

成员 对 int * 有问题吗
get() 没有——它只是把存的指针还给你
empty() 没有
count() 没有
push() / 构造函数 ★ 有!它该做深拷贝

只有一两个成员需要改,却要"把整个类重写一遍"——不划算。

更糟的是:整体重写之后,通用版改了,特化版不会跟着改——两份代码要各自维护。改动越小,越安全。

所以需要"成员特化"。

二、语法:和"全特化"只差一点

// ─── 全特化(12.3.1):换整个类 ───
template <>
class Stack<int *> {                    // ★ 关键字 class,后面是"类名 + 具体类型"
    ... 所有成员都要自己写 ...
};

// ─── 成员特化(本节):只换一个函数 ───
template <>                             // ★ ① template <> 照样要写
void Stack<int *>::push(int *v) {       // ★ ② 类名写全 Stack<int *>::    ③ 函数名后不写 <...>
    ...
}

三处要点,和全特化的对照:

部位 全特化(12.3.1) 成员特化(本节)
开头 template <> template <>(一样要写)
名字部分 class Stack<int *> { ... } void Stack<int *>::push(...)(类名<具体类型>::函数名)
函数名带不带 <...> ——(构造、析构等都在类里) 不带——<int *> 只在类名上写一次
要不要写全部成员 要(整个类都得自己写) 不要(只写这一个)

最容易写错的两处:

template <>
void Stack<int *>::push(int *v) { ... }        // ✓ 正确

template <>
void Stack::push(int *v) { ... }               // ✗ 漏了 <int *> → 报错

template <>
void Stack<int *>::push<int *>(int *v) { ... } // ✗ 函数名后面多写了 <int *> → 报错

template <typename T>
void Stack<T>::push(T v) { ... }               // ← 这是"通用版的类外定义",不是特化

一句话记法::: 左边 = 哪个类的哪个版本(写全 <具体类型>);:: 右边 = 哪个函数(不带尖括号)。 和 12.2.1 那条":: 前带 <T>、后不带"的规则,是同一个手感。

三、完整 Demo:给 Stack<int *> 换一份"深拷贝的 push"

#include <iostream>
using namespace std;

// ─────────── 通用版 ───────────
template <typename T>
class Stack {
private:
    T   data[100];
    int top;
public:
    Stack() : top(0) { }
    void push(T v) { if (top < 100) data[top++] = v; }     // ★ 通用版:只存"值本身"
    T    pop()     { return top > 0 ? data[--top] : T(); }
    bool empty() const { return top == 0; }
    int  count() const { return top; }
};

// ─────────── 成员特化:只为 Stack<int *> 换掉 push ───────────
template <>
void Stack<int *>::push(int *v) {
    int *copy = new int(*v);          // ★ 新开一块内存,把"值"抄过去(深拷贝)
    if (top < 100) data[top++] = copy;
}

int main() {
    // ① 普通类型:走通用版 push
    Stack<int> si;
    si.push(10);
    si.push(20);
    cout << "整数栈弹出:" << si.pop() << " " << si.pop() << endl;

    // ② 指针类型:走特化版 push
    Stack<int *> sp;
    int x = 5, y = 6;
    sp.push(&x);
    sp.push(&y);

    x = 99;                            // ★ 把源变量改掉
    y = 88;

    cout << "指针栈弹出:" << *sp.pop() << " " << *sp.pop() << endl;
    return 0;
}

看到什么算成功:

整数栈弹出:20 10
指针栈弹出:6 5

读结果:

现象 说明
si 一切正常 Stack<int> 走通用版,push 就是普通赋值
sp 弹出 6 5(不是 88 99) ★ 这就是深拷贝的效果:push(&y) 那一刻,y 里的 6 已经被复制进一块新内存;后来 y = 88 改的是原来那块,栈里那份不受影响
弹序是 6 5 后进先出:先压 &x、后压 &y,所以先弹出 y 的那份(6)

对比一下"如果 push 是通用版":输出会变成 88 99——因为栈里存的是 &x、&y,读到的时候 x、y 已经被改了。

30 秒实验(一个字看清整个机制):

  1. 把那段成员特化整段注释掉,重新编译运行。
  2. 输出立刻变成 88 99——同一份 main、同一个类模板,只因为"那个 push 换没换",结果就完全不同。
  3. 恢复注释,输出回到 6 5。
  4. 顺带想一件事:这次换掉的只是"存"这一下;pop 返回的还是那个 new 出来的指针——谁负责 delete?
  5. 答案是:没人。 这个 Demo 故意留了一个内存泄漏(10.3 / 三法则讲的正是这个)。真实项目里,管理资源的类型要"成对"地写好构造、复制构造、析构——所以"三法则"这条规矩,在模板里同样适用。

四、两个粒度的对比:怎么选

全特化(12.3.1) 成员特化(本节)
改动范围 整个类 一个成员函数
写法 template <> class Stack<X> { ... }; template <> 返回类型 Stack<X>::f(...) { ... }
要写多少 全部成员 一个
适合 大部分成员都要改(字符串栈:存、取、打印全变) 只有个别成员要改(指针类型:只需深拷贝的 push)
维护成本 高(通用版改了,这份不会跟着改) 低(一行"补丁",容易被 diff 看见)
共同点 都要求"具体类型"一字不差;都要写 template <>;都要放在通用模板之后 同左

判断方法:

先数一数"有几个成员需要特殊处理"。 一个 → 成员特化;一片 → 全特化。

五、常见误区

  1. "成员特化时类名写 Stack:: 就行"——不行,要写 Stack<int *>::。漏了 <...> 报 invalid use of template-name 'Stack' without an argument list。
  2. "函数名后面也要写 <int *>"——不要。<具体类型> 只在类名上写一次:void Stack<int *>::push(...)。
  3. "一个类的每个成员函数特化前,只要在第一个前面写一次 template <>"——不行。每个成员特化都要自己带一行 template <>(和 12.2.1 "每个类外定义都要带模板头"是同一个道理)。
  4. "成员特化会把整个类变成专门版"——不会。只换这一个函数,其余成员照旧走通用版。
  5. "类型差个 const 没关系"——有关系,而且不报错。写 Stack<char *> 的特化,实际用 Stack<const char *> → 两不相干,悄悄走通用版(12.3.1 的同一个坑)。
  6. "深拷贝了就万事大吉"——不是。本节的 Demo 故意留了内存泄漏(new 出来的 int 没人 delete)。"深拷贝 + 负责释放"必须成对出现——这就是 10.3 三法则在模板里的重演。
  7. "实际项目里该到处写特化"——不该。现代 C++ 的习惯是:让类型自己管好资源(用 string、vector 代替裸指针),于是"通用模板"就真的通用了,特化也就少见了。特化是"应急补丁",不是"日常工具"。
自测三问(点开对答案) 1. 成员特化的正确写法里,`<具体类型>` 写在哪儿?——**只写在类名上**:`template <> void Stack::push(int *v) { ... }`。**函数名后面不写** `push`。 2. 全特化和成员特化怎么选?——**数"有几个成员要改"**:**一个(或极少)→ 成员特化**(改动小、维护成本低);**一片 → 全特化**(反正都要重写)。 3. 本节 Demo 的"深拷贝 push"还有什么问题?——**内存泄漏**:`new int(*v)` 出来的内存没人 `delete`。**深拷贝和释放必须成对**(10.3 三法则)。**现代做法是让类型自己管资源**(用 `string` / `vector` 取代裸指针),这样通用模板就真的通用了。

相关笔记