编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程虚拟地址空间
      • 栈与堆底层对决
      • 指针本质与多级解引
      • 指针运算底层真相
      • 函数指针与回调机制
      • 限定符与指针语义
      • 补码与位运算原理
      • IEEE754浮点本质
      • 数组与指针的纠葛
      • 结构体对齐与优化
      • 字符串存储与安全
      • 预处理器宏与条件编译
      • 编译到汇编全流程
      • 链接器符号与重定位
      • 静态库与动态库对比
      • Make与CMake构建
        • 目录
        • 1. 案例引入
          • 1.1 改代码不生效
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构概览
          • 2.1 构建系统的进化树
          • 2.2 Make vs CMake 的职责边界
        • 3. Makefile 三要素
          • 3.1 目标、依赖、命令
          • 3.2 时间戳
          • 3.3 变量与条件判断
          • 3.4 .PHONY 与伪目标
        • 4. 自动变量与模式规则
          • 4.1 $@ $< $^ $? 四兄弟
          • 4.2 模式隐式规则
          • 4.3 递归Make陷阱
          • 4.4 并行构建 -j 与依赖顺序
        • 5. CMake 最小工程
          • 5.1 从零搭建一个 CMake 工程
          • 5.2 target 的现代 CMake 范式
          • 5.3 生成器表达式
          • 5.4 out-of-source
        • 6. 库与外部依赖管理
          • 6.1 add_library 的四种形态
          • 6.2 find_package 查找机制
          • 6.3 FetchContent 自动下载依赖
          • 6.4 install 与导出目标
        • 7. 构建类型与交叉编译
          • 7.1 Debug/Release/RelWithDebInfo/MinSizeRel
          • 7.2 交叉编译工具链文件
          • 7.3 多平台编译
          • 7.4 ccache 与增量编译加速
        • 8. 综合案例串讲
          • 8.1 案例真相揭晓
          • 8.2 从 gcc 到 CMake 构建的进化之路
          • 8.3 面试高频问题清单
          • 8.4 构建系统速查卡
      • 文件IO与系统调用
      • 动态内存管理揭秘
      • 未定义行为与防御
      • C工程化与设计哲学
    • 标准集库

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • C语言入门精通
  • 专栏博客
杨充
2026-06-10
目录

Make与CMake构建

# 16.Make与CMake构建

Makefile 目标/依赖/命令三要素、自动变量 $@/$</$^、隐式规则与模式规则、伪目标 .PHONY、递归 Make 的变量传递、CMake 最小 CMakeLists.txt、add_library/target_link_libraries、find_package 查找外部依赖、install 规则、构建类型 Debug/Release 切换、交叉编译工具链指定

# 目录

  • 1. 案例引入
    • 1.1 改代码不生效
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构概览
    • 2.1 构建系统的进化树
    • 2.2 Make vs CMake 的职责边界
  • 3. Makefile 三要素
    • 3.1 目标、依赖、命令
    • 3.2 时间戳
    • 3.3 变量与条件判断
    • 3.4 .PHONY 与伪目标
  • 4. 自动变量与模式规则
    • 4.1 $@ $< $^ $? 四兄弟
    • 4.2 模式隐式规则
    • 4.3 递归Make陷阱
    • 4.4 并行构建 -j 与依赖顺序
  • 5. CMake 最小工程
    • 5.1 从零搭建一个 CMake 工程
    • 5.2 target 的现代 CMake 范式
    • 5.3 生成器表达式
    • 5.4 out-of-source
  • 6. 库与外部依赖管理
    • 6.1 add_library 的四种形态
    • 6.2 find_package 查找机制
    • 6.3 FetchContent 自动下载依赖
    • 6.4 install 与导出目标
  • 7. 构建类型与交叉编译
    • 7.1 Debug/Release/RelWithDebInfo/MinSizeRel
    • 7.2 交叉编译工具链文件
    • 7.3 多平台编译
    • 7.4 ccache 与增量编译加速
  • 8. 综合案例串讲
    • 8.1 案例真相揭晓
    • 8.2 从 gcc 到 CMake 构建的进化之路
    • 8.3 面试高频问题清单
    • 8.4 构建系统速查卡

# 1. 案例引入

# 1.1 改代码不生效

某即时通讯服务后端有一行关键的配置常量,某天凌晨运维紧急修改了这个值,重新编译部署后——

// config.h —— 全局配置头文件
#define MAX_CONNECTIONS  10000   // ← 从 5000 改成 10000
1
2
$ make
gcc -c server.c -o server.o    # server.c #include "config.h"
gcc -c network.c -o network.o
gcc server.o network.o -o server
1
2
3
4

部署上线后,压测发现连接数还是在 5000 左右崩溃。看了 config.h——值确实是 10000。看了二进制——strings server | grep 10000 什么都没找到。

$ strings server | grep -E '10000|5000'
5000    # 💀 还是 5000!
1
2

直觉怀疑:是不是 make 没重新编译?重新跑一下:

$ make clean && make
$ strings server | grep 10000
10000   # 这次对了
1
2
3

问题:为什么 make(不加 clean)没有检测到 config.h 的变化?

# 1.2 顺藤摸到根因

追查:

  • 假设 1:是不是 make 的依赖写错了?—— 打开 Makefile:
server: server.o network.o
    gcc server.o network.o -o server

server.o: server.c           # ← 只有 server.c!没有 config.h!
    gcc -c server.c -o server.o

network.o: network.c
    gcc -c network.c -o network.o
