☰
第十三章 模板
2026/11/3 2:37:34 网站建设 项目流程

第十三章 模板

1. 函数模板

1.1 图纸只画一张

把六胞胎合成一张图纸,写法只多一行。

#include <iostream> #include <string> using namespace std; template <typename T> T maxOf(T a, T b) { return a > b ? a : b; } int main() { cout << maxOf(3, 5) << endl; cout << maxOf(2.5, 1.5) << endl; cout << maxOf(string("abc"), string("abd")) << endl; cout << maxOf('x', 'm') << endl; return 0; }

输出四行,5、2.5、abd、x。一套代码,四种类型全接住了,第七章那两版 maxOf 的账,到这里一笔还清。

┌────────────────────────────────────────┐ │ 把图纸交给编译器 │ ├────────────────────────────────────────┤ │ 图纸一张,施工多份 │ │ │ │ template <typename T> │ │ T maxOf(T a, T b) { │ │ return a > b ? a : b; │ │ } │ │ │ │ T 是个空名字,先占个座 │ │ 调用时候编译器拿实参填 │ │ │ │ ┌──────────────────────────────────┐ │ │ │图纸只此一份 │ │ │ │int double string 各生成一套 │ │ │ │改图纸,等于改所有版本 │ │ │ └──────────────────────────────────┘ │ └────────────────────────────────────────┘

template 加一对尖括号,这是图纸的头。typename T 声明一个占位符,T 叫什么无所谓,叫 U 叫 Value 都行,它在图纸里代表某种还没定下来的类型。函数定义和普通函数一模一样,参数、返回值、函数体,都是老朋友,只是 int 和 double 换成了 T。

typename 还有个老写法,class,template 和 template 完全等价,老代码里常见。新写的代码建议 typename,意思更直白,T 未必是个类。

图纸本身不干活,躺在文件里,编译器不给它生成任何代码。它等着被调用,等的那一刻才长成真正的函数,下一节说这个过程。

1.2 编译时候长出来

调用 maxOf(3, 5) 的那一刻,编译器开工。两个实参都是 int,推出 T 是 int,照图纸填一套 int 版的函数体,编译成机器码。紧接着 maxOf(2.5, 1.5) 又来了,再起一套 double 版。一个类型一套,各生成各的,互相不认识。

┌───────────────────────────────────────────┐ │ 实例化 │ ├───────────────────────────────────────────┤ │ 调用一次,长一套 │ │ │ │ maxOf(3, 5) 进来 │ │ | │ │ v │ │ ┌──────────────────────────────────┐ │ │ │实参两个都是 int │ │ │ │T 落定是 int │ │ │ │照图纸填一套 int │ │ │ └──────────────────────────────────┘ │ │ | │ │ v │ │ 输出 5 │ │ │ │ maxOf(2.5, 1.5) 另起 double 版 │ │ maxOf(string, string) 再起 string 版 │ │ │ │ 生成的活儿全在编译期 │ │ 跑起来就是普通函数,不花代价 │ └───────────────────────────────────────────┘

这套流程有个名字,叫实例化。图纸叫模板,长出来的叫实例,int 版就是模板函数的一个实例。实例化的活儿全在编译期干完,跑起来的 maxOf 就是个普通函数,一次跳转到位,不查表不绕路。所以模板和虚函数是两条路,虚函数跑起来查表挑版本,模板编译期把版本生成好,第十二章那个静态多态的帽子,模板也戴得上。

代价也说在明处,每种类型生成一套代码,一个 maxOf 调用十种类型,二进制里就躺着十份。函数小看不出来,模板函数写得肥,代码会胀。还有一条,实例化要看到图纸的全身,声明和定义不能分家,类模板那节的分离编译坑,根子在这。

1.3 推导的脾气

