想象你是一家连锁餐厅的行政总厨。每个分店的厨房设备完全不同——有的用燃气灶,有的用电磁炉,有的还配了烤箱。你手里有一份标准菜谱(CMakeLists.txt),但你不可能亲自飞去每家店炒菜。于是你定下规矩:把菜谱交给当地厨师长(CMake),由他根据厨房条件写出任务卡(Makefile 或 Ninja build),再交给后厨(Make/Ninja)照单执行。灶台的火力大小、锅具型号,厨师长都替你考虑好了。这样一来,同一份菜谱,无论开在哪个城市,端上桌的菜永远一个味道。这就是 CMake 的灵魂——你只需写一次构建规则,就能在任何平台、任何编译器下轻松编译出 C++ 程序。它不亲自“做菜”,却让每个后厨都做出同样的佳肴。
学会 CMake,是 C++ 开发者从“只会编译”走向“专业构建”的分水岭:它能帮你管理复杂依赖、屏蔽平台差异,让跨平台部署和团队协作事半功倍。
可视化演示
动态演示CMake的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
CMake:CMak CMake构建系统定位与CMa target依赖管理与库链接 核心机制可视化 CMake整体架构与关键组件
CMake 基础入门
拿到一份新菜谱,你要做的第一件事是什么?先看开头的“用料清单”和“厨房要求”——需要几口锅、什么火候、准备哪些食材。CMakeLists.txt 就是 C++ 项目的“菜谱封面”,它用三行字把整道菜的基本信息交代清楚:本菜谱需要哪个版本的厨师长(cmake_minimum_required)、这道菜叫什么名字(project)、最后要用哪些食材做出一道什么菜(add_executable)。这三行是任何一个 CMake 项目的起点,就像做菜前必须先开火预热。
在实际操作中,你并不会直接读 CMakeLists.txt 去编译,而是把它交给“厨师长”——cmake 命令。厨师长读完整本菜谱后,会根据当前厨房的设备(操作系统、编译器)生成一份具体的任务卡。任务卡上写着“第一步做什么、第二步做什么”,但它本身还不是菜。这份任务卡在 Linux 上通常是 Makefile,在 Windows 上可能是 Visual Studio 工程,在 macOS 上可以是 Xcode 工程。这就是 CMake 的“翻译”本质:它把你写的菜谱翻译成当前厨房听得懂的派工单。
生成任务卡后,你只需一条命令就能让后厨开火:cmake --build build。聪明的厨师长还会要求把“菜谱”和“操作台”分开——源码放在一个目录,构建产生的任务卡和半成品放在另一个目录(如 build/)。这样哪怕任务卡被删,菜谱依然完好,随时可以重新生成一套。
厨师的智慧在于:菜谱永远只写一份,但每个厨房都能用它做出同样的菜。下次你会看到,这份三行开胃小菜,撑起了无数大型 C++ 项目的整桌宴席——而后续的依赖、跨平台、打包,都是在它上面不断添柴加火。
- 规定最低版本告诉 CMake 至少要用 3.16 版本才能按这份菜谱操作,版本太老会直接拒绝执行。
- 声明工程名称给整道菜起个名字,后续生成的所有构建文件和变量都会挂在这个名字下。
- 创建可执行目标把
main.cpp这个“主食材”加工成一道名为app的成品菜,即最终的 C++ 可执行程序。
cmake_minimum_required(VERSION 3.16)project(MyApp)add_executable(app main.cpp)
目标依赖管理
一道完整的宴席,不可能只靠一口锅炒一个菜。红烧肉要先炖好配菜,鱼香肉丝的调料要提前备好。C++ 项目也一样:程序往往由多个“菜”组成——有些是可执行程序,有些是函数库,它们之间有先有后、互相搭配。在 CMake 里,每个可执行文件或库都叫一个“目标”(target),add_executable 创建可执行目标,add_library 创建库目标。目标就是菜谱里的每一道独立菜品。
菜品之间常有“配菜关系”:主菜 app 需要用到 utils 这个配菜库。如果你不告诉厨师长,他可能先炒主菜,结果发现配菜还没下锅,整道菜全废。所以你要在菜谱上明确写下“依赖声明”:target_link_libraries 就是这句附注,它告诉 CMake,“app 这道菜要等 utils 做完才能开炒”。
这里的 prec 符号非常关键:CMake 会从所有 target_link_libraries 声明中画出一张“依赖图”,然后自动算出做菜顺序——先把所有被依赖的库编译好,再编译依赖它们的程序。当项目有几十个目标、几百个文件时,这张图就是后厨的总调度表,而你只需要在菜谱上写明“谁配谁”,剩下的逻辑 CMake 全包了。
- 创建库目标把
utils.cpp编译成一个名为utils的静态或动态库,就像先把配菜切好装盘备用。 - 建立链接关系声明 app 在使用 utils 库中的函数,链接器会在最后把两者拼成一个完整程序。
- 理解 PRIVATE 作用域PRIVATE 表示“这个依赖只对 app 自己可见,不传给 app 的消费者”,避免依赖关系像病毒一样蔓延,保持了菜谱的干净。
跨平台编译
同一道麻婆豆腐,在成都的川菜馆和纽约的融合餐厅,做法得微调——成都用郫县豆瓣,纽约可能买不到,得用辣酱替代。C++ 项目也一样:Windows 上的编译器是 MSVC,Linux 上是 GCC,macOS 上又是 Clang。它们对编译选项的写法略有不同。CMake 的跨平台能力,正是让厨师长学会“看厨房下菜碟”:判断当前是哪个平台,再选择对应的做法。
判断平台靠的是 CMake 内置的“厨房标识变量”:WIN32 在 Windows 上为真,UNIX 在所有类 Unix 系统上为真,APPLE 在苹果系统上为真。你可以在 CMakeLists.txt 里用 if()/else() 写条件分支,就像菜谱里写“若有烤箱则烤,若只有微波炉则蒸”。
有了这套“看厨房下菜碟”的功夫,你还能进一步用 target_compile_definitions 给源码传递平台宏,让 C++ 代码里也能通过 #ifdef _WIN32 做条件编译。于是一份 CMakeLists.txt 走天下:换平台时只需重新运行 cmake,它会自动识别厨房、调整做法。你写的菜谱从此真正做到了“一份菜谱,千家后厨,同一道菜”。
- Windows 分支
WIN32变量在 Windows 平台为真,此时可针对 MSVC 编译器做特殊配置。 - Linux 分支
UNIX为真且APPLE不为真时,当前是 Linux,适合 GCC 的编译选项。 - macOS 分支
APPLE为真时命中 macOS,Clang 编译器下的写法又与 Linux 略有不同。
构建后端集成
厨师长写好任务卡后,后厨谁来执行?这就是构建后端的选择。CMake 是灵活的:默认生成的 Makefile 由 make 执行,像传统大锅饭,稳扎稳打;而 Ninja 则以极快的速度著称,像流水线快餐,特别适合大型项目。你可以在 cmake 时用 -G Ninja 指定用 Ninja 当后厨,也可以用 -G "Unix Makefiles" 用 Make。
不管选哪个后厨,它们都有个共同的智慧:不重复劳动。每次真正开火前,后厨会检查每道菜的“半成品”是否够新鲜——如果源文件最后修改时间比对应的目标文件(.o)晚,说明食材变了,得重新炒;如果目标文件更新,说明这道菜没动过,直接跳过。这就是增量构建,它让每顿饭后厨都只做必要的活。
增量构建配合并行编译,效果更佳——cmake --build build -j8 一句命令,8 个灶台同时开火,将编译时间大幅压缩。这也是为什么大型项目普遍选择 CMake + Ninja 的组合:Ninja 在增量构建上比 Make 更激进、更快。选对后厨,能让你的构建体验从“每天等十分钟”变成“三秒搞掂”。
\displaystyleB = \left\{\, f \in \mathcal{F}_{\text{src}} \;\middle|\; \tau_f > \tau_{\text{obj}(f)} \,\right\}- 源文件集合$\mathcal{F}_{\text{src}}$ 是项目所有源文件的全集,后厨从中筛选哪些要动工。
- 保鲜期比较$\tau_f > \tau_{\text{obj}(f)}$ 的意思是源文件比目标文件“更新鲜”,说明改了代码还没编译。
- 需要重编的集合用集合描述语言把所有满足条件的文件收集进 $B$,后厨只需处理 $B$ 中的文件。
find_package机制
做菜时,你不可能连郫县豆瓣都自己酿,大多用现成的调料包。C++ 项目也一样:OpenCV、Boost、gRPC 这些第三方库,没人会去读它们的源码再自己编译,而是直接用别人打包好的“调料包”。CMake 里的 find_package(OpenCV 4 REQUIRED) 就是去仓库里找这个调料包的命令。
找到调料包后,它会在 CMake 里生成几个“配方变量”:OpenCV_FOUND 表示找到了没有,OpenCV_INCLUDE_DIRS 告诉编译器头文件在哪,OpenCV_LIBS 告诉链接器库文件在哪。你需要做的,就是把这些变量接到自己的目标上。查找的过程遵循一套明确的顺序:先翻冰箱(CMAKE_PREFIX_PATH 指定的缓存路径),再看橱柜(包自带的 Config 文件),最后去小区超市(系统的 Find 模块)。
注意 REQUIRED 这个关键词很有用:加了它,如果找不到包,CMake 会直接报错并停止,相当于厨师长发现缺了关键调料,宁可不动火也不做砸菜。利用 find_package,你可以在 CMakeLists 里像搭积木一样把一个个第三方库拼进来,而不用关心它们各自复杂的安装位置。
- 发起查找
find_package(OpenCV 4)要求查找名为 OpenCV、主版本号为 4 的库包。 - 按路径搜索CMake 依次检查
CMAKE_PREFIX_PATH、包自身的*Config.cmake文件、以及/usr等系统目录。 - 接收结果变量查找结束后,
OpenCV_FOUND为 TRUE 时才能安全地使用另外两个变量。
构建类型选项
同样一道糖醋排骨,平时自家吃和过年宴客,做法有区别——自家吃可以随意,宴客就得讲究摆盘。C++ 编译也分场合:开发调试时需要保留详细的调试信息,方便定位问题;发布上线时则要开启优化,让程序跑得飞快。CMake 用“构建类型”来区分这两种做法:Debug 和 Release。
每次运行时用 cmake -DCMAKE_BUILD_TYPE=Debug 或 =Release 来选择做法。切到 Debug,编译器加上 -g -O0:保留完整调试符号、关闭优化,就像把菜切得清清楚楚、不急着起大火。切到 Release,则换成 -O3 -DNDEBUG:全力优化并关闭断言,只为宴客时菜品最精致、上菜最快。
除了这两种经典做法,CMake 还提供 RelWithDebInfo(带调试信息的优化版,相当于保留摆盘还提前上菜)和 MinSizeRel(体积最小化,适合磁盘紧张的嵌入式设备)。更妙的是,你还能用 option(USE_GPU "启用GPU加速" ON) 定义自己的“口味开关”,配合缓存变量在命令行随时切换——就像顾客点菜时备注“少辣”“免葱”一样灵活。构建类型和选项组合起来,让一道菜能适应从深夜加班调试到大型发布会的一切场景。
-g 并关闭优化 -O0;Release 模式开启最高优化 -O3 并定义 NDEBUG 关闭断言。- Debug 映射
-g让可执行文件携带源码行号信息,-O0保证代码执行顺序与源码一致,方便调试器断点。 - Release 映射
-O3开启激进优化让程序速度最快,-DNDEBUG把代码里的assert断言全部移除,减少运行开销。
AI 应用场景
场景一:AI 推理服务的跨平台部署
阶段:模型部署与工程化
解决的问题:AI 模型常在 Linux 服务器上训练,但最终要部署到 Windows 客户端、macOS 桌面甚至 ARM 嵌入式设备上。传统方式是在每个平台上手写一套编译脚本,维护成本极高,且依赖库版本稍有差异就编译失败。用 CMake 统一管理后,一套 CMakeLists.txt 搞定所有平台的编译配置,配合 find_package(ONNX Runtime) 自动定位推理引擎的依赖。
具体案例:某智慧安防公司要交付一个 c++ 推理服务,目标环境是客户现场的 Windows 工控机和自家云端的 Linux 服务器。他们用 CMake 管理项目,构建类型分别配置 Debug(现场调试)和 Release(云端生产),跨平台编译时通过 WIN32、UNIX 分支自动选择系统 API,最终一次编写、两处部署,交付周期从两周缩短到两天。
场景二:深度学习算子库的依赖管理
阶段:自定义算子开发与集成
解决的问题:训练框架(如 PyTorch)提供了 C++ 前端扩展接口,但开发者经常被第三方库依赖搞到头大——CUDA、OpenCV、gRPC 每个库的安装位置不同,版本也互相制约。CMake 的 find_package(Torch REQUIRED) 和 find_package(OpenCV REQUIRED) 把这些“调料包”的查找路径和链接方式统一接管。
具体案例:某自动驾驶团队开发自定义的 BEV 感知算子,需要同时调用 PyTorch C++ 前端和 CUDA 库。他们在 CMakeLists.txt 中用 find_package(Torch REQUIRED) 获取 torch_include_dirs 和 torch_libraries,再用 target_link_libraries(bev_op PRIVATE ${torch_libraries}) 精确绑定依赖,构建时自动从 CMAKE_PREFIX_PATH 找到对应版本的 PyTorch,彻底告别手写 -I 和 -L 参数的年代。
场景三:边缘 AI 设备的交叉编译
阶段:边缘端模型部署
解决的问题:树莓派、Jetson 等边缘设备算力有限,直接在设备上编译大型 AI 框架耗时数小时甚至失败。CMake 支持通过“工具链文件”(toolchain file)在 x86 服务器上交叉编译出 ARM 架构的程序,同一份 CMakeLists.txt 通过判断 CMAKE_SYSTEM_NAME 决定是本地编译还是交叉编译。
具体案例:某机器人公司要在 Jetson Orin 上部署视觉导航模型,开发者在笔记本上运行 cmake -DCMAKE_TOOLCHAIN_FILE=aarch64-linux-gnu.toolchain.cmake .. 生成 ARM 版本的构建文件,再用 Ninja 并行编译,十几分钟就产出可执行文件,直接拷贝到机器人上运行,迭代速度提升了一个数量级。
Python 演示
"""cmake_sim.py: 模拟CMake的依赖规划与增量编译判定"""import os class FileNode: def __init__(self, name, mtime): self.name = name self.mtime = mtime class Target: def __init__(self, name, sources, obj_mtime): self.name = name self.sources = sources # 源文件节点列表 self.obj_mtime = obj_mtime # 目标文件时间戳 self.deps = [] # 依赖的其他目标名 self._count = 0 # 占位属性, 避免与方法同名 def add_dependency(self, dep): self.deps.append(dep) def need_compile(self): for src in self.sources: if src.mtime > self.obj_mtime: return True return False def topo_visit(target, targets, visited, order): """深度优先遍历, 先访问依赖, 再访问自身""" if target in visited: return visited.add(target) for dep_name in target.deps: topo_visit(targets[dep_name], targets, visited, order) order.append(target) def main(): files = { 'main.cpp': FileNode('main.cpp', 100), 'utils.cpp': FileNode('utils.cpp', 98), 'data.cpp': FileNode('data.cpp', 120), } targets = { 'main': Target('main', [files['main.cpp']], 101), 'utils': Target('utils', [files['utils.cpp']], 100), 'data': Target('data', [files['data.cpp']], 115), } targets['main'].add_dependency('utils') targets['main'].add_dependency('data') order = [] visited = set() topo_visit(targets['main'], targets, visited, order) to_build = [t for t in order if t.need_compile()] print('构建规划(先依赖后自身):', ' -> '.join(t.name for t in order)) print('增量判定:') for t in order: status = '需要重编译' if t in to_build else '跳过' print(' [%s] %s' % (t.name, status)) print('结论: %d 个目标中仅 %d 个需要重新编译' % (len(order), len(to_build))) if __name__ == '__main__': main()
class FileNode:表示一个虚拟源文件,持有名字和修改时间两个属性,对应 CMake 判断时间戳所需的全部信息。def need_compile(self):核心增量判断:只要任一源文件修改时间晚于目标文件时间戳,就认为需要重编译。if src.mtime > self.obj_mtime:这一行就是 CMake 内部判断“要不要重新炒这道菜”的关键逻辑。def topo_visit(target, targets, visited, order):深度优先的拓扑排序,先递归访问所有依赖,再把自己加入构建顺序。targets['main'].add_dependency('utils')等价于 CMake 里的target_link_libraries(main PRIVATE utils),声明构建先后关系。to_build = [t for t in order if t.need_compile()]从完整构建计划中过滤出真正需要动工的目标,其余全部跳过。topo_visit(targets['main'], targets, visited, order)从根目标出发遍历整张依赖图,CMake 在配置阶段就是这么干的。
常见误区
误区我刚学 CMake 时,以为运行 `cmake .` 就会直接生成可执行文件,就像 `g++ main.cpp` 那样一步到位。结果只在目录里看到一堆 Makefile 文件,根本没有程序,还以为自己操作错了。
正解CMake 的角色是“厨师长”而不是“炒菜师傅”。它只负责读菜谱、生成任务卡,真正的编译执行要交给任务卡上的后厨——make 或 ninja。运行 cmake --build build 才是触发后厨开火的正确方式。理解这个分工,你才能看懂 CMake 的两阶段流程为什么存在。
为何常见:因为大多数构建工具都是“一条命令出结果”,而 CMake 刻意把“配置”和“构建”分成两步,初学者容易把第一阶段的输出误当成最终产物。
误区我明明在源码里 `#include "utils.h"` 了,编译也通过了,怎么链接时说 `undefined reference to utils_foo()`?是不是编译器坏了?
正解编译通过只代表“菜谱读懂了”,链接错误说明“配菜没端上桌”。必须在 CMakeLists.txt 里用 target_link_libraries(app PRIVATE utils) 把库目标和主程序连起来。这就像炖肉时调料都切好了,但你忘了告诉后厨把调料倒进锅里。
为何常见:很多新手只关注头文件包含,忽略了编译和链接是两个独立阶段,头文件让编译器认识函数,链接器需要库文件来找到函数实现。
误区CMake 承诺跨平台,那我写一套 CMakeLists.txt 就应该在 Windows 的 MSVC 和 Linux 的 GCC 下都编译得一模一样,不该有任何平台判断代码。
正解CMake 的跨平台是“翻译层的统一”,不是“做法完全相同”。它会自动适配部分差异,但编译器专属选项仍然需要你手动区分——比如 MSVC 的 /utf-8 和 GCC 的 -finput-charset=UTF-8。正确的做法是用 if(MSVC) 分支写不同配置,让每个平台都用“本地最优解”。
为何常见:对“跨平台”的抽象承诺产生了过度期待,忽略了底层编译器确实存在的客观差异,就像麻婆豆腐在成都要用郫县豆瓣,在北京就得用辣酱替代一样。
误区为了让每个目标都能用同一个库,我在 CMakeLists 根目录写了 `include_directories("/opt/boost")` 和 `link_directories("/opt/boost/lib")`,这样省事,所有子目录都能用。
正解这种全局写法会污染所有目标,导致“配菜乱塞进每道菜”——本不需要 Boost 的目标也被迫链接它,依赖关系混乱,编译变慢,还容易产生版本冲突。现代 CMake 推荐用 target_link_libraries(目标 PRIVATE/PUBLIC 库) 的粒度化声明,让每个目标只吃自己该吃的菜。
为何常见:早期 CMake 教程常教全局写法,且项目小时看不出副作用;等模块一多,依赖像蜘蛛网一样蔓延,才意识到“省事”换来了“拆不散的乱麻”。
面试题
Q1. 运行 cmake 命令后,当前目录下最常见的直接产物是什么?
- A.可执行程序文件
- B.平台对应的构建文件← 正确答案
- C.编译后的机器码文件
- D.经过加密的脚本文件
答案:B。CMake 是“厨师长”,产物是给后厨用的任务卡(Makefile 或 Ninja build)而非成品菜。选项 A 和 C 是 Make/Ninja 和编译器的工作成果,D 更是无中生有。记住“CMake 生成构建文件,构建文件再驱动编译”这条铁律即可。
Q2. 在 CMakeLists.txt 中,用于创建可执行程序目标的命令是哪个?
- A.project 声明工程名称
- B.add_executable 链接源文件← 正确答案
- C.cmake 命令行配置项目
- D.install 安装生成文件
答案:B。add_executable(app main.cpp) 把源文件“食材”做成名为 app 的可执行“菜品”。project 只声明工程名,cmake 是外部命令,install 负责安装。如果你选错,说明还没分清“声明目标”和“配置项目”两类职责。
Q3. target_link_libraries(app PRIVATE utils) 中 PRIVATE 的含义是什么?
- A.依赖仅当前目标使用← 正确答案
- B.依赖强制传递给下游
- C.依赖只在编译时检查
- D.依赖仅调试模式下启用
答案:A。PRIVATE 表示 utils 只对 app 自己可见,不会传染给链接 app 的其他目标,就像“这道菜的秘密调料不外传”。PUBLIC 才表示传递依赖,INTERFACE 表示只传递不直接使用。理解作用域是现代化 CMake 的基本功。
Q4. 运行 find_package(OpenCV REQUIRED) 失败时,第一步应该检查什么?
- A.库是否安装且路径可搜索← 正确答案
- B.编译器版本是否足够新
- C.源码文件是否命名正确
- D.CPU 是否支持所需指令集
答案:A。find_package 相当于去仓库找调料包,找不到首先得确认仓库里有没有货(库是否安装)以及地址对不对(CMAKE_PREFIX_PATH 是否正确)。编译器版本和 CPU 指令集与“找包”无关,源码命名更不影响查找。
Q5. 下列哪些属于 CMake 支持的构建后端?
- A.Make 和 Ninja 构建任务← 正确答案
- B.Visual Studio 生成器← 正确答案
- C.Xcode 工程生成器← 正确答案
- D.Docker 镜像构建器
答案:ABC。CMake 用 -G 参数选择后厨,Make、Ninja、Visual Studio、Xcode 都是它支持的任务卡格式。Docker 是容器工具,不属于构建后端——它只是把整个厨房装进集装箱,不让后厨直接炒菜。
Q6. 增量构建中,判断某源文件需要重新编译的核心依据是什么?
- A.源文件修改时间晚于目标文件← 正确答案
- B.源文件行数发生明显增加
- C.目标文件存在且非空
- D.编译器缓存被手动清空
答案:A。核心是时间戳比较 $\tau_f > \tau_{\text{obj}(f)}$:源文件比目标文件新,说明改过代码还没重编。行数变化不直接对应修改,目标存在且非空恰恰说明不需要重编,缓存清空也不是增量判断的依据。这一逻辑对应面试中最常考的构建系统原理题。
Q7. CMake 内置的用于判断目标平台的条件变量有哪些?
- A.WIN32 代表 Windows 系统← 正确答案
- B.UNIX 代表类 Unix 系统← 正确答案
- C.APPLE 代表苹果系统← 正确答案
- D.LINUX 代表 Linux 系统
答案:ABC。CMake 内置变量是 WIN32、UNIX、APPLE。Linux 系统的判断方式是 UNIX AND NOT APPLE,并不存在名为 LINUX 的内置变量。这是初学者最容易踩的坑,也是跨平台分支的常见考点。
Q8. Release 构建类型通常对应的编译选项组合是哪个?
- A.-O3 与 -DNDEBUG← 正确答案
- B.-g 与 -O0 调试符号
- C.-Wall 开启所有警告
- D.-fsanitize 内存检查
答案:A。Release 如同宴客做法:-O3 全力优化性能,-DNDEBUG 移除断言减小开销。-g -O0 是 Debug 的配置,-Wall 只是警告开关不改变优化级别,-fsanitize 是调试期专用的检测工具。
Q9. 下列哪些属于 CMake“现代”的目标级命令?
- A.target_include_directories← 正确答案
- B.target_compile_features← 正确答案
- C.include_directories 全局
- D.target_compile_definitions← 正确答案
答案:ABD。现代 CMake 强调“目标为中心”,target_* 系列命令把属性精确绑定到单个目标上。include_directories 是旧的全局写法,会把目录塞给所有目标,属于遗留风格。考察点在于你是否理解作用域控制的价值。
Q10. 生成器表达式 $<TARGET_FILE:app> 的作用是什么?
- A.获取目标 app 的产物路径← 正确答案
- B.删除目标 app 的源文件
- C.复制目标 app 到指定目录
- D.给目标 app 添加宏定义
答案:A。生成器表达式是 CMake 在生成任务卡阶段才解析的占位符,$<TARGET_FILE:app> 会被替换成 app 目标最终产物的完整路径,常用于自定义命令或安装规则中。它不删文件、不复制文件、不加宏,理解了“生成期求值”这一特性才能答对。