
第十三章 模板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 还有个老写法classtemplate 和 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 该是哪个挑不动 │ │ │ │推导不接隐式转换 │ │ │ └──────────────────────────────────┘ │ │ │ │ 自己写明要哪套就行 │ │ │ │ maxOfdouble(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 maxOfdouble(3, 5.0) endl; return 0; }输出 5。显式指定这张牌推导卡住时候打出来第七章的旧账在这连本带利还清。还有条更深的脾气图纸只管结构不管意思。模板里一句 a bint 和 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。第六章的旧账提醒一句数组当参数传进去会退化成指针函数里头拿不到数组长度所以长度得单独传个 nsizeof 在函数里算不出原来的尺寸。┌──────────────────────────────────────────────┐ │ 引用和数组 │ ├──────────────────────────────────────────────┤ │ 两个变种一副好身手 │ │ │ │ 引用版 │ │ 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 StackT::push(const T x) { data[top] x; } template typename T int StackT::count 0; int main() { Stackint a, b; a.push(1); a.push(2); a.push(3); b.push(10); Stackdouble c; c.push(2.5); c.push(7.1); cout int 栈 Stackint::getCount() 个; cout double 栈 Stackdouble::getCount() 个 endl; cout a pop: a.pop() endl; cout c pop: c.pop() endl; return 0; }输出三行。int 栈 2 个double 栈 1 个a 弹出来 3c 弹出来 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; │ │ }; │ │ │ │ ┌──────────────────────────────────┐ │ │ │Stackint si; │ │ │ │ push 1 2 3 │ │ │ │ pop 得 3 2 1 │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────┐ │ │ │Stackdouble 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 StackT::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() { Nodeint *head nullptr; for (int i 1; i 3; i) { Nodeint *p new Nodeint; p-data i; p-next head; head p; } for (Nodeint *p head; p ! nullptr; p p-next) { cout p-data ; } cout endl; Nodestring *s new Nodestring; 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 有一个 countStack 另有一个Stack 里建几个对象就数几个和 double 栈互不串门。┌─────────────────────────────────────────────────────┐ │ 每类一份 │ ├─────────────────────────────────────────────────────┤ │ 静态成员一人一份 │ │ │ │ Stackint a, b; │ │ Stackdouble c; │ │ │ │ ---------------------------------------------- │ │ |类型 |现存对象数 | │ │ ---------------------------------------------- │ │ |Stackint |2 | │ │ |Stackdouble |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 Stackint { 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 │ │ ┌──────────────────────────────────┐ │ │ │类模板 │ │ │ │ StackT │ │ │ │ 尖括号里点名类型 │ │ │ └──────────────────────────────────┘ │ │ │ │ 和虚函数的分工 │ │ │ │ ---------------------------------------------- │ │ |模板 |编译期生成代码 | │ │ ---------------------------------------------- │ │ |虚函数 |跑起来查表挑版 | │ │ ---------------------------------------------- │ │ │ │ STL 是栋大楼 │ └─────────────────────────────────────────────────────┘模板和虚函数的分工也在这摆明。模板在编译期生成代码接住的是类型不同这件事虚函数在运行期查表接住的是行为不同这件事。一个在编译期使劲一个在运行期使劲合起来才是泛型的两条腿。最后一盆冷水模板只管结构不管语义。图纸上写 a bchar* 上头就是比地址模板不拦着也不提醒。类型该有的行为写图纸的人自己把守。附录 A 速查卡写法含义template图纸的头typename 可换 classT maxOf(T a, T b)函数模板Stack si实例化类模板maxOf(3, 5.0)显式点名类型template void Stack::push(...)类外定义头和类名都带 Ttemplate 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用时候漏尖括号写 Stackchar* 比大小结果没谱比的是地址换 string派生类调基类函数说不存在依赖基类的名字被遮挡写 this-f()