实参类型推 T,这套推导有自己的脾气,最大的那条是不接私货。第七章里 maxOf(3, 5.0) 能跑,int 那个 3 悄悄转成 double,挑 double 版。模板这边行不通,把 main 里写成 maxOf(3, 5.0),编译器直接报错,no matching function for call to maxOf(int, double)。 它推 T 是 int,后一个实参不答应,推 double,前一个不答应,两个都要,一个都不给。模板推导不搞隐式转换那套,实参和参数对不上就摊手。

┌──────────────────────────────────────────┐ │ 推导的脾气 │ ├──────────────────────────────────────────┤ │ 推导不接私货 │ │ │ │ maxOf(3, 5.0) 编译器两手一摊 │ │ │ │ ┌──────────────────────────────────┐ │ │ │实参一个 int 一个 double │ │ │ │T 该是哪个,挑不动 │ │ │ │推导不接隐式转换 │ │ │ └──────────────────────────────────┘ │ │ │ │ 自己写明要哪套就行 │ │ │ │ maxOf<double>(3, 5.0) 输出 5 │ └──────────────────────────────────────────┘

破解的办法是自己点名。尖括号里写明要哪套,T 定了,两个实参都往那个类型上靠。

#include <iostream> using namespace std; template <typename T> T maxOf(T a, T b) { return a > b ? a : b; } int main() { cout << maxOf<double>(3, 5.0) << endl; return 0; }

输出 5。显式指定这张牌,推导卡住时候打出来,第七章的旧账在这连本带利还清。

还有条更深的脾气,图纸只管结构,不管意思。模板里一句 a > b,int 和 double 上头是比大小,string 上头是比字典序,到了 char* 上头,比的是地址。第六章说过,指针里头存的是地址,两个 char* 比大小,比的是谁家的门牌号小,字符串里的内容压根没上场。

#include <iostream> using namespace std; template <typename T> T maxOf(T a, T b) { return a > b ? a : b; } int main() { const char *a = "abc"; const char *b = "abd"; cout << maxOf(a, b) << endl; return 0; }

这段输出 abd,看着是对的。别信它,两个字符串字面量安排在哪,是编译器的事,这次恰好 abd 排在后面。换个编译器换个结果,线上排名说翻就翻。开篇那几颗雷,模板一颗都没拆,该用 string 就用 string,指针比大小这条弯路,图纸不拦着人走,走路的人自己长个心眼。

┌────────────────────────────────────┐ │ 模板的盲区 │ ├────────────────────────────────────┤ │ 图纸通吃,不管语义 │ │ │ │ 图纸一句 a > b │ │ 什么意思,图纸可不管 │ │ │ │ ┌──────────────────────────────┐ │ │ │int double string │ │ │ │ > 安分比大小 │ │ │ └──────────────────────────────┘ │ │ | │ │ v │ │ ┌──────────────────────────────┐ │ │ │char* │ │ │ │ > 比的是地址 │ │ │ │ 内容没掺和 │ │ │ └──────────────────────────────┘ │ │ │ │ 指针比大小,换 string 来得稳 │ └────────────────────────────────────┘

1.4 引用和数组

参数不止能传值,模板对老朋友一律接纳。

引用版,参数和返回值都挂引用,大的那个本身被交出来,接着改。

#include <iostream> using namespace std; template <typename T> T &maxOf(T &a, T &b) { return a > b ? a : b; } int main() { int x = 3, y = 5; cout << maxOf(x, y) << endl; maxOf(x, y) = 100; cout << "x = " << x << ", y = " << y << endl; return 0; }

输出两行,5 和 x = 3, y = 100。maxOf(x, y) = 100 这句,等号左边是函数调用,返回的是 y 的引用,100 实实在在写进了 y。传值版做不到这件事,返回的是副本,改个热闹。

数组版,参数收一个数组加个长度,遍历找最大。

#include <iostream> using namespace std; template <typename T> T maxOf(T arr[], int n) { T m = arr[0]; for (int i = 1; i < n; i++) { if (arr[i] > m) { m = arr[i]; } } return m; } int main() { int scores[5] = {78, 92, 55, 100, 88}; cout << maxOf(scores, 5) << endl; return 0; }

