头文件——依赖关系的静态表达

依赖关系在代码里有两个层面的表现:运行期的动态调用(A 调用 B 的函数)和编译期的静态包含(A 包含 B 的头文件)。头文件是依赖关系的静态表达——它把「谁依赖谁」写在文件层面,不运行就能看到。这一条线:头文件如何表达依赖、它表达了什么依赖、不表达什么依赖。


一、依赖的两种表现:动态调用 vs 静态包含

1
2
运行期依赖(动态):A 调用 B 的函数 → 程序运行时才发生
静态依赖(编译期):A include B 的头文件 → 编译时就能看到
1
2
3
4
5
6
// a.c
#include "b.h" // 静态依赖:a.c 依赖 b.h(编译期可见)

void a_func() {
b_func(); // 动态依赖:a.c 调用 b 的函数(运行期发生)
}

静态包含是动态调用的前提——如果不包含 b.h,连 b_func 都声明不了,更谈不上调用。所以头文件表达的是「能依赖什么」,调用表达的是「实际依赖了什么」。


二、头文件表达的三层依赖

1. 包含关系 = 编译期依赖(构建依赖)

1
2
3
4
a.c 包含 b.h
→ a.c 的编译依赖 b.h
→ 构建时 b.h 必须先于 a.c 可用
→ 头文件图 = 构建顺序的依据

2. 声明 = 对符号的依赖

头文件里用到的类型、函数、常量,都是对某个符号的依赖:

1
2
3
4
// a.h
#include "b.h" // 依赖 b.h 里的类型
typedef struct { b_t member; } a_t; // 依赖 b_t
void a_use(b_t* p); // 依赖 b_t

看头文件里的声明,就知道这个模块离不开谁

3. 包含图 = 静态依赖图

把整个项目的 include 关系画出来,就是一张静态依赖图

1
2
main.c ──→ app.h ──→ service.h ──→ domain.h
└──→ infra.h ──→ os.h

这张图不运行代码就能得到——它是依赖关系的「静态快照」。


三、头文件表达什么、不表达什么

1
2
3
4
5
6
7
8
9
头文件表达的:
✅ 编译期依赖(谁包含谁)
✅ 符号依赖(用了谁的类型/函数)
✅ 构建顺序(依赖先构建)

头文件不表达的:
❌ 运行期调用频率(调用多少次、什么时候调)
❌ 依赖方向是否合规(有没有反向依赖、循环依赖——图上看不出好坏)
❌ 注入关系(谁把实例传给谁)

头文件给的是「依赖事实」,不是「依赖是否合理」——事实用它看,合理性要靠审查(见工程控制主题的审查线)。


四、和其他线的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
头文件(静态表达)←→ 依赖关系的演化(本主题 01)
依赖的形态在文件/构建阶段的表现 = 构建依赖
本线是它的静态侧:构建依赖长什么样

头文件(静态表达)←→ 接口
头文件 = 接口的物理形式(声明=接口,实现=源文件)

头文件(静态表达)←→ 七条流·多文件阶段
头文件 = 控制流边界(封装的物理基础)
看不到 = 不能直接调用

头文件(静态表达)←→ 工程控制·审查
头文件的静态依赖图是审查依赖关系的依据

收束

1
2
3
4
5
6
7
8
9
10
11
12
依赖有两种表现:
动态调用(运行期,程序运行时才发生)
静态包含(编译期,看头文件就知道)← 本线

头文件表达三层依赖:
包含关系 = 编译期依赖 / 构建顺序
声明 = 符号依赖
包含图 = 静态依赖图

头文件给「依赖事实」,不给「依赖是否合理」:
事实用它看(静态图)
合理性靠审查(工程控制主题的审查线)