函数模板的定义只有一行是新的:template <typename T>——它声明"这段代码里的 T 是个类型"。语法上要注意三件事:① typename 和 class 完全等价;② T 的作用域只到紧跟的那个函数为止;③ 模板参数不会替你做类型转换(bigger(3, 4.5) 会报 no matching function)。工程上有一条铁规矩:模板的定义必须放在"看得见"的地方(头文件里),否则链接时会报 undefined reference。
12.1 讲了"模板是什么、为什么需要",本节把那一行 template <typename T> 拆开看清楚,并顺着它解释三个反复会踩的坑。它和 10.1.3 类的实现(.h 放声明、.cpp 放实现)正好形成一个对照:普通函数可以拆开写,模板不行——原因就在"实例化"这件事上。
一、语法:拆开看这四个部分
template <typename T> // ① 模板头
T bigger(T a, T b) { // ② 返回类型:T ③ 参数类型:T
return a > b ? a : b; // ④ 函数体:只见 T,不见具体类型
}
| 部位 | 说明 |
|---|---|
template |
关键字,意思是"接下来这个定义带参数"——只是这参数是类型 |
< ... > |
里面叫模板参数表(和函数的参数表意思一样,只是装的是"类型") |
typename T |
声明一个类型参数,名字叫 T。typename 换成 class 完全等价(老书、老代码都用 class,因为它比 C++ 还老) |
| 函数体 | 里面只认识 T——所以这段代码必须"在每一种用到的类型上都讲得通" |
几个可以随便写、不影响正确性的地方:
- 名字:
T只是习惯(Type 的首字母)。ITEM、Elem、myType都行。多个参数时习惯用T1、T2或T、U。 - 个数:可以有多个,用逗号隔开:
template <typename T1, typename T2>。 - 不一定是"类型":还能是"值"(非类型模板参数),比如
template <int N>——12.1.2 的实例01 就用它。
一处不能随便写的:T 的作用域只到紧跟的那个定义为止。
template <typename T>
T bigger(T a, T b) { return a > b ? a : b; }
T other; // ✗ 错:这里没有 T 了(T 只活在上面那个函数里)
所以每写一个函数模板,都要重新来一行 template <...>——这一点在 12.2.1(类模板的成员函数在类外定义)会变成一个高频错误。
二、模板不是函数,是一张图纸
这是理解模板最要紧的一句话。
bigger(3, 7)
编译器看到这行,先做类型推导:两个参数都是 int → T = int → 然后照着图纸造出一个真函数:
int bigger(int a, int b) { return a > b ? a : b; } // 编译器自动生成的
这个"照图纸造函数"的动作叫 实例化(instantiation),造出来的东西叫模板的实例。
三条由此推出的性质:
| 性质 | 说明 |
|---|---|
| 不用不生成 | 你没写过 bigger(long, long) 的调用,编译器就不会生成它——图纸放在那儿不花钱 |
| 每种类型各一份 | bigger<int> 和 bigger<double> 是两个毫不相干的函数(不是一个函数的两副面孔) |
| 两种实例化方式 | 隐式:让编译器从参数推(bigger(3, 7));显式:自己写出来(bigger<int>(3, 7)) |
"显式指定"非常有用,看下一节。
三、类型推导的三条规矩
规矩 1:T 必须能被唯一确定
bigger(3, 4.5); // 一个 int、一个 double —— T 到底该是哪个?
编译错:
error: no matching function for call to 'bigger(int, double)'
note: candidate: template<class T> T bigger(T, T)
note: template argument deduction/substitution failed:
note: deduced conflicting types for parameter 'T' ('int' and 'double')
注意这条报错:它不是说"类型不匹配",而是说"T 推出来有两个不同的答案,我推不下去"。
为什么编译器不"顺手把 int 转成 double"? 因为模板的类型推导发生在"选函数"阶段,此时还没有到"类型转换"阶段。编译器只做"看参数、定 T",不做"改参数"。
解药有两种:
bigger<double>(3, 4.5); // ① 显式告诉它 T = double → 3 会被转成 3.0
bigger((double)3, 4.5); // ② 自己先把类型统一
这就是"显式指定模板实参"最常用的场合:当编译器推不出来、或推出来的不是你要的。
心法:普通函数重载是"先转类型、再选函数";模板是"先定类型、不接受就报错"。 顺序反了,行为就反了。
规矩 2:传值会复制,传 T & 才不复制
template <typename T>
T bigger(T a, T b) { ... } // ★ 按值:a、b 都是"拷贝品"
template <typename T>
const T &biggerRef(const T &a, const T &b) { return a > b ? a : b; } // ★ 按引用:不复制
什么时候该用 const T &:
| 类型 | 建议写法 | 原因 |
|---|---|---|
int、double、char |
T(按值) |
复制几个字节,比"解引用"还快 |
string、大结构体、自定义类 |
const T &(按常量引用) |
避免整份复制(10.2.2 的复制构造函数会跑) |
⚠️ 但 biggerRef 有个陷阱:它返回的引用指向传入的某个参数——只要参数还活着就没事(上面这样用是安全的,因为参数来自调用者)。
真正危险的是这种:
const T &bad(const T &a, const T &b) {
T tmp = a; // 局部变量
return tmp; // ✗ 错:返回局部变量的引用(悬空引用)
}
11.2.3 的 30 秒实验就是这个坑(Complex &operator+ 返回局部变量)——规则一句话:不要返回局部变量的引用。
规矩 3:T 一旦定下来,就是严格类型
bigger<int>(3, 4.5) // T = int;4.5 被转成 4,结果是 4
bigger<double>(3.5, 4) // T = double;4 被转成 4.0,结果是 4.0
显式指定之后,"类型转换"这条正常规则又回来了——但缩窄转换(double → int)会悄悄丢小数,编译器一般只给个 warning。别指望模板替你看着这件事。
四、两条工程铁律
铁律 1:模板的定义必须"看得见"(放头文件)
回想 10.1.3:普通类的做法是 .h 放声明、.cpp 放实现,编译各自进行、链接时拼起来。
模板不能这么干。
// math.h
template <typename T>
T bigger(T a, T b); // ★ 只有声明
// math.cpp
template <typename T>
T bigger(T a, T b) { return a > b ? a : b; } // ★ 实现在另一个"编译单元"里
// main.cpp
#include "math.h"
int main() { cout << bigger(3, 7); }
编译 main.cpp 时:编译器看到 bigger(3, 7),要造出 bigger(int,int)——可它手上只有声明,没有图纸(实现在 math.cpp 里,它看不见)。于是它只好留一个"待填的坑",希望链接器能找到。
链接时报:
undefined reference to `int bigger<int>(int, int)'
原因一句话:模板实例化需要"定义",而每个 .cpp 是分开编译的——别的 .cpp 里的模板定义,本文件根本看不到。
所以规矩是:
模板(以及类模板的所有成员函数定义),一律写在头文件里。 这是"头文件不放定义"这条规则的唯一例外——10.4.2 的内联成员函数也出于同一个理由破例(那里的原话是"定义必须进头文件")。
顺带解释一件事:为什么标准库的
<vector>打开一看全是代码(不是"只有声明")?就是这个原因。
铁律 2:模板的报错"生在深处",要找最后一条 note
模板的报错又长又吓人。读法:
| 看哪里 | 找什么 |
|---|---|
第一行 error: |
说"哪个调用失败了"(往往是用法的问题) |
note: candidate: |
列出它试过哪些版本——通常能看出"我给的参数和哪个模板对不上" |
最后一条 note:(最重要) |
往往直接说出根因,例如 deduced conflicting types for parameter 'T' ('int' and 'double') |
经验:从最后一条 note 往回读,比从第一行往下读快得多。
五、动手 Demo:swap + 显式指定类型
#include <iostream>
using namespace std;
// ① 通用交换:传引用,才改得动原件(7.8.2)
template <typename T>
void mySwap(T &a, T &b) {
T tmp = a;
a = b;
b = tmp;
}
// ② 取较大者
template <typename T>
T maxOf(T a, T b) {
return a > b ? a : b;
}
int main() {
int i = 1, j = 2;
double x = 1.5, y = 2.5;
char c1 = 'a', c2 = 'b';
mySwap(i, j);
mySwap(x, y);
mySwap(c1, c2);
cout << i << " " << j << endl;
cout << x << " " << y << endl;
cout << c1 << " " << c2 << endl;
// ★ 显式指定 T = double:3 会先被转成 3.0,再比较
cout << maxOf<double>(3, 4.5) << endl;
// cout << maxOf(3, 4.5) << endl; // ★ 取消注释 → 编译错(T 推不出来)
return 0;
}
看到什么算成功:
2 1
2.5 1.5
b a
4.5
读结果:
| 现象 | 说明 |
|---|---|
| 三种类型都交换成功 | 一份 mySwap 实例化出三个函数;T & 让它改得动原件 |
maxOf<double>(3, 4.5) 得 4.5 |
显式指定后,3 被转成 3.0,比较正常进行 |
| 注释掉的那行会报错 | deduced conflicting types for parameter 'T' ('int' and 'double') |
30 秒实验(把这个报错看一遍,以后不慌):
- 把
cout << maxOf(3, 4.5)那行的注释去掉,编译。 - 你会看到十几行报错。别从上往下读,直接跳到最后几行
note:。 - 找到
deduced conflicting types for parameter 'T' ('int' and 'double')——这就是根因。 - 改成
maxOf<double>(3, 4.5),编译通过。 - 记住这个手感:模板报错的答案,通常在最后一条 note 里。
六、常见误区
- "
typename和class有区别"——在模板参数里完全等价。template <class T>和template <typename T>一模一样。(typename还有一个"告诉编译器这是类型名"的用法,那是更高级的场合,以后遇到再说。) - "模板会自动帮我做类型转换"——不会。
bigger(3, 4.5)直接报错。要么显式指定bigger<double>(...),要么自己把参数类型统一。 - "模板可以像普通函数那样
.h声明、.cpp实现"——不行,会报undefined reference。模板的定义(含类模板成员函数的定义)必须写在头文件里。 - "模板参数名必须是
T"——随便叫。T只是"Type"的习惯缩写,多人协作时叫T最不容易让人误会。 - "
T声明一次能用好几个函数"——不能。T的作用域只到紧跟的那个定义为止,每个模板函数都要重新写template <...>。 - "模板报错太长,说明很难"——不是难,是啰嗦。从最后一条
note:往回读,往往一眼看到根因。
自测三问(点开对答案)
1. `template- ☐ 12.1.2 函数模板的作用(下一节)