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

第十二章讲的是"把类型变成参数"。一句话串起来:函数模板(12.1 组)解决了"同一套逻辑换类型",类模板(12.2 组)解决了"同一个类换类型",定制/特化(12.3 组)解决了"通用模板碰到不适用的类型怎么办"。模板是"编译期多态"——和第十一章的 virtual(运行期多态)正好配成一对:编译期能定类型就用模板(零开销),定不了就用虚函数(多一次间接跳转)。

📝 本节和前面几节的关系

收束整章。往回看:第十一章让"类与类之间能互相接替"(继承 + 多态),这一章让"代码与类型之间解耦"。 往前看:本章还欠着两笔账——① "能长大的容器"(12.2.5 的有界数组是固定长度的,真正能扩容的容器是标准库的 vector);② "出错怎么办"(越界时"返回零值"只是权宜之计,正规做法是抛异常)。它们会分别在标准库和异常处理里还上。

一、本章知识地图

块 小节 一句话要点
函数模板 12.1 / 12.1.1 template <typename T> T f(T a, T b);typename ≡ class;模板是图纸、不是函数(实例化 = 编译期造真函数);模板推导不做类型转换(f(3, 4.5) 报 deduced conflicting types);定义必须进头文件
模板的作用 12.1.2(含实例01) ① 消灭重复 ② 比宏安全(有类型检查、参数只求值一次)③ 是标准库的地基;非类型模板参数 template <typename T, int N>,N 从数组类型自动推出(const T (&arr)[N])
重载函数模板 12.1.3(含实例02) 通用模板对 const char * 比的是地址;选择顺序:精确匹配的普通函数 > 特化 > 通用模板;strcmp 要 <cstring>
类模板 12.2 / 12.2.1 类模板不是类型(Box b; ✗,必须 Box<int> b;);Box<int> 与 Box<double> 是两个独立类(静态成员各一份);成员函数类外定义三处不能漏(模板头 / Point<T>:: / 返回类型前);"用到才实例化"
简单类模板 12.2.2 Pair<T1, T2>:两个模板参数;类内定义自动内联,模板类因此常整体写在类内;std::pair 就是它
默认模板参数 12.2.3 / 12.2.4 template <typename T = int, int size = 10>;默认值必须从右往左连续给;同一作用域只给一次;Box<> 的尖括号省不掉;函数模板的默认模板参数不能用于推导
有界数组模板 12.2.5 T data[size](size 是编译期常量)+ 带检查的 get / set;operator[] 故意不检查(和 std::vector 的 [] vs at() 一致)
定制(特化) 12.3 / 12.3.1 / 12.3.2 通用 ≠ 正确(指针浅拷贝、字符串比地址、bool 浪费);全特化 template <> class Stack<X> { ... };(整个类重写);成员特化 template <> void Stack<X>::push(...) { }(只换一个)

