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

"定制类模板"的正式名字叫"显式特化(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 秒实验(把三条语法错误各撞一次):

  1. 删掉 template <> → 编译错(编译器把 class Stack<const char *> 当成"又一次定义 Stack",报重定义/冲突)。
  2. 加回来,把特化版整段剪到通用版上面 → 报 specialization of 'Stack<const char*>' before template declaration。
  3. 加回来,把类名 Stack<const char *> 改成 Stack → 报名字冲突(redefinition of 'class Stack')。
  4. 还有一个最值钱的实验:把 Stack<const char *> ss; 改成 Stack<char *> ss;(少写一个 const)——
  5. 编译能过(char * 也是一次新的实例化),但它走的是通用版!
  6. 因为 const char * 和 char * 是两个不同的类型,你的特化版只管前者。
  7. ss.push("apple") 这时甚至编不过(字符串字面量是 const char[N],不能转成 char *)——编译器顺手帮你挡了一下,但如果你手上恰好有个 char buf[],就会悄悄走通用版。
  8. 这个坑非常真实:特化的类型必须和实际用的类型"一字不差"。

四、编译器怎么选

你写的 编译器给哪个
Stack<int> 通用版
Stack<double> 通用版
Stack<const char *> 特化版(有一份"专门为它准备的",就优先用它)

比较一下这个顺序,和 12.1.3 的函数选择顺序是同一个精神:

场景 优先级
函数(12.1.3) 精确匹配的普通函数 > 模板特化 > 通用模板
类 全特化版 > 通用模板(类没有"普通类重载"这一层)

五、常见误区

  1. "特化就是继承通用模板"——不是。特化版是个独立的普通类,跟通用模板没有继承、没有共享。它甚至可以有完全不同的成员列表。
  2. "特化时只写要改的那几个成员就行"——不行。全特化是"整个类重写"——通用版有的成员,特化版不写就没有。想只改一个成员 → 用 12.3.2 的"成员特化"。
  3. "漏写 template <> 只是个小疏忽"——报错很绕(编译器会以为你在重复定义 Stack)。template <> 是"这是特化"的标志,不能省。
  4. "const char * 和 char * 差不多"——差很多。它们是两个类型,特化只认写出来的那一个。写错一个 const,就悄悄退回通用版——而且不报错(这才是最危险的)。
  5. "特化版可以放在通用版前面"——不行。必须先有通用模板的声明/定义,编译器才知道你在特化"谁"。
  6. "特化到哪儿都能写"——通常和通用模板写在同一个头文件里(同一个理由:编译器在实例化点必须看得到它,12.2.1 那条铁律)。
自测三问(点开对答案) 1. 全特化的语法有哪几处不能少?——**① `template <>`(空的尖括号);② 类名写清具体类型 `Stack`;③ 放在通用模板之后。** 少任何一处都报错,而且报错信息各不相同。 2. 特化版和通用模板是什么关系?——**没有机制上的关系**。**它是"另起炉灶写的一个普通类"**:不继承、不共享、成员列表可以完全不同。它和通用模板的相似之处只有"名字长得像"。 3. 把 `Stack` 写成 `Stack` 会怎样?——**编译能过,但走的是通用版**(因为 `const char *` 和 `char *` 是两个不同类型)。**结果是"打印地址""浅拷贝"全回来了**——**而且不报错。所以特化的类型必须和实际使用一字不差。**

相关笔记