输出 100,第五章那组成绩数据的头名。T arr[] 里的 T 照样推导,实参是 int 数组,T 就是 int。第六章的旧账提醒一句,数组当参数传进去会退化成指针,函数里头拿不到数组长度,所以长度得单独传个 n,sizeof 在函数里算不出原来的尺寸。

┌──────────────────────────────────────────────┐ │ 引用和数组 │ ├──────────────────────────────────────────────┤ │ 两个变种,一副好身手 │ │ │ │ 引用版 │ │ template <typename T> │ │ T &maxOf(T &a, T &b) │ │ │ │ maxOf(x, y) = 100; │ │ 大的那个真的改成 100 │ │ │ │ 数组版 │ │ template <typename T> │ │ T maxOf(T arr[], int n) │ │ │ │ int scores[5] = {78, 92, 55, 100, 88}; │ │ maxOf(scores, 5) 得 100 │ └──────────────────────────────────────────────┘

函数模板到这就齐了,一张图纸,推导、显式指定、引用、数组,四个招式。接下来把图纸画到类上。

2. 类模板

2.1 栈的图纸

函数模板把函数画成图纸,类模板把整个类画成图纸。这回请个新角色,栈,后进先出,数据躺在一个数组里。成员数组装什么还没定,写成 T,用的时候再点名。

#include <iostream> using namespace std; template <typename T> class Stack { public: Stack() : top(0) { count++; } ~Stack() { count--; } void push(const T &x); T pop() { return data[--top]; } bool empty() const { return top == 0; } int size() const { return top; } static int getCount() { return count; } private: T data[8]; int top; static int count; }; template <typename T> void Stack<T>::push(const T &x) { data[top++] = x; } template <typename T> int Stack<T>::count = 0; int main() { Stack<int> a, b; a.push(1); a.push(2); a.push(3); b.push(10); Stack<double> c; c.push(2.5); c.push(7.1); cout << "int 栈 " << Stack<int>::getCount() << " 个,"; cout << "double 栈 " << Stack<double>::getCount() << " 个" << endl; cout << "a pop: " << a.pop() << endl; cout << "c pop: " << c.pop() << endl; return 0; }

输出三行。int 栈 2 个,double 栈 1 个,a 弹出来 3,c 弹出来 7.1。Stack 和 Stack 是同一个模板的两个实例,两个完全独立的类,普通成员各对象一份,静态成员各类型一份,2.4 细说。

┌────────────────────────────────────────┐ │ 类模板 │ ├────────────────────────────────────────┤ │ 栈的图纸 │ │ │ │ template <typename T> │ │ class Stack { │ │ public: │ │ void push(const T &x); │ │ T pop(); │ │ static int getCount(); │ │ private: │ │ T data[8]; │ │ int top; │ │ }; │ │ │ │ ┌──────────────────────────────────┐ │ │ │Stack<int> si; │ │ │ │ push 1 2 3 │ │ │ │ pop 得 3 2 1 │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────┐ │ │ │Stack<double> sd; │ │ │ │ push 2.5 7.1 │ │ │ │ pop 得 7.1 │ │ │ └──────────────────────────────────┘ │ │ │ │ 类型点名才修得出来 │ └────────────────────────────────────────┘

类名后头的尖括号不能省。Stack s 这么写,编译器报 missing template arguments,它不知道给 s 搭一个装什么的栈。这段代码里 push 和 count 都定义在类外头,规矩和第十章一个样,只是多了几条模板的讲究,下一节说。

2.2 类外定义的规矩

回头看那段代码,push 和 count 都挪到了类外。类外定义模板成员,两条语法规矩加一条工程铁律。