1
2
3
4
5
6
7
8

凶手找到了:server.o 的依赖列表里没有 config.h。make 检查时间戳——server.c 没变过 → 认为 server.o 是最新的 → 跳过重新编译 → 旧的 .o 里还是 #define MAX_CONNECTIONS 5000 展开后的值。

  • 假设 2:为什么只有 server.c 受影响?—— 因为 network.c 没有 #include "config.h",所以它确实不需要重编。而 server.c 有 include,但 Makefile 不知道这层依赖。

  • 假设 3:那 GCC 能不能自动告诉 make 它依赖了哪些头文件?—— 能!gcc -MM 可以生成依赖关系:

$ gcc -MM server.c
server.o: server.c config.h            # ← GCC 帮你算出了完整依赖!
1
2

但需要手动集成进 Makefile——很多人不做这一步。

这个 bug 是构建系统的幽灵——它不崩不报错,只是"改了代码但没重新编译"。依赖声明不完整、时间戳检查的粒度问题、并行构建的竞态——这些都是在手动 gcc 到 Make 到 CMake 的进化路上,构建系统逐步解决的问题。

这个事故里藏着至少 8 个原理点:

① Makefile 的目标/依赖/命令三要素怎么工作?               → 第 3.1 节
② make 怎么判断一个 target 需不需要重新编译?              → 第 3.2 节
③ $@ $< $^ 这些自动变量代表什么?                         → 第 4.1 节
④ 模式规则 % 是怎么做到"通配"的?                         → 第 4.2 节
⑤ .PHONY 是什么?为什么 clean 要声明为 .PHONY?           → 第 3.4 节
⑥ CMake 怎么自动解决头文件依赖问题?                       → 第 5 章
⑦ find_package 怎么找到外部库?                           → 第 6.2 节
⑧ 交叉编译时 CMake 工具链文件怎么写?                      → 第 7.2 节
1
2
3
4
5
6
7
8

# 1.3 我们要回答什么

这个"改了头文件但没重编"的事故是每个 C/C++ 程序员都会遇到的血泪教训。它折射出构建系统的核心使命:正确地表达文件之间的依赖,只重新编译真正需要重编的部分。

本篇路线:

架构总图 (第 2 章)
   ↓
Makefile 三要素与时间戳 (第 3 章) ─→ 解开"make 怎么知道要不要重编"
   ↓
自动变量与模式规则 (第 4 章) ─→ 解开"怎么写通用的编译规则"
   ↓
CMake 最小工程 (第 5 章) ─→ 解开"CMakeLists.txt 到底做了什么"
   ↓
库与依赖管理 (第 6 章) ─→ 解开"怎么引入第三方库"
   ↓
构建类型与交叉编译 (第 7 章) ─→ 解开"Debug/Release/ARM 怎么切"
   ↓
综合案例 (第 8 章) ─→ 彻底剖开 + 速查卡
1
2
3
4
5
6
7
8
9
10
11
12
13

📌 本篇定位:第 11-15 章讲了"代码怎么变成二进制",本章讲"怎么把这一过程自动化、可重复化"。构建系统是工程的骨架——没有它,每改一行代码就要手动拼一堆 gcc 命令。Make 是元老(1976年),CMake 是现代标准(2000年,但 3.x 后才是真正的"现代 CMake")。

# 2. 架构概览

# 2.1 构建系统的进化树

1970s: 手动 gcc/g++ 一行行敲
   │
   ▼
1976: Make —— 以"文件时间戳"为驱动的依赖管理器
   │     问题: 平台相关语法,不可移植
   │
   ▼
1980s: autotools (autoconf + automake) —— 生成跨平台 Makefile
   │     问题: 配置脚本极慢,难以调试
   │
   ▼
2000: CMake —— "meta-build" 系统: 生成 Makefile / Ninja / VS Project
   │     优势: 跨平台、语法现代、生态丰富
   │
   ▼
2010s+: Meson, Bazel, Buck, xmake...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Make 没有死——Linux 内核、glibc、GCC 本身都在用 Make。但新项目几乎清一色 CMake。

# 2.2 Make vs CMake 的职责边界

┌─────────────────────────────────────────────────────────────────┐
│                         CMake (meta-build)                       │
│                                                                 │
│  输入: CMakeLists.txt (描述"想构建什么")                          │
│  输出: Makefile / Ninja / VS Solution / Xcode Project            │
│                                                                 │
│  做了: 检测平台 (OS/编译器/库是否存在)                             │
│       管理头文件依赖 (.d 文件自动生成)                             │
│       生成干净的 out-of-source 构建目录                           │
│       跨平台 (Linux/macOS/Windows/Android/iOS)                   │
└──────────────────────────────┬──────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│                          Make (build executor)                   │
│                                                                 │
│  输入: Makefile (描述"文件之间怎么依赖,怎么编译")                  │
│  输出: 编译产物 (.o, .so, executable)                            │
│                                                                 │
│  做了: 检查时间戳 → 只重编变化的部分                               │
│       并行执行 (-j8)                                              │
│       变量展开、函数调用                                           │
└─────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

关键:CMake 不直接编译代码——它生成 Makefile(或 Ninja 等),然后 Make 拿这个 Makefile 去调 gcc。CMake 是"构建系统的构建系统"(meta-build system)。

# 3. Makefile 三要素

# 3.1 目标、依赖、命令

Makefile 的最小单元是一条规则:

# 格式: 目标: 依赖1 依赖2 ...
# <TAB> 命令1
# <TAB> 命令2

