"简单类模板"的意思不是"很弱的类模板",而是"一个完整、能用、代码不长、全部写在类内的类模板"。本节用 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 秒实验(试出"模板不是万能盒子"):
- 把
p1.setSecond("一")改成p1.setSecond(1)。 - 编译 → 报错:
invalid conversion from 'int' to 'const char*'或no matching function for call to 'Pair<int, std::string>::setSecond(int)'。 - 注意报错里的名字:
Pair<int, std::string>::setSecond——编译器已经把它当成一个具体的普通类了。 - 结论:模板参数一旦定下来,这个类就是"严格的、有类型检查的普通类"——没有任何"运行时橡皮泥"的成分。
- 再把
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 的有界数组);没有 → 当小结构体用,公开也合理。
五、常见误区
- "模板类天然什么类型都能塞"——错。
Pair<int, string>就是一个严格的类,setSecond(1)会当场报错。模板只是"造类的机器",造出来的类是普通的、强类型的类。 - "成员函数写在类内会变慢"——不会。类内定义自动内联(10.4.2),只是建议编译器内联,不影响正确性。模板类这么写反而主流,因为省掉了那一堆
template <typename T1, typename T2>。 - "两个模板参数必须不同类型"——不必。
Pair<int, int>完全合法(就是两个int)。 - "
Pair<int, string>和Pair<string, int>差不多"——是两个完全不同的类(顺序不同,实参不同)。Pair<string, int>里setFirst要的是string。 - "写
Pair<int> p(1, "a")应该能猜到第二个是 string"——不能。模板实参不写全就报错(除非第二个参数有默认值,那是 12.2.3 的内容)。 - "自己写模板是日常工作的主流"——不是。现实里 90% 是"用"现成模板(
pair、vector、string),自己写模板是"库开发者"的活。但看懂模板、会给模板打补丁,是每个 C++ 程序员的必备技能。
自测三问(点开对答案)
1. `Pair- ☐ 12.2.3 默认模板参数(下一节)