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

函数模板=一份"函数图纸",图纸上留了一个空位,等着填"类型"。写一次 template <typename T> T bigger(T a, T b),编译器就按你实际传进去的类型,分别"造"出 bigger(int,int)、bigger(double,double)、bigger(char,char) 三个真正独立的函数。它解决的是"同一套逻辑、只换类型"这种重复劳动,而且比宏安全——它有类型检查。

📝 本节和前面几节的关系

第十一章结尾埋了一句话:多态分两类——运行期多态(虚函数)和编译期多态(重载与模板)。11.4 把前一半讲完了,本章讲后一半。所以模板不是凭空冒出来的语法糖:它是"编译期的多态"——编译器在编代码的时候就把类型替换好,运行时一点额外开销都没有。 往回收旧账:模板会用到 6.5 的重载(多个模板版本共存)、10.2 的构造函数(类模板要初始化)、10.4.3 的静态成员(Stack<int> 和 Stack<double> 各有一份)。

一、先看一份真实的痛苦

假设你要写一个"取两个数中较大的那个"。只写 int 版,第一天够用:

int bigger(int a, int b) { return a > b ? a : b; }

第二天要处理小数,第三天要比较字符,第四天要比较字符串……于是:

int         bigger(int a, int b)         { return a > b ? a : b; }
double      bigger(double a, double b)   { return a > b ? a : b; }
char        bigger(char a, char b)       { return a > b ? a : b; }
string      bigger(string a, string b)   { return a > b ? a : b; }

四份函数,除了类型名,一个字都不差。 这带来三个问题:

问题 说明
写得烦 每一种类型抄一遍
改得险 哪天发现逻辑错了(比如 > 该改成 >=),得改四处——漏一处就留下一个"行为不一致"的函数
加不动 将来要用在 long long、float、自定义的 Money 上,还得继续抄

根本原因:这四份代码里,唯一变化的东西是"类型",可类型偏偏是写死的。

模板要干的事只有一件:把"类型"也变成参数。

二、模板:把类型变成参数

template <typename T>                    // ★ 声明:"下面这个函数里,T 是一个类型"
T bigger(T a, T b) {                     // ★ T 出现在哪儿,就换成什么类型
    return a > b ? a : b;
}

用的时候,什么都不用特别写:

bigger(3, 7)              // 编译器看参数是 int    → 把 T 换成 int
bigger(2.5, 1.5)          // 看参数是 double       → 把 T 换成 double
bigger('a', 'z')          // 看参数是 char         → 把 T 换成 char
bigger(string("a"), string("b"))   // T 换成 string

编译器在背后做的事,等价于你手写了这四份:

int    bigger(int a, int b)          { return a > b ? a : b; }
double bigger(double a, double b)    { return a > b ? a : b; }
char   bigger(char a, char b)        { return a > b ? a : b; }
string bigger(string a, string b)    { return a > b ? a : b; }

所以模板是"图纸",不是"房子"。 一张图纸能盖出很多间房子——每间房子都是真的、独立的。

术语:编译器"照着图纸造出真函数"这个过程叫 实例化(instantiation),造出来的函数叫模板的实例。你说不出"模板编译后有几个函数"——它取决于你用了几种类型。

三、这是"编译期的多态"

第十一章的 virtual 是运行期多态:程序跑起来之后,才知道该调哪个函数(看对象的真实身份)。

模板是编译期多态:代码还没开始跑,编译器就把类型替换好了。

运行期多态(11.4) 编译期多态(本章)
靠什么 virtual + 指针/引用 template + 重载
什么时候定 运行时(顺着 vtable 查) 编译时(直接替换类型)
运行开销 一次间接跳转 零
要求 类型之间有继承关系 类型之间可以毫无关系——只要"能跑通那段代码"
加新类型 写个新子类就行,老代码不动 换个类型重新编译(代码要重编)