server.o: server.c server.h
    gcc -c server.c -o server.o
1
2
3
4
5
6

三要素:

  • 目标 (target):要生成的文件(server.o)
  • 依赖 (prerequisites):目标依赖的文件列表(server.c, server.h)
  • 命令 (recipe):生成目标要执行的 shell 命令(必须以 TAB 开头)

# 注释 vs 命令:

# 这是注释
all:
    # 这不是注释!这是传给 shell 的命令!
    @echo "hello"     # @ 符号抑制命令回显
1
2
3
4

第一个 target 是默认 target:

# 当你只敲 make 不带参数时,执行第一个 target
# 约定: 第一个 target 叫 all,依赖所有要构建的产品
all: server client

server: server.o network.o
    gcc server.o network.o -o server

client: client.o network.o
    gcc client.o network.o -o client
1
2
3
4
5
6
7
8
9

# 3.2 时间戳

疑惑:make 怎么知道 server.o 需不需要重新编译?

论证——make 的核心算法极简单:

对于每个 target T,其依赖列表为 [D1, D2, ...]:
  1. 如果 T 文件不存在 → 必须执行命令
  2. 如果 任意 Di 的时间戳 > T 的时间戳 (Di 比 T 新) → 必须执行命令
  3. 否则 → 跳过,T 是最新的
1
2
3
4

第 1 章事故的根本原因:

server.o 的依赖列表: [server.c]   ← 缺少 config.h!
config.h 的时间戳:  2026-06-10 03:00  (刚改的)
server.o 的时间戳:  2026-06-09 12:00  (旧的)
server.c 的时间戳:  2026-06-09 12:00  (没变)

make 检查: server.c 不比 server.o 新 → 跳过!
→ server.o 还是旧的 → 里面还是 MAX_CONNECTIONS=5000
1
2
3
4
5
6
7

头文件依赖问题的标准解法——自动生成 .d 文件:

# GCC 可以自动生成依赖
$ gcc -MM server.c
server.o: server.c config.h

# 集成进 Makefile:
%.d: %.c
    @$(CC) -MM $< | sed 's/\($*\)\.o[ :]*/\1.o $@ : /g' > $@

include $(wildcard *.d)     # 把自动生成的 .d 文件 include 进来
1
2
3
4
5
6
7
8
9

这样当头文件改变时,make 就知道需要重新编译哪些 .o。CMake 内置了这一机制——所以你用 CMake 时不需要手动写 -MM。

# 3.3 变量与条件判断

# 变量定义 (懒求值)
CC       = gcc
CFLAGS   = -Wall -Wextra -O2
LDFLAGS  = -lm
SRCS     = $(wildcard *.c)
OBJS     = $(SRCS:.c=.o)          # 替换后缀: foo.c → foo.o
TARGET   = server

# 条件判断
ifeq ($(DEBUG), 1)
    CFLAGS += -g -O0 -DDEBUG
else
    CFLAGS += -O2 -DNDEBUG
endif

# 使用变量
$(TARGET): $(OBJS)
    $(CC) $^ -o $@ $(LDFLAGS)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

= vs := vs ?= vs +=:

# = : 递归展开 (recursive) —— 使用时才求值
VAR1 = $(VAR2)
VAR2 = hello
# echo $(VAR1) → hello (VAR2 在后面才定义,但 OK)

# := : 简单展开 (simple) —— 定义时立刻求值
VAR3 := $(VAR4)
VAR4 = world
# echo $(VAR3) → (空) (VAR4 此时还未定义)

# ?= : 只在未定义时赋值
CC ?= gcc           # 如果环境变量 CC 已设,就沿用;否则用 gcc

# += : 追加
CFLAGS += -O2
# 等价于 CFLAGS := $(CFLAGS) -O2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Makefile 的函数:

SRCS  = $(wildcard *.c)           # 通配符 → foo.c bar.c
OBJS  = $(patsubst %.c,%.o,$(SRCS)) # 模式替换 → foo.o bar.o
DIRS  = $(shell ls -d */)         # 执行 shell 命令
ADDS  = $(addprefix objs/,$(OBJS))  # 加前缀 → objs/foo.o
1
2
3
4

# 3.4 .PHONY 与伪目标

.PHONY: clean all test

clean:
    rm -f *.o $(TARGET)

test:
    ./run_tests.sh
1
2
3
4
5
6
7

为什么需要 .PHONY:

  1. 防止与文件名冲突——如果恰好有个文件名叫 clean,不加 .PHONY 的 make clean 会检查 clean 文件的时间戳,发现它"已是最新"就跳过执行。
  2. 性能优化——.PHONY 告诉 make "这个 target 不代表一个文件,每次都要执行"——make 可以跳过时间戳检查。

典型用法:

.PHONY: all clean install test format lint

all: $(TARGET)

clean:
    rm -rf build/

format:
    clang-format -i *.c *.h
1
2
3
4
5
6
7
8
9

# 4. 自动变量与模式规则

# 4.1 $@ $< $^ $? 四兄弟

自动变量让 Makefile 的规则可以复用——你不用在每个 target 里重复写文件名:

# 不用自动变量:啰嗦
server.o: server.c server.h
    gcc -c server.c -o server.o

# 用自动变量:简洁 + 通用
%.o: %.c
    $(CC) -c $< -o $@