┌────────────────────────────────────────┐ │ 挪到类外头 │ ├────────────────────────────────────────┤ │ 类外定义,图纸再抄一遍 │ │ │ │ 类里只留声明 │ │ │ │ template <typename T> │ │ void Stack<T>::push(const T &x) { │ │ data[top++] = x; │ │ } │ │ │ │ ┌──────────────────────────────────┐ │ │ │两条语法规矩 │ │ │ │template 头再抄一遍 │ │ │ │类名后头带 <T> │ │ │ │缺哪条都报错 │ │ │ │声明和定义,同一个文件 │ │ │ └──────────────────────────────────┘ │ └────────────────────────────────────────┘

第一条,template 头原样重复。类里怎么写,类外再抄一遍 template ,放在返回类型前头。漏了它,编译器在类外找不见 T,报 T 没声明过。

第二条,类名后头带尖括号。Stack::push,不是 Stack::push,实例还没定,编译器不知道你在说哪个 Stack 的 push。

工程铁律是另一回事,模板的声明和定义必须待在同一个文件,一般是头文件。普通类可以声明在 .h 实现在 .cpp,模板不行。实例化发生在编译期,调用点只看得见 .h 里的声明,看不见 .cpp 里的定义,链接时候报 undefined reference to Stack::push,实现明明写了,就是链不上。所以模板库清一色头文件式交付,STL 的源码全是这个套路,一个头文件包打天下。

2.3 模板链表

第九章那个链表,Node 的 data 是 int。改成图纸,data 的类型交给 T,一套代码两种节点。

#include <iostream> #include <string> using namespace std; template <typename T> struct Node { T data; Node *next; }; int main() { Node<int> *head = nullptr; for (int i = 1; i <= 3; i++) { Node<int> *p = new Node<int>; p->data = i; p->next = head; head = p; } for (Node<int> *p = head; p != nullptr; p = p->next) { cout << p->data << " "; } cout << endl; Node<string> *s = new Node<string>; s->data = "张三"; s->next = nullptr; cout << s->data << endl; return 0; }

输出两行,3 2 1 和 张三。头插的老规矩,和第九章一字不差,1 2 3 进去,3 2 1 出来。Node 接分数,Node 接姓名,new Node 和 new Node 长出来是两个模样,遍历的代码却是一套。

┌────────────────────────────────┐ │ 链表换图纸 │ ├────────────────────────────────┤ │ 第九章链表,换成图纸 │ │ │ │ template <typename T> │ │ struct Node { │ │ T data; │ │ Node *next; │ │ }; │ │ │ │ int 节点装分数 │ │ string 节点装姓名 │ │ 一套遍历代码,两种节点通吃 │ │ │ │ 头插 1 2 3,得 3 2 1 │ │ 老身板没变,图纸换了新 │ └────────────────────────────────┘

类里写自己的指针,Node *next 不用写成 Node *next。类名在自家作用域里认得自己,尖括号省得掉。出了这个类可不行,main 里写 Node *p,一个字母都不能省。

2.4 静态成员和派生

静态成员碰上模板,第十章那套规矩照旧,只是每种类型各一份。count 是模板的静态成员,Stack 有一个 count,Stack 另有一个,Stack 里建几个对象,就数几个,和 double 栈互不串门。

┌─────────────────────────────────────────────────────┐ │ 每类一份 │ ├─────────────────────────────────────────────────────┤ │ 静态成员,一人一份 │ │ │ │ Stack<int> a, b; │ │ Stack<double> c; │ │ │ │ +------------------------+----------------------+ │ │ |类型 |现存对象数 | │ │ +------------------------+----------------------+ │ │ |Stack<int> |2 | │ │ |Stack<double> |1 | │ │ +------------------------+----------------------+ │ │ │ │ 同一张图纸,不同实例 │ │ 静态成员各归各,不串门 │ └─────────────────────────────────────────────────────┘

上一节的输出就是证据,int 栈现存 2 个,double 栈现存 1 个。静态成员在类外定义,也要带 template 头,int Stack::count = 0 那一行,漏了头照样链不上。