二、必须记住的八个结论

  1. 模板是"图纸",不是"房子"。写一份 template <typename T> T f(T, T),用了 int、double、char 三种类型,最终会编出三个真函数。这个"照图纸造东西"的动作叫实例化,发生在编译期——所以模板是编译期多态(和 11.4 的运行期多态并列)。
  2. typename 和 class 在模板参数里完全等价;T 的作用域只到紧跟的那个定义为止(所以每个类外成员函数都要重写模板头)。模板推导阶段不做类型转换 → f(3, 4.5) 报 deduced conflicting types,要显式写 f<double>(3, 4.5)。
  3. 模板的定义(含类模板的全部成员函数)必须写在头文件里。拆成 .h 声明 + .cpp 实现 → 链接期 undefined reference to 'int f<int>(int, int)'。原因:编译器在实例化点必须看到定义。(这也是 10.4.2 内联成员函数 的同款例外。)
  4. 类模板不是类型:Stack s; ✗,必须 Stack<int> s;(即使所有模板参数都有默认值,也要写 Stack<> s;——那对尖括号省不掉)。Stack<int> 与 Stack<double> 是两个毫不相干的类:静态成员各一份、不能互相赋值、函数重载算两个。
  5. 类模板的成员函数在类外定义时,三处不能漏:① 前面写 template <typename T>;② 类名写 Point<T>::;③ 返回类型要在模板头之后。构造函数记成":: 前带 <T>、:: 后不带"。
  6. 默认模板参数从右往左连续给,同一作用域只给一次。类模板写 Box<> 可以省实参,但省不掉尖括号。函数模板的默认模板参数不能用于推导(f() 是错的),所以这个特性主要是给类模板用的。
  7. 通用模板只保证"能编译",不保证"结果对"。它在指针 / const char * / 需要深拷贝的类型上都会办错事。解法是定制(特化):整个类都换 → 全特化(template <> class Stack<X> { ... };);只换一个成员 → 成员特化(template <> void Stack<X>::push(...) { })。两者都要求"具体类型一字不差"(const char * ≠ char *,写错会悄悄退回通用版)。
  8. 能"用"就别"写"。现实里 90% 的模板是拿来用的(vector<int>、sort、string);自己写模板是"造工具箱"的活;特化是"工具箱某处不合适"的应急补丁。学习顺序:会用 → 会定制 → 会写。

三、动手 Demo:两关自测

第一关(写代码):一个"通用小工具箱"

要求:用本章的东西,做一个能统计任意类型、任意长度数据的工具。

项 要求
函数模板 myMax(T, T) —— 取两个值里较大的(12.1)
类模板 Stats<T, size> —— 装一批数据,能算最大/最小/和/平均
默认模板参数 T = int、size = 10,让 Stats<> 能直接用(12.2.3 / 12.2.4)
边界保护 装满之后再 add 要拒绝并提示,不能越界写(12.2.5)
T 的通用性 至少要能用 int、double、string 三种类型跑通

参考实现(自己先写,写不出来再看):

#include <iostream>
#include <string>
using namespace std;

// ① 函数模板
template <typename T>
T myMax(T a, T b) { return a > b ? a : b; }

// ② 类模板(带默认模板参数)
template <typename T = int, int size = 10>
class Stats {
private:
    T   data[size];
    int n;

public:
    Stats() : n(0) {
        for (int i = 0; i < size; i++) data[i] = T();     // ★ T 的零值
    }

    // ★ 带边界保护的写入
    bool add(T v) {
        if (n >= size) {
            cout << "!已满(" << size << " 个)," << v << " 没能进来" << endl;
            return false;
        }
        data[n++] = v;
        return true;
    }

    int count() const { return n; }
    int capacity() const { return size; }

    T maxValue() const {
        T m = data[0];
        for (int i = 1; i < n; i++) if (data[i] > m) m = data[i];
        return m;
    }

    T minValue() const {
        T m = data[0];
        for (int i = 1; i < n; i++) if (data[i] < m) m = data[i];
        return m;
    }

    T sum() const {
        T s = T();
        for (int i = 0; i < n; i++) s += data[i];
        return s;
    }

    // ★ 平均值要转 double —— 否则 int 数据会做整数除法
    double average() const { return n > 0 ? (double)sum() / n : 0.0; }

    void show() const {
        cout << "[" << n << "/" << size << "] ";
        for (int i = 0; i < n; i++) cout << data[i] << " ";
        cout << endl;
    }
};

int main() {
    Stats<>         s;                       // int[10](全默认)
    int a[] = { 7, 2, 9, 4, 5 };
    for (int i = 0; i < 5; i++) s.add(a[i]);

    s.show();
    cout << "最大 " << s.maxValue() << ",最小 " << s.minValue()
         << ",和 "   << s.sum()      << ",平均 " << s.average() << endl;

    Stats<double, 3> d;                      // 只给类型和长度
    d.add(1.5);
    d.add(2.7);
    d.add(0.3);
    d.add(9.9);                              // ★ 满了,会被拒绝
    d.show();
    cout << "最大 " << d.maxValue() << ",平均 " << d.average() << endl;

    Stats<string, 3> t;                      // ★ T 换成 string 也能装
    t.add("pear");
    t.add("apple");
    t.add("orange");
    t.show();
    cout << "最大 " << t.maxValue() << ",最小 " << t.minValue() << endl;

    cout << myMax(3, 7) << " " << myMax(2.5, 1.5) << endl;
    return 0;
}