1
2
3
4
5
6
7
变量 含义 示例 (%.o: %.c..h)
$@ 当前目标名 server.o
$< 第一个依赖 server.c
$^ 所有依赖(去重) server.c server.h
$? 比目标新的依赖列表 (只有改过的依赖)
$* 模式匹配的 stem(% 的部分) server

实战示例:

# 编译规则
%.o: %.c
    @echo "[CC] $< → $@"
    $(CC) $(CFLAGS) -c $< -o $@

# 链接规则
$(TARGET): $(OBJS)
    @echo "[LD] $@"
    $(CC) $^ -o $@ $(LDFLAGS)

# 展开后等价于:
# server: server.o network.o
#     gcc server.o network.o -o server
1
2
3
4
5
6
7
8
9
10
11
12
13

$(@D) 和 $(@F) 拆分目录和文件名:

# 如果 $@ = build/objs/server.o
#    $(@D) = build/objs      (目录部分)
#    $(@F) = server.o         (文件名部分)

build/objs/%.o: src/%.c
    @mkdir -p $(@D)           # 确保输出目录存在
    $(CC) -c $< -o $@
1
2
3
4
5
6
7

# 4.2 模式隐式规则

模式规则(pattern rule)——用 % 通配:

# 显式规则: 一个规则只匹配一个文件
server.o: server.c

# 模式规则: 一个规则匹配一类文件
%.o: %.c
    $(CC) -c $< -o $@
1
2
3
4
5
6

GNU Make 的内置隐式规则:

$ make -p -f /dev/null | grep -A5 '^%.o: %.c'
%.o: %.c
#  recipe to execute (built-in):
    $(COMPILE.c) $(OUTPUT_OPTION) $<

# 你什么都不写,make 也知道怎么从 .c 生成 .o
# → 因为有一个内置的隐式规则: %.o: %.c
# → 甚至连 $(COMPILE.c) 这个变量都内置了:
#   COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
1
2
3
4
5
6
7
8
9

利用隐式规则,最短的 Makefile:

# 3 行就能编译一个单文件 C 程序
CC      = gcc
CFLAGS  = -Wall -O2

hello: hello.o       # make 自动知道怎么从 hello.c 生成 hello.o
1
2
3
4
5

变量继承链(可通过 make -p 查看完整列表):

CC        → gcc
CFLAGS    → (空)
CPPFLAGS  → (空)
CXX       → g++
LDFLAGS   → (空)
LDLIBS    → (空)
1
2
3
4
5
6

# 4.3 递归Make陷阱

当项目很大时,你可能会在每个子目录里放一个 Makefile,然后用递归调用:

# 顶层 Makefile
SUBDIRS = src tests tools

all:
    for dir in $(SUBDIRS); do \
        $(MAKE) -C $$dir; \
    done

clean:
    for dir in $(SUBDIRS); do \
        $(MAKE) -C $$dir clean; \
    done
1
2
3
4
5
6
7
8
9
10
11
12

递归 Make 的坑——变量不自动传递:

# 顶层 Makefile
export CFLAGS = -O2

# 子目录 Makefile
# 如果顶层没有 export CFLAGS,子 make 看不到这个变量
# 方法1: export CFLAGS
# 方法2: $(MAKE) CFLAGS=$(CFLAGS) -C subdir
1
2
3
4
5
6
7

更好的做法——用 include 替代递归:

# 顶层 Makefile
include src/module.mk
include tests/module.mk
# 把所有子模块的 Makefile 片段 include 进一个"大 Makefile"
# → 所有变量在同一个命名空间,不需要递归传递
# → make 可以全局分析依赖图,做到更优的并行
1
2
3
4
5
6

# 4.4 并行构建 -j 与依赖顺序

$ make -j8    # 最多同时跑 8 个编译任务
$ make -j     # 不限制并行数 (小心 OOM)
1
2

并行构建的关键:Makefile 中的依赖必须正确声明依赖关系,否则并行构建会出现竞态:

# ❌ 有竞态: A 和 B 没有声明依赖 → make 可能同时编译它们
#    如果 B 依赖 A 编译生成的 .h 文件 → 并行构建崩
A:
    gcc -c a.c
    generate_header "a.h"    # 生成头文件

B:
    gcc -c b.c               # b.c 里 #include "a.h"
                              # 如果 A 还没跑完 → b.c 找不到 a.h!

# ✅ 正确声明:
B: A                          # B 依赖 A → make 保证 A 先编译完
    gcc -c b.c
1
2
3
4
5
6
7
8
9
10
11
12
13

.NOTPARALLEL 禁用并行:

.NOTPARALLEL:     # 整个 Makefile 串行
1

# 5. CMake 最小工程

# 5.1 从零搭建一个 CMake 工程

最小的 CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(MyServer VERSION 1.0.0 LANGUAGES C)

add_executable(server
    src/main.c
    src/server.c
    src/network.c
)

target_include_directories(server PRIVATE include)
target_compile_options(server PRIVATE -Wall -Wextra)
target_link_libraries(server PRIVATE m pthread)
1
2
3
4
5
6
7
8
9
10
11
12

构建命令:

# 经典三步
$ mkdir build && cd build       # 1. 创建独立的构建目录
$ cmake ..                       # 2. 配置 (生成 Makefile)
$ make                           # 3. 编译

# 或者用 CMake 的 --build (不用记底层是 make 还是 ninja)
$ cmake --build .
1
2
3
4
5
6
7

这段 CMakeLists.txt 背后做了什么:

cmake .. (配置阶段)
  ├─ 检测: 有没有 gcc? 什么版本? 在哪个路径?
  ├─ 检测: libm.so 在哪?
  ├─ 检测: pthread 库在哪?
  ├─ 生成: Makefile (包含所有编译规则 + 头文件依赖自动追踪)
  └─ 生成: CMakeCache.txt (缓存检测结果,下次 cmake 更快)

make (构建阶段)
  ├─ 编译 src/main.c → src/main.c.o
  ├─ 编译 src/server.c → src/server.c.o
  ├─ 编译 src/network.c → src/network.c.o
  └─ 链接 → server
1
2
3
4
5
6
7
8
9
10
11
12

# 5.2 target 的现代 CMake 范式

"Modern CMake"(3.0+)的核心思想:一切围绕 target 来组织,而不是全局变量:

# ❌ 老式 CMake (2.x 风格) —— 全局变量满天飞
include_directories(include)         # 全局的 include 路径
link_libraries(m pthread)            # 全局的链接库
add_definitions(-DENABLE_FEATURE)    # 全局的宏定义
add_executable(server main.c)

# ✅ 现代 CMake —— 每个 target 有自己的属性
add_executable(server main.c server.c)
target_include_directories(server PRIVATE include)     # server 的私有 include
target_link_libraries(server PRIVATE m pthread)        # server 的私有链接
target_compile_definitions(server PRIVATE ENABLE_FEATURE)

add_library(network network.c)                         # 另一个 target: 库
target_include_directories(network PUBLIC include)     # PUBLIC: 使用者也能继承
target_link_libraries(server PRIVATE network)          # server 依赖 network
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

PRIVATE vs PUBLIC vs INTERFACE:

# PRIVATE: 只对当前 target 生效,不传播给依赖者
target_include_directories(server PRIVATE src/internal)
# → 编译 server.c 时用这个路径,但链接 server 的其他 target 看不到

# PUBLIC: 当前 target 和依赖者都能用
target_include_directories(network PUBLIC include)
# → 编译 network.c 时用,链接 network 的 server 也能用

# INTERFACE: 当前 target 不需要,但依赖者需要 (header-only 库)
add_library(header_only INTERFACE)           # 没有 .c 文件
target_include_directories(header_only INTERFACE include)
target_compile_definitions(header_only INTERFACE USE_HEADER_ONLY)
1
2
3
4
5
6
7
8
9
10
11
12

# 5.3 生成器表达式

生成器表达式 (Generator Expression) 允许在生成阶段(cmake → Makefile)做条件判断:

# 根据构建类型选择编译选项
target_compile_options(server PRIVATE
    $<$<CONFIG:Debug>:-g -O0>
    $<$<CONFIG:Release>:-O2 -DNDEBUG>
)

# 如果编译器是 GCC/Clang 才加这个 warning flag
target_compile_options(server PRIVATE
    $<$<OR:$<C_COMPILER_ID:GNU>,$<C_COMPILER_ID:Clang>>:-Wshadow>
)

# 链接时的条件
target_link_libraries(server PRIVATE
    $<$<PLATFORM_ID:Linux>:dl pthread>
    $<$<PLATFORM_ID:Darwin>:>           # macOS 不需要 -ldl
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

为什么用生成器表达式而不是 if():因为 if() 在配置阶段求值(cmake 时),而生成器表达式在生成阶段求值(make 时)。多配置生成器(如 VS/Xcode)需要后者——一份 CMakeLists.txt 同时支持 Debug 和 Release。

# 5.4 out-of-source

In-source build(反模式):

$ cmake .            # 💀 在当前目录生成构建文件
$ ls
CMakeLists.txt
CMakeCache.txt       # 污染源代码目录
Makefile
CMakeFiles/
src/              # 源代码和构建产物混在一起
1
2
3
4
5
6
7

Out-of-source build(标准做法):

$ mkdir build && cd build
$ cmake ..
$ ls ..
CMakeLists.txt       # 源代码目录保持干净
src/
$ ls build
Makefile             # 所有构建产物在 build/ 下
CMakeCache.txt
CMakeFiles/
1
2
3
4
5
6
7
8
9

收益:

  1. 多配置共存:build_debug/、build_release/、build_arm/ 共存
  2. .gitignore 极简:只需要加一行 build*/
  3. 清理简单:rm -rf build/ 就是彻底的 clean

# 6. 库与外部依赖管理

# 6.1 add_library 的四种形态

# 1. STATIC: 静态库 .a
add_library(mylib STATIC src/foo.c src/bar.c)
# → 生成 libmylib.a

# 2. SHARED: 动态库 .so
add_library(mylib SHARED src/foo.c src/bar.c)
# → 生成 libmylib.so

# 3. INTERFACE: 纯头文件库 (header-only)
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include)
# → 不生成二进制,只传递 include 路径和编译定义

# 4. OBJECT: 编译成 .o 但不链接成库 (减少重复编译)
add_library(mylib_obj OBJECT src/foo.c src/bar.c)
add_executable(server main.c $<TARGET_OBJECTS:mylib_obj>)
add_executable(client cli.c $<TARGET_OBJECTS:mylib_obj>)
# → foo.c/bar.c 只编译一次,但 server 和 client 各自链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 6.2 find_package 查找机制

find_package 的两种模式:

# 模式 1: Config 模式 (查找 .cmake 配置文件)
#   查找 <Package>Config.cmake 或 <package>-config.cmake
find_package(OpenSSL REQUIRED)
# REQUIRED: 找不到就报错退出 (QUIET: 找不到静默跳过)