最后一行是模板的代价:模板是"用编译时间换运行效率、用重新编译换零开销"。这也是为什么 C++ 编译慢。

一句话记住区别:虚函数是"运行时按身份挑",模板是"编译时按类型换"。

四、本章路线图

节 讲什么
12.1 组 函数模板:语法(12.1.1)、作用与"数组当模板参数"(12.1.2,含实例01)、重载与字符串的坑(12.1.3,含实例02)
12.2 组 类模板:定义与声明(12.2.1)、简单类模板(12.2.2)、默认模板参数(12.2.3)、给具体类型设默认值(12.2.4)、有界数组模板(12.2.5)
12.3 组 模板的使用与定制:什么时候必须"定制"(12.3)、定制整个类(12.3.1)、只定制一个成员函数(12.3.2)
12.4 小结:知识地图 + 八个必记结论 + 两关自测

五、动手 Demo:一个 bigger 应付所有类型

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

// ★ 只有这一份代码 —— 它是一张"图纸"
template <typename T>
T bigger(T a, T b) {
    return a > b ? a : b;
}

int main() {
    cout << bigger(3, 7) << endl;                    // T = int
    cout << bigger(2.5, 1.5) << endl;                // T = double
    cout << bigger('a', 'z') << endl;                // T = char

    string s1 = "apple", s2 = "banana";
    cout << bigger(s1, s2) << endl;                  // T = string(按字典序比)
    return 0;
}

看到什么算成功:

7
2.5
z
banana

读结果:

调用 T 被换成 为什么
bigger(3, 7) int 两个参数都是 int
bigger('a', 'z') char 两个参数都是 char,输出字符 z(不是 122)
bigger(s1, s2) string string 重载过 >,按字典序比,"banana" 排在 "apple" 后面

这一份 bigger,替掉了四份代码。 而且哪天逻辑要改,只改这一处。

30 秒实验:把 bigger 里的 a > b 改成 a < b(改成"取小的"),重新编译运行。

六、常见误区

  1. "模板会拖慢程序"——运行时不会。实例化出来的函数,和你手写的一模一样。代价在编译期:用了几种类型就编出几份代码,编译更慢、可执行文件更大。
  2. "模板是给高手用的炫技语法"——恰恰相反。模板存在的理由是消灭重复。你写第四份"只换类型"的函数时,就该想到模板。
  3. "一份模板能省掉所有重载"——不能。有些行为天生跟类型有关(比如字符串的"大小"要比内容、不能比地址),这时还是得单独写(12.1.3 讲)。
  4. "模板只对内置类型有效"——不是。上面 bigger(s1, s2) 用的就是 string。只要那段代码在那个类型上"讲得通",模板就能用——这正是它比继承更灵活的地方(int 和 string 可没有任何继承关系)。
  5. "先学了模板,以后就不用管虚函数了"——两件事解决不同问题。模板要求"编译时就知道类型";而虚函数处理的正是"编译时不知道具体是哪个子类"(比如用户输入的形状、插件里加载的类)。该用哪个,看"类型在什么时候才能确定"。
自测三问(点开对答案) 1. `template T bigger(T a, T b)` 用了 `int`、`double`、`char` 三种类型,编译器最终生成了几个函数?——**三个真正独立的函数**(模板的实例)。**写代码时你只写了一份,编完之后是三份**——这就是"模板是图纸不是房子"。 2. 为什么说模板是"编译期多态"?和 `virtual` 有什么根本区别?——**模板在编译时就把类型替换好**(零运行开销,但类型必须编译时确定);**`virtual` 在运行时按对象真实身份查表**(有一次间接跳转,但能处理"编译时不知道是什么子类"的场合)。 3. 模板的代价是什么?——**编译时间和可执行文件大小**(用了几种类型就编几份代码)、**报错难读**,以及**改动模板要重新编译使用它的所有代码**(不像虚函数那样"加个新子类就行")。

相关笔记