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 秒实验(一个字看清整个机制):
- 把那段成员特化整段注释掉,重新编译运行。
- 输出立刻变成
88 99——同一份main、同一个类模板,只因为"那个push换没换",结果就完全不同。 - 恢复注释,输出回到
6 5。 - 顺带想一件事:这次换掉的只是"存"这一下;
pop返回的还是那个new出来的指针——谁负责delete? - 答案是:没人。 这个 Demo 故意留了一个内存泄漏(10.3 / 三法则讲的正是这个)。真实项目里,管理资源的类型要"成对"地写好构造、复制构造、析构——所以"三法则"这条规矩,在模板里同样适用。
四、两个粒度的对比:怎么选
| 全特化(12.3.1) | 成员特化(本节) | |
|---|---|---|
| 改动范围 | 整个类 | 一个成员函数 |
| 写法 | template <> class Stack<X> { ... }; |
template <> 返回类型 Stack<X>::f(...) { ... } |
| 要写多少 | 全部成员 | 一个 |
| 适合 | 大部分成员都要改(字符串栈:存、取、打印全变) | 只有个别成员要改(指针类型:只需深拷贝的 push) |
| 维护成本 | 高(通用版改了,这份不会跟着改) | 低(一行"补丁",容易被 diff 看见) |
| 共同点 | 都要求"具体类型"一字不差;都要写 template <>;都要放在通用模板之后 |
同左 |
判断方法:
先数一数"有几个成员需要特殊处理"。 一个 → 成员特化;一片 → 全特化。
五、常见误区
- "成员特化时类名写
Stack::就行"——不行,要写Stack<int *>::。漏了<...>报invalid use of template-name 'Stack' without an argument list。 - "函数名后面也要写
<int *>"——不要。<具体类型>只在类名上写一次:void Stack<int *>::push(...)。 - "一个类的每个成员函数特化前,只要在第一个前面写一次
template <>"——不行。每个成员特化都要自己带一行template <>(和 12.2.1 "每个类外定义都要带模板头"是同一个道理)。 - "成员特化会把整个类变成专门版"——不会。只换这一个函数,其余成员照旧走通用版。
- "类型差个
const没关系"——有关系,而且不报错。写Stack<char *>的特化,实际用Stack<const char *>→ 两不相干,悄悄走通用版(12.3.1 的同一个坑)。 - "深拷贝了就万事大吉"——不是。本节的 Demo 故意留了内存泄漏(
new出来的int没人delete)。"深拷贝 + 负责释放"必须成对出现——这就是 10.3 三法则在模板里的重演。 - "实际项目里该到处写特化"——不该。现代 C++ 的习惯是:让类型自己管好资源(用
string、vector代替裸指针),于是"通用模板"就真的通用了,特化也就少见了。特化是"应急补丁",不是"日常工具"。
自测三问(点开对答案)
1. 成员特化的正确写法里,`<具体类型>` 写在哪儿?——**只写在类名上**:`template <> void Stack- ☐ 12.4 小结(下一节)