这一篇在干嘛?
全书代码都用 C++ 模板写成,而这带来一个工程现实:模板的实现通常必须整个放在头文件里。这个附录解释为什么、有哪些变通,以及”分离编译”在实际项目里的正确姿势。篇幅短,却是把书里的代码编译通过绕不开的一课。
为什么模板难分离
普通类的编译流程:.cpp 编译成目标文件,调用其他模块的函数只需在头文件里看到声明,链接器负责把名字对上——实现可以藏在任何 .cpp 里。
模板不行。vector<int> 的代码在被使用的那个编译单元里现场生成(实例化):编译器必须看到模板的完整定义(包括函数体),才能替你”盖章复制”出 vector<int> 版本的机器码。只在头文件里放声明,编译器两手空空,链接时找不到符号,报出一屏”undefined reference”。
白话:普通函数是”印好的书,各处引用书名”;模板是”印刷模板,谁用谁印刷”——要用就必须把雕版(定义)给人家看。这就是书中所有数据结构代码(
Vector.h、BinarySearchTree.h……)都把实现写在头文件里的原因。
C++11 的现实:没有 export
C++98 曾设想 export template 关键字让模板像普通函数一样分离,但编译器实现代价巨大,主流编译器几乎没人支持,C++11 中被正式移除。所以标准答案只有一个:
- 默认做法:模板类 = 一个
.h,声明 + 实现全在里面。 - 头文件用
#ifndef/#define/#endif包裹防重复包含。
两个变通方案
方案一:显式实例化。实现放 .cpp,在该文件末尾显式生成需要的类型:
// Stack.cpp —— 实现不公开
template <typename Object>
void Stack<Object>::push(const Object &x) { /* ... */ }
template class Stack<int>; // 只实例化这几个类型
template class Stack<double>;白话:在实现文件里亲手”印刷”用得上的几个版本。优点是编译快、实现保密(商业库常用);缺点是用户用列表外的类型就直接链接错误——灵活性换工程性。
方案二:#include .cpp 变体。把实现放 .cpp,头文件里 #include "Stack.cpp"。能跑,但破坏 .cpp 的语义约定,容易让构建系统困惑,不推荐。
实践清单
- 模板库 = 单头文件(书中
SeparateCompilation一节的标准答案)。 - 大量模板会拖慢编译:公用库可组合”显式实例化 + 常用类型”。
- 模板编译错误信息可读性差:从第一条错误看起(后面的多是连锁反应)。
常见坑
把模板成员函数的实现写到
.cpp里,然后在自己的程序里vector<MyType>——链接错误undefined reference。这是 C++ 新手最常见的困惑之一,原因正是本篇所述”实例化需要看到定义”。
通关标准
能解释:① 为什么模板实现必须进头文件(实例化机制);② 显式实例化的用途与代价;③ export 关键字为什么被移除。
template class Stack<int>;这行代码做了什么?在当前编译单元强制实例化 Stack
的全部成员并导出符号,使其他看不到模板定义的编译单元也能链接使用——这是”实现保密”式模板库的基础。
为什么普通类不需要这些讲究?
普通类代码是固定的,编译一次到处链接;编译单元之间靠声明 + 链接器即可。模板的机器码取决于使用处的类型实参,无法预先编译成通用符号。
书中代码全部把实现写在 .h 里,会不会违背"接口与实现分离"(第 1.4.3 节)?
逻辑上仍分离(public 接口在前、private 在后,配合文档),物理上同文件是模板实例化机制的强制要求——工程上接受”物理耦合”换取”逻辑清晰”。