模板类当基类,两种路子。普通类继承模板的某个实例,IntStack : public Stack,实例化在继承名单里一次做完,之后 IntStack 就是个普通类,第十章第十一章的规矩全套适用。

#include <iostream> using namespace std; template <typename T> class Stack { public: Stack() : top(0) {} void push(const T &x) { data[top++] = x; } T pop() { return data[--top]; } bool empty() const { return top == 0; } private: T data[8]; int top; }; class IntStack : public Stack<int> { public: int sum() { int s = 0; while (!empty()) { s += pop(); } return s; } }; int main() { IntStack st; st.push(1); st.push(2); st.push(3); cout << st.sum() << endl; return 0; }

输出 6,三个数弹出来加起来。另一种路子是模板类继承模板基类,Derived 接 Base,基类也带参数。那条路上的坑多,最出名的一个,派生类成员函数里调基类函数,编译器可能说不存在,得写 this->f() 或者 Base::f() 才认。模板的两段式查找,本书不展开,碰到报错时候知道往这找就行。

到此模板的两条腿都落地了。函数模板管函数,类模板管类,推导、显式指定、类外定义、静态成员、继承,该见的场面都见了一面。

3. 本章小结

本章讲了两件事,都可以一句话说完。

函数模板,一张图纸多份施工。template 加上去,T 先占座,调用那一刻,编译器按实参生成,编译期干活,跑起来不花代价。推导有脾气,不接隐式转换,卡住了显式点名。

类模板,整类画成图纸。Stack 和 Stack 是两个独立的类,静态成员一人一份,类外定义要重复 template 头,声明和定义必须同文件。

┌─────────────────────────────────────────────────────┐ │ 模板收口 │ ├─────────────────────────────────────────────────────┤ │ 模板两件套 │ │ │ │ ┌──────────────────────────────────┐ │ │ │函数模板 │ │ │ │ T maxOf(T a, T b) │ │ │ │ 看实参施工 │ │ │ └──────────────────────────────────┘ │ │ | │ │ v │ │ ┌──────────────────────────────────┐ │ │ │类模板 │ │ │ │ Stack<T> │ │ │ │ 尖括号里点名类型 │ │ │ └──────────────────────────────────┘ │ │ │ │ 和虚函数的分工 │ │ │ │ +------------------------+----------------------+ │ │ |模板 |编译期生成代码 | │ │ +------------------------+----------------------+ │ │ |虚函数 |跑起来查表挑版 | │ │ +------------------------+----------------------+ │ │ │ │ STL 是栋大楼 │ └─────────────────────────────────────────────────────┘

模板和虚函数的分工也在这摆明。模板在编译期生成代码,接住的是类型不同这件事,虚函数在运行期查表,接住的是行为不同这件事。一个在编译期使劲,一个在运行期使劲,合起来才是泛型的两条腿。

最后一盆冷水,模板只管结构不管语义。图纸上写 a > b,char* 上头就是比地址,模板不拦着,也不提醒。类型该有的行为,写图纸的人自己把守。

附录 A 速查卡

写法含义
template图纸的头,typename 可换 class
T maxOf(T a, T b)函数模板
Stack si实例化类模板
maxOf(3, 5.0)显式点名类型
template void Stack::push(...)类外定义,头和类名都带 T
template int Stack::count = 0静态成员的类外定义
类内写 Node *next自家名字省得掉尖括号
undefined reference定义没和声明待在同一个文件

附录 B 排错表

常见现象病根药方
maxOf(3, 5.0) 报 no matching function推导不接隐式转换显式写 maxOf(3, 5.0)
undefined reference to Stack::push定义放进了 .cpp声明和定义挪回同一文件
报 T 没声明过类外定义漏 template 头返回类型前补上
报 missing template arguments用时候漏尖括号写 Stack
char* 比大小结果没谱比的是地址换 string
派生类调基类函数说不存在依赖基类的名字被遮挡写 this->f()

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询