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

"简单类模板"的意思不是"很弱的类模板",而是"一个完整、能用、代码不长、全部写在类内的类模板"。本节用 Pair<T1, T2>(一对儿)把它写完整:两个模板参数、构造、取/改成员、打印。全部写在类内的写法有个好处——成员函数自动内联(10.4.2),而且完全不用写那行"类外定义"的模板头,所以模板类在源码里通常整体写在类内。

📝 本节和前面几节的关系

12.2.1 讲的是"语法不能怎么写错",本节是"照着写一个完整的东西"。它把两个模板参数(T1、T2)正式用起来,为 12.2.3 的"默认模板参数"提供落脚点——默认参数总是加在这种"多参数类模板"上。 顺带说一句:Pair 这个类在 C++ 标准库里真的存在,叫 std::pair(在 <utility> 里)——你学完这节,就相当于"重新发明"了一次标准库的零件。

一、目标:一个"装两个东西"的盒子

要比 12.2 的 Box 再进一步:装两个东西,而且两个东西的类型可以不同。

为什么需要它:现实里到处是"成对"的数据 —— 学号配姓名(int + string)、单词配释义(string + string)、坐标配颜色(int + char)……

如果用普通类:得为每种组合写一个类(IntStringPair、StringStringPair……)——又是那个老问题。

用类模板:

template <typename T1, typename T2>
class Pair {
    ...
};

两个模板参数,用逗号隔开,各自可以在类里任何一个需要类型的地方出现。

二、完整实现(全部写在类内)

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

template <typename T1, typename T2>
class Pair {
private:
    T1 first;                     // ★ 第一个东西:类型 T1
    T2 second;                    // ★ 第二个东西:类型 T2

public:
    // 构造函数:两个参数各自对应一个模板参数
    Pair(T1 a, T2 b) : first(a), second(b) { }

    // 取
    T1 getFirst()  const { return first;  }
    T2 getSecond() const { return second; }

    // 改(注意:参数类型分别也是 T1、T2)
    void setFirst(T1 v)  { first  = v; }
    void setSecond(T2 v) { second = v; }

    // 打印
    void show() const {
        cout << "(" << first << ", " << second << ")" << endl;
    }
};

几个可以留意的地方:

位置 说明
T1、T2 的用法 它们和普通类型名完全一样——可以当成员类型、当参数类型、当返回类型
Pair(T1 a, T2 b) 构造函数必须给出两个参数(没写默认值)——这和普通类没有任何区别
show() const const 成员函数的规则(10.4.1)照旧生效:里面不能改成员
全部写在类内 成员函数自动内联(10.4.2),并且完全不用写"类外定义"那行模板头——这就是"简单类模板"的省事之处

三、动手 Demo:四种组合

int main() {
    Pair<int, string>    p1(1, "one");          // 学号 + 名字
    Pair<string, double> p2("pi", 3.14);        // 单词 + 数值
    Pair<int, int>       p3(3, 4);              // 两个整数
    Pair<char, char>     p4('A', 'B');

    p1.show();
    p2.show();
    p3.show();
    p4.show();

    cout << "── 改一个成员 ──" << endl;
    p1.setSecond("一");                          // ★ 参数必须是 string
    p1.show();

    cout << "── 把两个成员取出来用 ──" << endl;
    cout << p3.getFirst() << " + " << p3.getSecond()
         << " = " << p3.getFirst() + p3.getSecond() << endl;

    return 0;
}

看到什么算成功:

(1, one)
(pi, 3.14)
(3, 4)
(A, B)
── 改一个成员 ──
(1, 一)
── 把两个成员取出来用 ──
3 + 4 = 7

读结果:

现象 说明
四种完全不同类型的组合都能用 编译器造出了四个类:Pair<int,string>、Pair<string,double>、Pair<int,int>、Pair<char,char>
p1.setSecond("一") 能编过 在 Pair<int,string> 里,setSecond 的参数已经是 string
p3.getFirst() + p3.getSecond() 在 Pair<int,int> 里,两个 get 都返回 int,加法是 int 的加法
p4 打印出 (A, B) 而不是数字 T1、T2 都是 char,cout 输出字符

