"定制类模板"的正式名字叫"显式特化(explicit specialization)":给通用模板的某个具体类型,另写一整个类。语法是 template <>(一对空的尖括号)+ 类名后面写清具体类型:template <> class Stack<const char *> { ... };。四条规矩要背:① template <> 不能少;② 类名必须写 Stack<具体类型>;③ 必须写在通用模板之后;④ 特化版跟通用版可以完全不同(不继承、不"共享"任何东西,它就是另起炉灶的一个普通类)。它是"整个类都换"的做法——用在"大部分成员都需要改"的场合(比如字符串栈)。
12.3 讲了"为什么必须能定制",本节讲"定制一整个类"的语法与实战。它是 12.1.3 那个"普通函数重载补丁"的类版本——函数可以靠"再写一个同名函数"打补丁,类没有"重载",只能靠特化。
另外注意一路的伏笔:const char * 不能比大小(12.1.3 实例02)在这里第二次出现,因为类里也会犯同一个错。
一、问题现场:一个"字符串栈"用通用模板会怎样
先写一个通用栈(这是本章到处要用的零件):
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; }
void print() const {
for (int i = 0; i < top; i++) cout << data[i] << " ";
cout << endl;
}
};
它对 int 很好用。 那么 Stack<const char *> 呢?——四个成员全有问题:
| 成员 | 用通用版会发生什么 |
|---|---|
push("apple") |
只存了指针(浅拷贝)——如果字符串来自一个临时缓冲区,缓冲区一改/一销毁,栈里的内容就跟着坏/悬空 |
print() |
打印的是地址(0x...),不是字符串 |
pop() |
返回地址——那个地址指向的字符串可能早就没了 |
| 想比较两个字符串 | 拿指针比,毫无意义(12.1.3 的老问题) |
结论:这个类型的"存、取、打印"全都需要专门的实现——"大部分成员都要改" → 适合"整体换"。
二、全特化的语法:四条规矩
template <> // ★ 规矩①:template <>,尖括号里"什么都不写"
class Stack<const char *> { // ★ 规矩②:类名后面写清"具体类型"
private:
char data[100][50];
int top;
public:
...
}; // ★ 规矩③:必须写在"通用模板之后"
// 通用模板(必须在上面先出现)
template <typename T>
class Stack { ... };
| # | 规矩 | 漏了会怎样 |
|---|---|---|
| ① | 必须写 template <> |
漏了 → 编译器以为你想重新定义 Stack 这个模板 → 报"定义冲突"之类的错,或者报 'Stack' is not a template |
| ② | 类名写 Stack<const char *>(不是 Stack) |
写成 class Stack { ... } → 和通用模板名字撞车,直接冲突 |
| ③ | 必须出现在通用模板之后 | 报 specialization of 'Stack<const char*>' before template declaration("我还没见过那个模板呢") |
| ④ | 可以和通用版长得完全不一样 | ——(不是错误,是特性)特化版是个独立的普通类:可以有通用版没有的成员、可以没有通用版有的成员、成员函数的名字都能不同 |
第④条要特别理解:
特化不是"继承"通用模板,也不是"改几个地方"——它是"另写一个类"。 通用模板和特化版之间,除了"名字长得像",没有任何机制上的联系。
顺带说一个概念(认得即可):还有一种"部分特化/偏特化(partial specialization)"——即只固定一部分模板参数:
template <typename T>
class Stack<T *> { ... }; // ★ 所有"指针类型"都走这个版本(T 仍是模板参数)
注意:偏特化只能用在类模板上,函数模板没有偏特化。 教材不涉及,看到 template <typename T> class Stack<T *> 这种"类名里带参数、模板头里也有参数"的写法时,知道那是偏特化即可。
三、完整 Demo:给 Stack 定制一个字符串版
#include <iostream>
#include <cstring> // ★ strncpy / strcpy 在这里
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; }
void print() const {
for (int i = 0; i < top; i++) cout << data[i] << " ";
cout << endl;
}
};
// ─────────── 特化版:专门给"字符串"用 ───────────
template <>
class Stack<const char *> {
private:
char data[100][50]; // ★ 存的是"字符数组",不是"指针"
int top;
public:
Stack() : top(0) { }
void push(const char *s) {
if (top < 100) {
strncpy(data[top], s, 49); // ★ 复制内容(深拷贝)
data[top][49] = '\0'; // ★ 手动补上结尾
top++;
}
}
const char *pop() { return top > 0 ? data[--top] : ""; }
bool empty() const { return top == 0; }
void print() const {
for (int i = 0; i < top; i++) cout << data[i] << " "; // ★ 打印字符串本身
cout << endl;
}
};
int main() {
// ① 普通类型:走通用版
Stack<int> si;
si.push(1);
si.push(2);
si.push(3);
cout << "整数栈:";
si.print();
// ② 字符串:走特化版
Stack<const char *> ss;
ss.push("apple");
ss.push("banana");
cout << "字符串栈:";
ss.print();
// ③ 关键验证:源内存被改掉,栈里的内容会不会跟着变?
char buf[50] = "first";
ss.push(buf);
strcpy(buf, "changed"); // ★ 把 buf 的内容改掉
cout << "改掉源内存后:";
ss.print();
cout << "弹出一个:" << ss.pop() << endl;
return 0;
}
看到什么算成功:
整数栈:1 2 3
字符串栈:apple banana
改掉源内存后:apple banana first
弹出一个:first
读结果:
| 现象 | 说明 |
|---|---|
整数栈打印 1 2 3(不是地址) |
Stack<int> 走通用版,cout << int 正常 |
字符串栈打印 apple banana(不是 0x...) |
Stack<const char *> 走特化版,打印的是字符数组的内容 |
改掉 buf 之后,栈里那个 "first" 没变 |
★ 这是本节最关键的一行:特化版的 push 把那 5 个字符复制进了自己的 data[top]——源内存怎么改,都不影响栈里那份 |
弹出 first |
后进先出(buf 是最后压进去的) |
用通用版会是什么样? —— print 会打印出 0x…(地址),而且改掉 buf 后栈里那个"元素"会跟着变成 changed。同一份 main,两套实现,一个对、一个错。
30 秒实验(把三条语法错误各撞一次):
- 删掉
template <>→ 编译错(编译器把class Stack<const char *>当成"又一次定义Stack",报重定义/冲突)。 - 加回来,把特化版整段剪到通用版上面 → 报
specialization of 'Stack<const char*>' before template declaration。 - 加回来,把类名
Stack<const char *>改成Stack→ 报名字冲突(redefinition of 'class Stack')。 - 还有一个最值钱的实验:把
Stack<const char *> ss;改成Stack<char *> ss;(少写一个const)—— - 编译能过(
char *也是一次新的实例化),但它走的是通用版! - 因为
const char *和char *是两个不同的类型,你的特化版只管前者。 ss.push("apple")这时甚至编不过(字符串字面量是const char[N],不能转成char *)——编译器顺手帮你挡了一下,但如果你手上恰好有个char buf[],就会悄悄走通用版。- 这个坑非常真实:特化的类型必须和实际用的类型"一字不差"。
四、编译器怎么选
| 你写的 | 编译器给哪个 |
|---|---|
Stack<int> |
通用版 |
Stack<double> |
通用版 |
Stack<const char *> |
特化版(有一份"专门为它准备的",就优先用它) |
比较一下这个顺序,和 12.1.3 的函数选择顺序是同一个精神:
| 场景 | 优先级 |
|---|---|
| 函数(12.1.3) | 精确匹配的普通函数 > 模板特化 > 通用模板 |
| 类 | 全特化版 > 通用模板(类没有"普通类重载"这一层) |
五、常见误区
- "特化就是继承通用模板"——不是。特化版是个独立的普通类,跟通用模板没有继承、没有共享。它甚至可以有完全不同的成员列表。
- "特化时只写要改的那几个成员就行"——不行。全特化是"整个类重写"——通用版有的成员,特化版不写就没有。想只改一个成员 → 用 12.3.2 的"成员特化"。
- "漏写
template <>只是个小疏忽"——报错很绕(编译器会以为你在重复定义Stack)。template <>是"这是特化"的标志,不能省。 - "
const char *和char *差不多"——差很多。它们是两个类型,特化只认写出来的那一个。写错一个const,就悄悄退回通用版——而且不报错(这才是最危险的)。 - "特化版可以放在通用版前面"——不行。必须先有通用模板的声明/定义,编译器才知道你在特化"谁"。
- "特化到哪儿都能写"——通常和通用模板写在同一个头文件里(同一个理由:编译器在实例化点必须看得到它,12.2.1 那条铁律)。
自测三问(点开对答案)
1. 全特化的语法有哪几处不能少?——**① `template <>`(空的尖括号);② 类名写清具体类型 `Stack- ☐ 12.3.2 定制类模板成员函数(下一节)