03-多文件的十条流
多文件的十条流
当代码从一个文件拆成多个文件时,最关键的变化不是「文件变多了」,而是接口出现了。头文件定义了函数签名,实现了「能做什么」和「怎么做」的分离。十条流在这个阶段发生了质变——控制流第一次有了明确的「边界」,反馈流终于可以跨文件回退(启动流仍未形成,链接顺序是编译期问题,见第二节)。
一、从一个文件到多个文件
上一篇的 args.c 把 main、print_args、format_arg 写在一个文件里。现在拆开:
1 | // format.h |
1 | // print.h |
1 | // main.c |
三个文件,三个编译单元。main.c 通过 print.h 知道 print_args 的存在,通过 print.c 间接使用 format.h。文件之间只通过头文件(接口)通信,不通过实现细节通信。
这个变化让十条流全部重新洗牌。
二、启动流:尚未形成——链接顺序是编译期问题
启动流只有有框架才有。多文件阶段仍然只有一个入口 main,没有框架,启动流还没有形成。
这个阶段看起来和「启动」有关的新东西是链接顺序——但它是编译期问题,不是运行期启动:
1 | 编译时: |
链接顺序决定符号解析的顺序,头文件包含关系决定编译时的可见性:
1 | main.c 能看到的:print_args(通过 print.h) |
文件之间的可见性由头文件决定——看不到 = 不能直接调用。这是「封装」的物理基础,但属于接口/控制流的范畴,不是启动流。
启动流要等到多系统阶段——框架化子系统出现——才真正形成。
三、执行流:从调用图到跨文件调用链
1 | graph TD |
执行流现在跨文件了:
1 | main.c: main() |
执行流的跨文件追踪变得困难——在 main.c 的调试器里,你看到 print_args 被调用了,但看不到 format_arg(因为 main.c 不包含 format.h)。要看完整调用链,需要切换到 print.c 的上下文。
多文件执行流的核心变化:调用链跨文件边界时,调试和追踪的难度陡增。
四、数据流:跨文件的数据传递
数据现在跨文件流动:
1 | main.c: argc, argv |
数据流跨文件的关键观察:**buf 在 print.c 中分配,通过指针传给 format.c 修改,再回到 print.c 使用**。两个不同的编译单元通过指针共享了同一块内存。
这是多文件数据流的核心模式:通过指针(或引用)跨文件传递可变数据。如果没有指针,format.c 就无法修改 print.c 的 buf——数据流就会断裂。
多文件数据流的核心约束:跨文件数据传递依赖指针/引用,类型信息依赖头文件。
五、状态流:全局状态和静态状态出现
多文件引入了两种新的状态:
1 | // print.c |
1 | // format.c |
print_count 和 format_errors 是文件级全局状态——它们的生命周期是整个程序运行期,但只有各自文件内的函数能访问。
更进一步,如果需要跨文件共享状态:
1 | // shared.h |
多文件状态流的核心变化:出现了三种状态——函数局部(栈上)、文件全局(static)、跨文件全局(extern)。它们的生命周期和可见性完全不同。
六、用户流程:一类多条,止步于 main
多文件阶段用户流程仍然是一类多条:每条命令一个实例,验证 = 输入命令 → 结果符合预期。
1 | 用户 |
用户流程和执行流的交界点始终在 main——main 以下的所有文件都是纯执行流。用户不关心文件怎么拆分、函数怎么调用,只关心命令的结果。
多文件没有给用户流程带来任何新东西——它仍然是同一类(命令行)的多条实例。多步操作、逐步验证的完整用户流程从 TUI 的屏幕导航开始萌芽,到 GUI 才成为真正的线。
七、业务流:从业务步骤到模块职责
单文件级的业务流是「几个函数协作做什么」。多文件级的业务流变成了:每个文件承担一个业务职责。
1 | format.c → 职责:把数据格式化成字符串 |
业务流和文件的对应关系:
1 | 业务步骤 1:接收参数 → main.c |
多文件业务流的核心变化:业务步骤开始和文件(模块)一一对应。 这就是「按职责拆文件」的来源——每个文件负责业务流中的一个步骤。
八、功能流:仍然在函数内部
功能流仍然是单个函数内部的逻辑步骤,不随文件数量变化。format_arg 的功能流和单文件时完全相同。
功能流是唯一不随组织规模变化的流——它始终是函数级别的。
九、控制流:接口成为控制流的边界
1 | graph LR |
这是多文件阶段最重要的变化之一。
单文件时,控制流是「调用者通过参数间接控制被调用者」。多文件时,控制流增加了一个新维度:接口决定了控制流的可见范围。
1 | main.c 能控制的:print_args(因为 print.h 暴露了它) |
头文件 = 控制流的边界。main.c 通过 print.h 只能看到 print_args——它不知道 print_args 内部调用了 format_arg。这意味着:
1 | main.c 的控制权范围: |
控制流的分层:
1 | main.c: 控制 print_args(直接控制) |
每一层只控制自己的直接下级,不知道更深层发生了什么。这就是「封装」在控制流上的体现——每一层只暴露给上层自己愿意暴露的部分。
十、错误流:跨文件的错误传播
1 | graph TD |
多文件的错误流有了真正的传播路径:
1 | format_arg 遇到格式化错误 |
错误传播的方向:从底层到上层,逐级传递。
1 | format.c: snprintf 返回 -1(格式化失败) |
多文件错误流的核心变化:错误传播路径变得清晰,但每一层可以选择吞掉错误。 如果 print_args 不返回错误码,main.c 就完全不知道发生过格式化错误。
十一、反馈流:第一次可以跨文件回退
反馈流在多文件级终于有了实质内容。
假设 format_arg 改为返回错误码:
1 | int format_arg(char *buf, size_t size, int index, const char *value); // 返回 0 成功,-1 失败 |
print_args 可以根据返回值决定:
1 | void print_args(int argc, char **argv) { |
这是线性回退——出错了跳过当前,继续下一个。更进一步,可以实现重试:
1 | int ret = format_arg(buf, sizeof(buf), i, argv[i]); |
但仍然有一个根本限制:**print_args 无法让 format_arg 回到某个中间状态重试**——函数调用是单向的,返回后不能「回到」函数内部的某个点。
多文件反馈流的核心进展:调用者可以根据返回值决定跳过/降级/重试。但仍然是「跳过重来」而非「精确回退」。
反馈流的真正力量——「回到问题产生的那一步,从那里重新执行」——要等到多层系统出现时才会实现。那时有了「层」的概念,上层可以接住下层的失败,从合适的步骤重新开始。
十二、十条流在多文件级的全景
1 | 多文件级的十条流 |
相比文件级的关键变化:
| 流 | 文件级 | 多文件级 | 质变点 |
|---|---|---|---|
| 启动流 | 尚未形成 | 尚未形成(链接是编译期问题) | 无——启动流到多系统才形成 |
| 执行流 | 文件内调用图 | 跨文件调用链 | 调试需要跨文件上下文 |
| 数据流 | 文件内传递 | 指针跨文件传递 | 指针成为跨文件数据流的关键 |
| 状态流 | 局部 + 可能的全局 | 三种生命周期 | static/extern 区分了可见性 |
| 控制流 | 调用者间接控制 | 头文件 = 控制边界 | 封装的物理基础 |
| 错误流 | 底层吞掉 | 跨文件传播 | 传播路径变清晰 |
| 反馈流 | 不存在 | 线性回退(跳过/降级) | 第一次有实质内容 |
多文件级的核心概念:接口。 头文件定义了「能做什么」,源文件实现了「怎么做」。接口是控制流的边界、数据流的通道、错误流的传播路径。
当文件进一步增加,开始出现「分层」——有些文件负责入口,有些文件负责业务,有些文件负责底层能力——流的形态会发生更大的变化。下一篇看多层系统。
