函数模板=一份"函数图纸",图纸上留了一个空位,等着填"类型"。写一次 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(改成"取小的"),重新编译运行。
- 四行输出全部跟着变了——因为从头到尾就只有一份实现。
- 这就是模板买来的东西:一处修改,处处生效。
六、常见误区
- "模板会拖慢程序"——运行时不会。实例化出来的函数,和你手写的一模一样。代价在编译期:用了几种类型就编出几份代码,编译更慢、可执行文件更大。
- "模板是给高手用的炫技语法"——恰恰相反。模板存在的理由是消灭重复。你写第四份"只换类型"的函数时,就该想到模板。
- "一份模板能省掉所有重载"——不能。有些行为天生跟类型有关(比如字符串的"大小"要比内容、不能比地址),这时还是得单独写(12.1.3 讲)。
- "模板只对内置类型有效"——不是。上面
bigger(s1, s2)用的就是string。只要那段代码在那个类型上"讲得通",模板就能用——这正是它比继承更灵活的地方(int和string可没有任何继承关系)。 - "先学了模板,以后就不用管虚函数了"——两件事解决不同问题。模板要求"编译时就知道类型";而虚函数处理的正是"编译时不知道具体是哪个子类"(比如用户输入的形状、插件里加载的类)。该用哪个,看"类型在什么时候才能确定"。
自测三问(点开对答案)
1. `template- ☐ 12.1.1 函数模板的定义(下一节)
相关笔记
- 函数模板的定义 —— 下一节:把
template这行拆开看 - 虚函数概述 —— 上一章讲完了"运行期多态"
- 重载函数 —— 编译期多态的另一半:重载
- 静态类成员 ——
Stack<int>与Stack<double>各有各的静态成员 - 🔷 C++基础 · 索引 —— 全课路线图