📄 一句话概括
第十二章讲的是"把类型变成参数"。一句话串起来:函数模板(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(...) { }(只换一个) |
二、必须记住的八个结论
- 模板是"图纸",不是"房子"。写一份
template <typename T> T f(T, T),用了int、double、char三种类型,最终会编出三个真函数。这个"照图纸造东西"的动作叫实例化,发生在编译期——所以模板是编译期多态(和 11.4 的运行期多态并列)。 typename和class在模板参数里完全等价;T的作用域只到紧跟的那个定义为止(所以每个类外成员函数都要重写模板头)。模板推导阶段不做类型转换 →f(3, 4.5)报deduced conflicting types,要显式写f<double>(3, 4.5)。- 模板的定义(含类模板的全部成员函数)必须写在头文件里。拆成
.h声明 +.cpp实现 → 链接期undefined reference to 'int f<int>(int, int)'。原因:编译器在实例化点必须看到定义。(这也是10.4.2 内联成员函数的同款例外。) - 类模板不是类型:
Stack s;✗,必须Stack<int> s;(即使所有模板参数都有默认值,也要写Stack<> s;——那对尖括号省不掉)。Stack<int>与Stack<double>是两个毫不相干的类:静态成员各一份、不能互相赋值、函数重载算两个。 - 类模板的成员函数在类外定义时,三处不能漏:① 前面写
template <typename T>;② 类名写Point<T>::;③ 返回类型要在模板头之后。构造函数记成"::前带<T>、::后不带"。 - 默认模板参数从右往左连续给,同一作用域只给一次。类模板写
Box<>可以省实参,但省不掉尖括号。函数模板的默认模板参数不能用于推导(f()是错的),所以这个特性主要是给类模板用的。 - 通用模板只保证"能编译",不保证"结果对"。它在指针 /
const char */ 需要深拷贝的类型上都会办错事。解法是定制(特化):整个类都换 → 全特化(template <> class Stack<X> { ... };);只换一个成员 → 成员特化(template <> void Stack<X>::push(...) { })。两者都要求"具体类型一字不差"(const char *≠char *,写错会悄悄退回通用版)。 - 能"用"就别"写"。现实里 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 |
如果这一关你能独立写出来:说明本章三组(函数模板、类模板、默认参数 + 边界保护)你已经能连起来用了。
加分实验:
- 把
average()里的(double)去掉,重新运行 →平均变成5(整数除法)。这条坑在模板里特别隐蔽,因为类型是T,你看不出到底是不是整数。 - 把
Stats<string, 3> t;改成Stats<string> t;→ 容量变成 10(默认值生效)。 - 给
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 秒实验:把 ①②③④⑤⑥⑦ 逐个在编译器里敲一遍,认一下每种报错的样子。
- 重点看 ② 和 ⑦:这两种报错最常出现,也最容易让人"卡住".
- 第 ⑧ 条要单独记:它不报错。所以"定制之后一定要真的跑一遍、看一眼结果"——编译器不会替你确认"你定制的那个类型是不是实际用的那个"。
四、常见误区(本章汇总)
- "模板会拖慢程序"——运行时不会(实例化出来的就是普通函数/类)。代价在编译期:用几种类型就编几份代码,编译更慢、可执行文件更大。
- "模板什么类型都能塞"——它不是"运行时橡皮泥"。模板参数一确定,生成的类/函数就是严格类型检查的普通类(
Pair<int,string>的setSecond(1)会报错)。 - "模板和虚函数是竞争关系,哪个新用哪个"——两者解决不同问题。类型在编译时能确定 → 模板(零开销);编时确定不了(用户输入、插件、容器里的异构对象)→ 虚函数。判断标准只有一句话:类型什么时候才能确定?
- "通用模板已经够通用了,不用特化"——通用 ≠ 正确。指针要深拷贝、字符串要比内容、
bool可以按位存——这些"例外"正是标准库大量使用特化的原因。 - "自己写模板才是本事"——顺序反了。先用熟现成的(
string、vector、sort)、再看懂模板怎么写的、最后才会自己写。现实工作里,会用 + 会定制占 90%。 - "模板编过了就没问题"——成员函数"用到才实例化",没被调用的成员即使写错也不会被发现。所以模板代码一定要"每种用法都真跑一遍"。
- "特化写完就对上了"——不检查类型是否一字不差 → 悄悄退回通用版(第 ⑧ 条)。跑一遍看结果,是唯一的保险。
- "模板是纯语法技巧,和工程没关系"——恰恰相反:标准库、编译器、你能想到的所有高性能库,都是模板搭的。它也是 C++ 编译慢的头号原因——这门语言用"编译时间"换来了"运行效率"。
五、写完这一章,你会什么了
这一章之后,你不再只会"用手边的类型",而是能"和类型无关地写代码"。 具体来说:
- 能消灭"只换类型"的重复代码(12.1)——而且比
#define安全。 - 能写出跟类型无关的函数:
maxOf、sumOf、printAll(12.1.2)。 - 能让模板处理"数组长度"这类编译期常量(12.1.2 实例01、12.2.5)。
- 能写自己的容器类模板:
Pair、Stack、BoundedArray(12.2 组)。 - 能配默认值,让常用配置少写几个字(12.2.3 / 12.2.4)。
- 能在通用模板不合适时,用全特化 / 成员特化"精准打补丁"(12.3 组)。
- 能看懂标准库里那些吓人的类型名:
basic_string<char, char_traits<char>, allocator<char>>—— 知道多出来的都是默认参数。
同时拿到两条能一直用的判断力:
| 判断 | 出处 | 一句话 |
|---|---|---|
| "这个重复是不是'只换了类型'?" | 12.1 | 是 → 抽模板;不是 → 别硬套 |
| "这次该用模板还是虚函数?" | 12.1、11.4 | 类型什么时候能确定? 编译时 → 模板;运行时 → 虚函数 |
自测八问(点开对答案)
1. 模板"实例化"是什么意思?发生在什么时候?——**编译器照着模板图纸,按实际使用的类型"生成真函数/真类"**。发生在**编译期**。**用了几种类型就生成几份**,所以它属于**编译期多态**。 2. 为什么 `maxOf(3, 4.5)` 编译不过?——因为**模板的参数推导阶段不做类型转换**,`T` 被推出 `int` 和 `double` 两个矛盾答案,报 `deduced conflicting types`。改法:**显式指定 `maxOf- ☑ 第十三章 STL(标准模板库) —— 已完结:小结(容器 + 算法 + 迭代器:
vector/map/sort/ erase-remove 惯用法)