30 秒实验(试出"模板不是万能盒子"):

  1. 把 p1.setSecond("一") 改成 p1.setSecond(1)。
  2. 编译 → 报错:invalid conversion from 'int' to 'const char*' 或 no matching function for call to 'Pair<int, std::string>::setSecond(int)'。
  3. 注意报错里的名字:Pair<int, std::string>::setSecond——编译器已经把它当成一个具体的普通类了。
  4. 结论:模板参数一旦定下来,这个类就是"严格的、有类型检查的普通类"——没有任何"运行时橡皮泥"的成分。
  5. 再把 Pair<char, char> p4('A', 'B') 改成 Pair<char, char> p4("A", "B") → 报错(const char * 不能当 char)。同样是类型检查在起作用。

四、Pair 和标准库的 std::pair

你刚写的东西,标准库里早就有了,名字叫 std::pair(头文件 <utility>):

#include <utility>
pair<int, string> p(1, "one");
cout << p.first << " " << p.second << endl;     // ★ 它把两个成员公开成 first / second
对比 我们写的 Pair 标准库的 pair
成员访问 getFirst() / getSecond()(函数) p.first / p.second(直接访问)
设计理由 private + 函数(10.1.1 的规矩) 它故意做成"透明的小捆包"——反正就两个值,藏着反而添麻烦
用途 练习 map 的元素、make_pair(...) 的返回值……

为什么标准库敢把成员公开?因为这属于"值语义的小聚合"(像 8.1 的结构体)——没有"不变式"要维护(不存在"两个成员必须满足某个关系"的规矩),所以封装的收益很小、透明的收益很大。

判断标准:类里有没有"必须守住的规矩"(不变式)? 有 → private + 函数(如 12.2.5 的有界数组);没有 → 当小结构体用,公开也合理。

五、常见误区

  1. "模板类天然什么类型都能塞"——错。Pair<int, string> 就是一个严格的类,setSecond(1) 会当场报错。模板只是"造类的机器",造出来的类是普通的、强类型的类。
  2. "成员函数写在类内会变慢"——不会。类内定义自动内联(10.4.2),只是建议编译器内联,不影响正确性。模板类这么写反而主流,因为省掉了那一堆 template <typename T1, typename T2>。
  3. "两个模板参数必须不同类型"——不必。Pair<int, int> 完全合法(就是两个 int)。
  4. "Pair<int, string> 和 Pair<string, int> 差不多"——是两个完全不同的类(顺序不同,实参不同)。Pair<string, int> 里 setFirst 要的是 string。
  5. "写 Pair<int> p(1, "a") 应该能猜到第二个是 string"——不能。模板实参不写全就报错(除非第二个参数有默认值,那是 12.2.3 的内容)。
  6. "自己写模板是日常工作的主流"——不是。现实里 90% 是"用"现成模板(pair、vector、string),自己写模板是"库开发者"的活。但看懂模板、会给模板打补丁,是每个 C++ 程序员的必备技能。
自测三问(点开对答案) 1. `Pair` 里调用 `setSecond(1)` 会发生什么?为什么?——**编译错**(`invalid conversion from 'int' to 'const char*'` 之类)。因为 **`T2` 已经定成 `string`**,这个类就是**一个严格类型的普通类**,参数必须是 `string`。 2. 为什么模板类的成员函数"通常全部写在类内"?——因为**类内定义自动内联**(10.4.2),而且**完全不用写"类外定义"时必须补的那行 `template ` 和 `Pair::`**——**省事、少错**,而且模板本来就得整个放在头文件里,"藏实现"也没有意义。 3. 标准库的 `std::pair` 为什么敢把 `first`、`second` 直接公开?——因为**它没有"需要守住的不变式"**(两个成员之间没有任何约束)。**封装的价值来自"有规矩要守"**;没有规矩的小聚合,公开反而更方便理解。(有规矩的例子见 12.2.5 的有界数组。)

相关笔记