# 模式 2: Module 模式 (查找 Find<Package>.cmake)
#   CMake 内置了大量 Find*.cmake 模块
find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)
find_package(CURL REQUIRED)

# 使用找到的库
target_link_libraries(server PRIVATE
    OpenSSL::SSL          # CMake target (现代用法)
    Threads::Threads
    ZLIB::ZLIB
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

find_package 的搜索路径优先级:

1. CMAKE_PREFIX_PATH 环境变量 / CMake 变量指定的路径
2. <Package>_ROOT 变量 (如 OpenSSL_ROOT=/opt/openssl)
3. 系统默认路径: /usr, /usr/local, /opt
4. CMAKE_SYSTEM_PREFIX_PATH
1
2
3
4

手写 Find 模块示例:

# FindMyLib.cmake
find_path(MyLib_INCLUDE_DIR mylib.h
    PATHS ${MyLib_ROOT}/include)

find_library(MyLib_LIBRARY mylib
    PATHS ${MyLib_ROOT}/lib)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MyLib
    REQUIRED_VARS MyLib_LIBRARY MyLib_INCLUDE_DIR)

if(MyLib_FOUND AND NOT TARGET MyLib::MyLib)
    add_library(MyLib::MyLib UNKNOWN IMPORTED)
    set_target_properties(MyLib::MyLib PROPERTIES
        IMPORTED_LOCATION "${MyLib_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${MyLib_INCLUDE_DIR}")
endif()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 6.3 FetchContent 自动下载依赖

CMake 3.11+ 的 FetchContent 可以在配置阶段下载并编译外部依赖:

include(FetchContent)

FetchContent_Declare(
    zlib
    GIT_REPOSITORY https://github.com/madler/zlib.git
    GIT_TAG        v1.3.1
)
FetchContent_MakeAvailable(zlib)
# → 自动 git clone → cmake → make → 编译成 zlib target
# → 你的代码可以直接 target_link_libraries(server PRIVATE zlib)
1
2
3
4
5
6
7
8
9
10

FetchContent vs git submodule vs 系统包管理器:

方案 优点 缺点
FetchContent 完全自动化,一个 cmake 搞定 首次慢(需下载+编译),CI 需缓存
git submodule 源码级可调试 需要手动 git submodule update --init
系统包管理器 (apt/brew) 预编译,极快安装 版本不可控,跨平台不一致
vcpkg/conan 预编译 + 跨平台 需要额外的包管理器安装

# 6.4 install 与导出目标

install 规则——让库"可被安装":

# 安装二进制
install(TARGETS mylib server
    RUNTIME DESTINATION bin        # .exe / 可执行文件
    LIBRARY DESTINATION lib        # .so
    ARCHIVE DESTINATION lib/static # .a
)

# 安装头文件
install(DIRECTORY include/
    DESTINATION include/mylib
    FILES_MATCHING PATTERN "*.h"
)

# 安装 CMake 配置 (让 find_package 能找到)
install(EXPORT mylibTargets
    NAMESPACE MyLib::
    DESTINATION lib/cmake/mylib
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

安装后,其他项目就可以通过 find_package(mylib REQUIRED) 找到它。

# 7. 构建类型与交叉编译

# 7.1 Debug/Release/RelWithDebInfo/MinSizeRel

CMake 内置四种构建类型:

$ cmake -DCMAKE_BUILD_TYPE=Debug ..
$ cmake --build .

# 对应的 GCC 标志:
# Debug:          -g -O0
# Release:        -O3 -DNDEBUG
# RelWithDebInfo: -O2 -g -DNDEBUG
# MinSizeRel:     -Os -DNDEBUG
1
2
3
4
5
6
7
8

自定义标志:

# 追加而不是覆盖
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=address")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -march=native")

# 添加新的构建类型
set(CMAKE_C_FLAGS_PROFILE "-O2 -g -pg")
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "-pg")
1
2
3
4
5
6
7

多配置并存:

$ mkdir build_debug && cd build_debug && cmake -DCMAKE_BUILD_TYPE=Debug ..
$ mkdir build_release && cd build_release && cmake -DCMAKE_BUILD_TYPE=Release ..
# 两个构建目录互不干扰
1
2
3

# 7.2 交叉编译工具链文件

工具链文件 (toolchain file) 是 CMake 交叉编译的核心:

# arm-linux-gnueabihf.cmake —— 给 ARM Linux 交叉编译的工具链文件
set(CMAKE_SYSTEM_NAME        Linux)
set(CMAKE_SYSTEM_PROCESSOR   arm)

# 指定交叉编译器
set(CMAKE_C_COMPILER         arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER       arm-linux-gnueabihf-g++)

# 指定 sysroot (目标系统的根目录)
set(CMAKE_SYSROOT            /opt/arm-sysroot)
set(CMAKE_FIND_ROOT_PATH     /opt/arm-sysroot)

# 查找库/头文件时只搜索 sysroot
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)     # 程序用本机
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)      # 库只在目标 sysroot 找
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)      # 头文件只在目标 sysroot 找
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)      # find_package 只找目标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用:

$ cmake -DCMAKE_TOOLCHAIN_FILE=arm-linux.cmake ..
$ make
1
2

Android NDK 交叉编译示例:

$ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
        -DANDROID_ABI=arm64-v8a \
        -DANDROID_PLATFORM=android-21 \
        ..
1
2
3
4

# 7.3 多平台编译