看到什么算成功:

[5/10] 7 2 9 4 5 
最大 9,最小 2,和 27,平均 5.4
!已满(3 个),9.9 没能进来
[3/3] 1.5 2.7 0.3 
最大 2.7,平均 1.5
[3/3] pear apple orange 
最大 pear,最小 apple
7 2.5

读结果:

现象 说明
一份 Stats 同时服务 int、double、string 三个类型 → 三个独立的类(12.2 性质 2)
Stats<> 直接能用 两个默认模板参数同时生效(12.2.4)
满员时 add 被拒绝、数据完好 12.2.5 的边界保护——把规矩封进了类型里
t.maxValue() 得 pear T = string 时,> 是字符串的字典序比较(sort 的道理)
average() 返回 double 模板里要提防"整数除法"——sum() 是 int,不转 double 的话 27/5 会得到 5 而不是 5.4

如果这一关你能独立写出来:说明本章三组(函数模板、类模板、默认参数 + 边界保护)你已经能连起来用了。

加分实验:

  1. 把 average() 里的 (double) 去掉,重新运行 → 平均 变成 5(整数除法)。这条坑在模板里特别隐蔽,因为类型是 T,你看不出到底是不是整数。
  2. 把 Stats<string, 3> t; 改成 Stats<string> t; → 容量变成 10(默认值生效)。
  3. 给 Stats 加一个 Stats<const char *> 的全特化版本(12.3.1)→ 让字符串也用"复制内容"的方式存。这就是标准的"定制"练习。

第二关(找错):八个判断,逐个说明对错与原因

# 代码 对/错 为什么
① template <class T = int> T f(T x); 然后调用 f(); 错 默认模板参数不能用于推导。要写成 f<int>()(而且函数参数还得有默认值)
② template <class T> class S { ... }; 然后 S s; 错 类模板必须给模板实参,报 class template argument required。要写 S<int> s;
③ template <class T> void S::f() { }(S 是类模板) 错 类名漏了 <T>,报 invalid use of template-name 'S' without an argument list。应为 void S<T>::f(),且前面要有 template <class T>
④ template <class T = int, class U> class P { }; 错 默认值必须从右往左连续给。T 有默认、U 没有 → 报 no default argument for 'U'
⑤ 把 template <> class Stack<const char *> { ... }; 写在通用模板之前 错 特化必须出现在通用模板之后,报 specialization of 'Stack<const char*>' before template declaration
⑥ 模板声明放 a.h、定义放 a.cpp 错 别的 .cpp 看不到定义 → 链接期 undefined reference。模板定义必须进头文件
⑦ maxOf(3, 4.5) 错 T 推出两个矛盾答案 → deduced conflicting types for parameter 'T' ('int' and 'double')。改 maxOf<double>(3, 4.5)
⑧ 写了 Stack<char *> 的成员特化,实际用 Stack<const char *> 错(而且不报错) char * 与 const char * 是两个类型 → 悄悄退回通用版 → 浅拷贝、打印地址全回来了。这是最危险的一种错:没有编译错误。

30 秒实验:把 ①②③④⑤⑥⑦ 逐个在编译器里敲一遍,认一下每种报错的样子。

