函数模板有三个作用:① 消灭重复(一份逻辑适配无数类型);② 比宏安全(有真正的类型检查,而且参数只求值一次);③ 是标准库的地基(sort、vector、max 全靠它)。本节还讲一个"看起来不起眼、其实很实用"的能力——实例01:把"数组长度"也变成模板参数。写法是 template <typename T, int N>,N 由编译器从数组长度自动推出来,你一个数字都不用数。
12.1 说"模板消灭重复",12.1.1 讲了语法,本节回答"到底值不值得用、比老办法好在哪"。这里的"老办法"指的是 #define 宏——它在 C 语言时代就是干这个的,用模板替掉宏,是 C++ 最早的进化之一。
另外,实例01 用到的"非类型模板参数",是 12.2.4 和 12.2.5 有界数组模板的铺垫——那里的 int size 就是同一个东西。
一、作用一:消灭重复(代码复用)
这是最直接的作用。要求"每段逻辑只写一遍":
| 用模板前 | 用模板后 |
|---|---|
maxInt、maxDouble、maxChar、maxString 四个函数 |
一个 maxOf<T> |
交换三个类型,写三份 swap |
一个 mySwap<T> |
数组求和:sumInt、sumDouble…… |
一个 sum<T> |
| 哪天逻辑要改 | 改一处,处处生效 |
这不只是"少写几个字"。真正值钱的是后面那条:逻辑只有一份 → 不可能出现"四个函数里有一个忘了改"的事故。
判断标准:当你发现自己在写第二份"只换了类型名"的代码时,就该动手抽模板了。
二、作用二:比宏安全
在模板之前,C 语言是这么解决这个问题的:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
它能用,但埋着两个雷。
雷一:参数可能被求值两次
宏是纯文本替换。MAX(i++, 3) 展开后变成:
((i++) > (3) ? (i++) : (3))
如果 i++ > 3 成立,i++ 会被执行两次——i 凭空多加了一次。这类 bug 极难查,因为代码看起来完全正常。
模板不会:maxOf(i++, 3) 的参数是传值,i++ 只算一次。
雷二:没有类型检查
MAX("apple", 3) // 展开后是 (("apple") > (3) ? ...) —— 拿字符串和整数比
在 C++ 里这甚至可能编过(指针和整数的比较),跑起来结果荒唐。模板会直接报错——T 推不出唯一答案。
对照表
#define 宏 |
函数模板 | |
|---|---|---|
| 类型检查 | ❌ 没有 | ✅ 有(实例化时按真实类型检查) |
| 参数求值次数 | 可能多次(i++ 会被加两次) |
一次 |
| 调试 | ❌ 调试器看不到(它已经不存在了) | ✅ 正常函数,能打断点、能单步 |
| 作用域 | ❌ 从定义处到文件末,全局污染 | ✅ 受命名空间/作用域约束 |
| 能不能有"局部变量" | ❌ 只能写成一整行表达式 | ✅ 可以写多行函数体 |
| 报错信息 | 报的是"展开后那一坨" | 报的是模板名和参数(虽然啰嗦,但认得出) |
所以:宏能干的"类型无关的运算",今天都应该用模板或 constexpr 函数。(宏还剩两个正经用途:条件编译 #ifdef 和 字符串拼接 # / ##——那些是模板做不到的。)
回收旧账:这就是为什么 C++ 里那些"看起来像函数"的东西(
std::max、std::swap)都是模板而不是宏。
三、作用三:它是标准库的地基
你在前面十一章里其实一直在用模板的产物(只是没点破):
| 你写过的 | 它其实是 |
|---|---|
string s; |
basic_string<char, char_traits<char>, allocator<char>> |
cout << ... |
basic_ostream<char, char_traits<char>> |
vector<int> v;(还没学,但见过) |
template <class T, class Alloc = allocator<T>> class vector |
sort(a, a + n) |
template <class Iter> void sort(Iter first, Iter last) |
标准库的写法就是"模板 + 默认参数"(12.2.4 会讲后半截)。不会看模板,等于读不懂标准库的头文件。
四、实例01:使用数组作为模板参数
现在讲本节的重头戏。
问题:写一个"打印数组"的函数。难点不在打印,在"知道数组有几个元素"。
void showArray(int arr[], int n) { // 老办法:长度要"手动传"
for (int i = 0; i < n; i++) cout << arr[i] << " ";
}
每次调用都要自己记住长度,而且传错一个数就悄悄读越界(7.5.1 讲过"数组作参数会退化成指针"):
int a[] = {1, 2, 3, 4, 5};
showArray(a, 5); // 对
showArray(a, 4); // 不报错,只是少打一个 —— 没人提醒你
showArray(a, 50); // 不报错,只是读到越界 —— 灾难
模板的解法:把长度也变成模板参数。
template <typename T, int N>
void showArray(const T (&arr)[N]) { // ★ 参数是"数组的引用",长度写在类型里
for (int i = 0; i < N; i++) cout << arr[i] << " ";
}
关键在于 T (&arr)[N] 这个写法:
| 写法 | 结果 |
|---|---|
T arr[] |
退化成 T *——长度信息丢了,N 推不出来 ❌ |
T (&arr)[N] |
"引用一个长度为 N 的 T 数组"——长度留在类型里,编译器能推出 N ✅ |
int N 是什么? 它是非类型模板参数——参数不是类型,而是一个值。所以:
N是编译期常量(不是变量,不能改)- 每种长度都是"不同的类型实例":
showArray< int, 5 >和showArray< int, 3 >是两个独立的函数 - 调用时什么都不用写——
N从数组本身推出来
于是:
int a[] = {1, 2, 3, 4, 5}; // N = 5
double b[] = {1.5, 2.5, 3.0}; // N = 3
char c[] = {'A', 'B', 'C'}; // N = 3
showArray(a); // 不用写 5
showArray(b); // 不用写 3
showArray(c);
"忘了传长度"这件事,从此不可能发生——因为它不再需要人传。
五、动手 Demo:实例01 完整代码
#include <iostream>
using namespace std;
// ① 打印任意类型、任意长度的数组
template <typename T, int N>
void showArray(const T (&arr)[N]) {
cout << "共 " << N << " 个元素:";
for (int i = 0; i < N; i++) cout << arr[i] << " ";
cout << endl;
}
// ② 求任意类型、任意长度数组的和
// ★ T s = T(); —— 用 T 的"零值"初始化:int→0、double→0.0、string→""
template <typename T, int N>
T sumOf(const T (&arr)[N]) {
T s = T();
for (int i = 0; i < N; i++) s += arr[i];
return s;
}
// ③ 求数组里最大的元素
template <typename T, int N>
T maxOfArray(const T (&arr)[N]) {
T m = arr[0];
for (int i = 1; i < N; i++) if (arr[i] > m) m = arr[i];
return m;
}
int main() {
int a[] = { 7, 2, 9, 4, 5 }; // N = 5
double b[] = { 1.5, 2.5, 3.0 }; // N = 3
char c[] = { 'A', 'B', 'C' }; // N = 3
showArray(a);
showArray(b);
showArray(c);
cout << "整数和 = " << sumOf(a) << endl;
cout << "小数和 = " << sumOf(b) << endl;
cout << "最大整数 = " << maxOfArray(a) << endl;
cout << "最大字符 = " << maxOfArray(c) << endl;
return 0;
}
看到什么算成功:
共 5 个元素:7 2 9 4 5
共 3 个元素:1.5 2.5 3
共 3 个元素:A B C
整数和 = 27
小数和 = 7
最大整数 = 9
最大字符 = C
读结果:
| 现象 | 说明 |
|---|---|
| 三次调用的长度都对 | 长度不是人传的,是从数组类型里"读"出来的 |
sumOf(b) 得 7(不是 6.5?) |
1.5 + 2.5 + 3.0 = 7.0,打印时省略了 .0——数值是对的 |
maxOfArray(c) 得 C |
T 推成了 char——字符按 ASCII 码比较,'C' > 'A' |
T s = T(); |
这是"用 T 的零值初始化"的写法。模板里不能写 T s = 0;(string 没有 0 这个构造) |
30 秒实验:
- 给
char那个数组改成char c[] = "ABC";(用字符串字面量)。 - 重新编译运行,看第一行输出:"共 4 个元素"——不是 3!
- 因为字符串字面量的真实长度包含结尾的
'\0'(7.3 的老知识):"ABC"的类型是char[4]。 - 这个实验同时说明两件事:①
N确实是从类型里推出来的;② 字符串数组的长度永远比"看到的字符数"多 1。
六、常见误区
- "
#define和模板差不多,用哪个都行"——差别很大:宏没有类型检查、参数可能被求值两次、没法调试。只有"条件编译"和"字符串拼接"这两件事才非宏不可。 - "
template <int N>里的N是个变量"——不是。它是编译期常量(不能N++)。不同的N会生成不同的函数。 - "数组做参数时写
T arr[]就行"——不行,那会退化成指针(7.5.1),长度信息丢掉,N推不出来。必须写const T (&arr)[N](引用)。 - "
sumOf里写T s = 0;更简单"——不行。T换成string时,string s = 0;编译错。要写T s = T();——这句话的意思是"用T的默认值初始化"。 - "模板一定比手写快"——不一定,但通常至少一样快。实例化出来的代码和手写没有区别;真正的代价在编译期(每多一种类型/长度就多编一份代码)。别拿模板去套 20 种不同长度的小数组,那会让可执行文件白白变大。
- "有了模板,宏就彻底没用了"——还剩两件事:
#ifdef条件编译(按平台编不同代码)和#/##字符串化/拼接。别的场合,能用模板就用模板。
自测三问(点开对答案)
1. 为什么 `showArray(const T (&arr)[N])` 能自动知道数组长度,而 `showArray(const T arr[], int n)` 不能?——**因为前者是"数组的引用",长度是类型的一部分**(`int[5]` 里就写着 5),编译器能推出 `N`;**后者会退化成指针** `T *`,**长度信息在类型里已经不存在了**,只能靠人传。 2. 用模板替代 `#define MAX(a, b)` 主要好在哪两点?——**① 有类型检查**(`MAX("apple", 3)` 这种用法会当场报错);**② 参数只求值一次**(`MAX(i++, 3)` 里 `i++` 可能被执行两次,改成 `maxOf(i++, 3)` 就不会)。 3. 模板里为什么写 `T s = T();` 而不是 `T s = 0;`?——因为 **`T` 可能是 `string`**,而 `string s = 0;` 会编译错。**`T()` 的意思是"造一个 `T` 的默认值"**——`int` 得到 0、`double` 得到 0.0、`string` 得到空串。这是"模板里不能随便写具体字面量"的典型例子。- ☐ 12.1.3 重载函数模板(下一节)