# 检测平台
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    target_link_libraries(server PRIVATE dl pthread)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    # macOS: 不需要 -ldl, pthread 已内置
    target_link_libraries(server PRIVATE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    target_link_libraries(server PRIVATE ws2_32)
endif()

# 检测架构
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
    message(STATUS "Building for 64-bit")
    add_definitions(-DARCH_64BIT)
endif()

# 检测编译器
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
    target_compile_options(server PRIVATE -Wno-unused-parameter)
elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
    target_compile_options(server PRIVATE -Weverything)
elseif(MSVC)
    target_compile_options(server PRIVATE /W4)
endif()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 7.4 ccache 与增量编译加速

# 安装 ccache
$ sudo apt install ccache          # Ubuntu
$ brew install ccache              # macOS

# 方式1: CMake 自动集成
$ cmake -DCMAKE_C_COMPILER_LAUNCHER=ccache \
        -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ..

# 方式2: 环境变量 (对所有 make 生效)
$ export CC="ccache gcc"
$ export CXX="ccache g++"

# 查看命中率
$ ccache -s
cache hit (direct)                 12345
cache hit (preprocessed)            678
cache miss                          890
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

ccache 的原理:缓存编译的"输入哈希 → 输出 .o"映射。当同一个文件的编译选项、头文件、源代码都没变时,直接返回缓存的 .o——全量重编译变成几乎零开销。

# 8. 综合案例串讲

# 8.1 案例真相揭晓

回到第 1 章 config.h 修改未重编的事故,八个疑问逐条作答:

疑问 答案
① Makefile 目标/依赖/命令怎么工作? 第 3.1:target: deps → recipe。make 检查 deps 时间戳是否比 target 新
② make 怎么判断要不要重编? 第 3.2:依赖比目标新 → 重编;否则跳过
③ $@ $< $^ 代表什么? 第 4.1:$@=目标, $<=第一个依赖, $^=所有依赖, $?=变更的依赖
④ 模式规则 % 怎么通配? 第 4.2:%.o: %.c 匹配所有 .c→.o 的转换,$* = stem
⑤ .PHONY 为什么需要? 第 3.4:防止与同名文件冲突,告诉 make "每次都执行"
⑥ CMake 怎么解决头文件依赖? 第 5.2:内置 .d 文件自动生成机制,target_include_directories 隐式追踪
⑦ find_package 怎么找到库? 第 6.2:Config 模式(找 Config.cmake) 或 Module 模式(找 Find*.cmake)
⑧ 交叉编译工具链怎么写? 第 7.2:CMAKE_TOOLCHAIN_FILE 指定编译器 + sysroot + find_root_path

第 1 章事故的完整根因链条:

config.h 修改 (MAX_CONNECTIONS: 5000 → 10000)
   ↓
make 检查 server.o 的依赖: [server.c] ← 缺少 config.h!
   ↓
server.c 时间戳没变 → make 认为 server.o 已是最新 → 跳过
   ↓
server.o 还是旧的 MAX_CONNECTIONS=5000
   ↓
链接 → 部署 → 上限仍然是 5000
1
2
3
4
5
6
7
8
9

修复方案:

方案 A:Makefile 正确声明头文件依赖(手动)

server.o: server.c config.h         # ← 加上 config.h
    gcc -c server.c -o server.o
1
2

方案 B:Makefile 自动生成 .d 依赖文件

DEPS = $(SRCS:.c=.d)
-include $(DEPS)

%.d: %.c
    @$(CC) -MM $< | sed 's/\($*\)\.o[ :]*/\1.o $@ : /g' > $@
1
2
3
4
5

方案 C:切换到 CMake(自动搞定一切)

cmake_minimum_required(VERSION 3.16)
project(Server LANGUAGES C)
add_executable(server server.c network.c)
target_include_directories(server PRIVATE .)
# → CMake 自动追踪 #include 依赖,不需要手动维护
1
2
3
4
5

# 8.2 从 gcc 到 CMake 构建的进化之路

同一份 3 文件的 C 项目,三种构建方式的对比:

项目结构:
  src/main.c
  src/network.c
  src/network.h
  include/config.h

原始方式: 手动 gcc
  $ gcc -c src/main.c -o build/main.o -Iinclude
  $ gcc -c src/network.c -o build/network.o -Iinclude
  $ gcc build/main.o build/network.o -o server
  问题: 改了一个文件 → 要记得手动重编哪些 .o、怎么链接

Makefile 方式
  $ make
  问题: 需要手动维护依赖列表、手写编译规则、不跨平台

CMake 方式
  $ mkdir build && cd build && cmake .. && make
  自动: 跨平台、依赖追踪、多配置、find_package...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 8.3 面试高频问题清单

1. Makefile 的目标/依赖/命令三要素分别是什么?

目标(target):要生成的文件。依赖(prerequisites):生成目标所依赖的文件列表。命令(recipe):生成目标要执行的 shell 命令。make 通过比较目标和依赖的时间戳来决定是否执行命令。

2. $@ $< $^ 分别代表什么?

$@ = 当前目标名。$< = 第一个依赖(通常用于编译规则,如 %.o: %.c 中 $< = foo.c)。$^ = 所有依赖(去重)。$? = 比目标更新的依赖列表。详见第 4.1 节。

3. .PHONY 的作用?不加有什么后果?

声明目标为"伪目标"——不代表一个文件,每次都要执行。不加的话,如果恰好存在一个叫 clean 的文件,make clean 会因为"clean 文件已是最新"而跳过执行。

4. Makefile 中的 = 和 := 区别?

= 是递归展开(使用时才求值),:= 是简单展开(赋值时立刻求值)。递归展开可能导致意料之外的循环展开问题。

5. CMake 的 PRIVATE/PUBLIC/INTERFACE 区别?

PRIVATE:只对当前 target 生效,不传播。PUBLIC:当前 target 和依赖者都能用。INTERFACE:当前 target 不需要但依赖者需要(header-only 库)。详见第 5.2 节。

6. CMake 怎么配置 Debug 和 Release ?

cmake -DCMAKE_BUILD_TYPE=Debug .. 或 Release。可以在 CMakeLists.txt 中按构建类型追加编译选项:set(CMAKE_C_FLAGS_DEBUG "...")。多配置并存:build_debug/ 和 build_release/ 两个目录。

7. find_package 的查找机制是什么?

Config 模式:查找 <Package>Config.cmake(包自己提供的 CMake 配置)。Module 模式:查找 CMake 内置或用户写的 Find<Package>.cmmake。搜索路径包括 CMAKE_PREFIX_PATH、<Package>_ROOT、系统默认路径。

8. 交叉编译时 CMake 怎么指定工具链?

编写工具链文件(toolchain.cmake),设置 CMAKE_C_COMPILER、CMAKE_SYSROOT 等,然后用 cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake .. 配置。Android NDK 自带 android.toolchain.cmake。

9. Make 的 -j 并行构建有什么注意事项?

make -j8 最多同时 8 个任务。必须正确声明依赖关系,否则并行构建会出现竞态(如 B 需要 A 生成的头文件,但 A 还没跑完)。可以用 .NOTPARALLEL 禁用并行。

10. ccache 怎么加速 C/C++ 构建?

ccache 缓存编译的"输入哈希→输出 .o"映射。相同源码+编译选项+头文件 → 直接返回缓存。cmake -DCMAKE_C_COMPILER_LAUNCHER=ccache .. 或 export CC="ccache gcc"。首次慢,后续快数倍。

# 8.4 构建系统速查卡

Makefile 快速参考:

# 常用变量
CC       = gcc
CFLAGS   = -Wall -O2
LDFLAGS  = -lm
SRCS     = $(wildcard src/*.c)
OBJS     = $(patsubst src/%.c,build/%.o,$(SRCS))
TARGET   = server

# 默认目标
.PHONY: all clean
all: $(TARGET)

# 模式规则
build/%.o: src/%.c
    @mkdir -p $(@D)
    $(CC) $(CFLAGS) -c $< -o $@

# 链接
$(TARGET): $(OBJS)
    $(CC) $^ -o $@ $(LDFLAGS)

# 自动依赖生成
DEPS = $(OBJS:.o=.d)
-include $(DEPS)
build/%.d: src/%.c
    @$(CC) -MM $< -MT $(@:.d=.o) > $@

# 清理
clean:
    rm -rf build/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

CMake 快速参考:

# 最小模板
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES C)
add_executable(myapp main.c util.c)
target_include_directories(myapp PRIVATE include)
target_link_libraries(myapp PRIVATE m)
target_compile_options(myapp PRIVATE -Wall -Wextra)

# 库
add_library(mylib STATIC lib.c)
target_include_directories(mylib PUBLIC include)

# 外部依赖
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL)

# 安装
install(TARGETS myapp RUNTIME DESTINATION bin)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

常用命令:

# Make 相关
make                            # 构建默认目标
make -j$(nproc)                 # 并行构建 (所有核)
make target                     # 构建特定目标
make -n                         # dry-run (只打印命令不执行)
make V=1                        # 显示完整命令 (默认只显示简化版)
make --debug=b                  # 调试: 显示依赖检查过程

# CMake 相关
cmake -S . -B build             # 源码目录 . → 构建目录 build
cmake -DCMAKE_BUILD_TYPE=Debug ..      # Debug
cmake -DCMAKE_BUILD_TYPE=Release ..    # Release
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..  # 生成 compile_commands.json (IDE/工具用)
cmake --build . -j $(nproc)     # 构建 (不依赖底层是 make 还是 ninja)
ccmake ..                       # 终端 UI 配置界面

# 问题诊断
make -p                         # 打印所有规则和变量 (含内置)
cmake --system-information      # 系统信息 dump
cmake --trace                   # 跟踪 CMake 脚本执行 (极详细)
cmake --graphviz=deps.dot ..    # 生成依赖图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Makefile vs CMake 选型:

场景 推荐
< 5 个文件的小工具 Makefile (甚至不用,直接 shell 脚本)
中等 C 项目 (10~100 文件) Makefile 或 CMake,看团队偏好
大型 C/C++ 项目 CMake (跨平台、依赖管理、生态系统)
需要跨平台 (Win/Mac/Linux) CMake 几乎是唯一选择
嵌入式/交叉编译 CMake (工具链文件机制成熟)
Linux 内核/ glibc / GCC 本身 Make (历史原因 + 极度复杂的构建逻辑)

下一篇:17.GDB调试底层原理 —— 我们已经知道"怎么把代码编译成二进制",下一步进入调试:ptrace 系统调用怎么让 GDB 接管另一个进程?断点指令 int3 是怎么插入的?watchpoint 用了什么硬件机制?core dump 文件里到底存了什么?

上次更新: 2026/06/11, 09:01:44
静态库与动态库对比
文件IO与系统调用

← 静态库与动态库对比 文件IO与系统调用→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式