四、常见误区(本章汇总)

  1. "模板会拖慢程序"——运行时不会(实例化出来的就是普通函数/类)。代价在编译期:用几种类型就编几份代码,编译更慢、可执行文件更大。
  2. "模板什么类型都能塞"——它不是"运行时橡皮泥"。模板参数一确定,生成的类/函数就是严格类型检查的普通类(Pair<int,string> 的 setSecond(1) 会报错)。
  3. "模板和虚函数是竞争关系,哪个新用哪个"——两者解决不同问题。类型在编译时能确定 → 模板(零开销);编时确定不了(用户输入、插件、容器里的异构对象)→ 虚函数。判断标准只有一句话:类型什么时候才能确定?
  4. "通用模板已经够通用了,不用特化"——通用 ≠ 正确。指针要深拷贝、字符串要比内容、bool 可以按位存——这些"例外"正是标准库大量使用特化的原因。
  5. "自己写模板才是本事"——顺序反了。先用熟现成的(string、vector、sort)、再看懂模板怎么写的、最后才会自己写。现实工作里,会用 + 会定制占 90%。
  6. "模板编过了就没问题"——成员函数"用到才实例化",没被调用的成员即使写错也不会被发现。所以模板代码一定要"每种用法都真跑一遍"。
  7. "特化写完就对上了"——不检查类型是否一字不差 → 悄悄退回通用版(第 ⑧ 条)。跑一遍看结果,是唯一的保险。
  8. "模板是纯语法技巧,和工程没关系"——恰恰相反:标准库、编译器、你能想到的所有高性能库,都是模板搭的。它也是 C++ 编译慢的头号原因——这门语言用"编译时间"换来了"运行效率"。

五、写完这一章,你会什么了

这一章之后,你不再只会"用手边的类型",而是能"和类型无关地写代码"。 具体来说:

同时拿到两条能一直用的判断力:

判断 出处 一句话
"这个重复是不是'只换了类型'?" 12.1 是 → 抽模板;不是 → 别硬套
"这次该用模板还是虚函数?" 12.1、11.4 类型什么时候能确定? 编译时 → 模板;运行时 → 虚函数
自测八问(点开对答案) 1. 模板"实例化"是什么意思?发生在什么时候?——**编译器照着模板图纸,按实际使用的类型"生成真函数/真类"**。发生在**编译期**。**用了几种类型就生成几份**,所以它属于**编译期多态**。 2. 为什么 `maxOf(3, 4.5)` 编译不过?——因为**模板的参数推导阶段不做类型转换**,`T` 被推出 `int` 和 `double` 两个矛盾答案,报 `deduced conflicting types`。改法:**显式指定 `maxOf(3, 4.5)`**。 3. 模板的定义为什么必须写在头文件里?——因为**实例化点必须能看到定义**。声明在 `.h`、定义在别的 `.cpp` → **链接期 `undefined reference to '...(int, int)'`**。 4. `Box` 和 `Box` 是什么关系?`Box b;` 能写吗?——**两个完全独立的类**(静态成员各一份、不能互相赋值)。**`Box b;` 不能写**——**类模板必须给模板实参**;即使参数全有默认值,也要写 `Box<> b;`。 5. 类模板的成员函数在类外定义时,哪三处不能漏?——**① 每个函数前写 `template `;② 类名写 `Point::`;③ 返回类型写在模板头之后。** 构造函数记成"**`::` 前带 ``、`::` 后不带**"。 6. 默认模板参数的两条硬规则是什么?——**① 默认值必须从右往左连续给**(`template ` 错);**② 同一作用域只给一次**。另外:**类模板写 `Box<>` 时尖括号不能省**;**函数模板的默认模板参数不能用于推导**。 7. 通用模板在哪些类型上会"办错事"?怎么修?——**指针(浅拷贝)、`const char *`(比地址而不是内容)、`bool`(浪费空间)。** 修法:**对不合适的具体类型做"定制(特化)"**——**整个类都换用全特化**(`template <> class Stack { ... };`)、**只换一个成员用成员特化**(`template <> void Stack::push(...) { }`)。**注意:特化的类型必须和实际使用一字不差,否则悄悄退回通用版。** 8. 模板和虚函数分别用在什么场合?——**模板:类型在编译期就能确定**(零运行开销,代价是编译变慢、每种类型编一份)。**虚函数:类型要到运行期才知道**(用户输入、插件、异构容器),代价是一次间接跳转。**判断标准:类型什么时候才能确定?**

相关笔记