单文件多函数的十条流

当一个文件里不再只有一个函数,而是有若干个函数互相调用时,十条流开始发生变化。执行流不再是单棵树,而是多棵树组成的调用图;错误流第一次有了「传播」的可能;反馈流仍然很弱,但已经萌芽。


一、从一个函数到多个函数

上一篇的 print_args 只有一个函数。现在把它拆成多个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// file: args.c

// 函数 1:格式化单个参数
void format_arg(char *buf, size_t size, int index, const char *value) {
if (value == NULL)
snprintf(buf, size, "arg[%d] = (null)", index);
else
snprintf(buf, size, "arg[%d] = %s", index, value);
}

// 函数 2:打印所有参数
void print_args(int argc, char **argv) {
char buf[256];
for (int i = 0; i < argc; i++) {
format_arg(buf, sizeof(buf), i, argv[i]);
printf("%s\n", buf);
}
}

// 函数 3:入口
int main(int argc, char **argv) {
print_args(argc - 1, argv + 1);
return 0;
}

现在一个文件里有三个函数。它们之间有了调用关系——main 调用 print_argsprint_args 调用 format_arg

这个变化看起来很小,但十条流全部开始发生变化。


二、启动流:尚未形成——main 是唯一入口

启动流只有有框架才有——框架(有生命周期、要 start/stop 的子系统)才需要「启动编排」。单文件多函数阶段只有一个入口 main,没有框架,启动流还没有形成。

main 开始执行后按调用顺序进入每个函数——这是执行流,不是启动流:

1
2
3
4
5
6
7
8
9
10
main() 被操作系统调用
↓ 获得 argc, argv
↓ 调用 print_args(argc-1, argv+1) ← 执行流,不是启动流
↓ print_args 获得参数
↓ 调用 format_arg(buf, 256, i, argv[i])
↓ format_arg 执行格式化
↓ format_arg 返回
↓ print_args 继续循环
↓ print_args 返回
main 返回

「谁先谁后」在这里是执行顺序(调用发生的先后),不是启动顺序:

1
2
3
4
操作系统
→ main ← 唯一的入口
→ print_args ← 执行进入
→ format_arg ← 执行进入

单文件阶段只有一个入口,没有框架,所以没有启动流。 声明/定义顺序(C 语言要求函数在调用前声明)是编译期问题,与运行期启动无关。

启动流要等到多系统阶段——框架化子系统出现(有生命周期、start/stop)——才真正形成。


三、执行流:从单棵树到调用图

1
2
3
4
5
6
7
8
9
10
graph TD
A["main()"] --> B["print_args(argc-1, argv+1)"]
B -->|"循环"| C["format_arg(buf, 256, i, argv[i])"]
C -->|"NULL"| D["snprintf('(null)')"]
C -->|"非NULL"| E["snprintf('%s')"]
B --> F["printf('%s\n', buf)"]

style A fill:#4a9eff,color:#fff
style B fill:#4a9eff,color:#fff
style C fill:#ff9f4a,color:#fff

函数级的执行流是一棵树——从当前函数出发往下看。文件级的执行流变成了调用图——多个函数之间的调用关系网络:

1
2
3
4
5
main
└→ print_args
└→ format_arg
├→ snprintf (value == NULL 分支)
└→ snprintf (value != NULL 分支)

执行流的新变化:可以看到「从哪来到哪去」的完整路径了

1
2
3
4
5
6
7
8
9
10
一条完整的执行流(argv[i] == NULL 时):

main()
→ print_args(2, {"hello", NULL})
→ format_arg(buf, 256, 1, NULL)
→ snprintf(buf, 256, "arg[%d] = (null)", 1)
← 返回到 print_args
→ printf("%s\n", buf)
← 返回到 print_args
→ (循环继续)

执行流在文件级的关键新特征:调用和返回都可见了。函数级只能看到「往下调了谁」,文件级可以看到「调了谁、谁返回了、返回后继续执行了什么」。


四、数据流:参数传递链

1
2
3
4
5
6
7
8
9
graph LR
A["main 的 argc,argv"] -->|"传参 argc-1,argv+1"| B["print_args"]
B -->|"取值 argv[i]"| C["format_arg"]
C -->|"snprintf 写入"| D["buf[256]"]
D -->|"printf 读取"| E["屏幕输出"]

style A fill:#4a9eff,color:#fff
style D fill:#ff9f4a,color:#fff
style E fill:#4caf50,color:#fff

数据在函数之间流动的路径:

1
2
3
4
5
6
7
8
9
10
11
main 的 argc, argv
↓ 传参:argc-1, argv+1
print_args 的 argc, argv
↓ 取值:argv[i]
↓ 传参:buf, 256, i, argv[i]
format_arg 的 index, value
↓ 处理:snprintf 写入 buf
↓ 返回:buf 被 print_args 使用
print_args 的 buf
↓ 传参:buf
printf 输出到屏幕

数据流在文件级的关键新特征:数据经过了多个函数的转换链。每个函数接收数据、处理数据、传出数据。

1
2
3
4
5
原始数据: argv[1] = "hello"
↓ format_arg: 转换成 "arg[0] = hello"
↓ print_args: 传给 printf
↓ printf: 加换行符
最终输出: "arg[0] = hello\n"

更重要的是:数据流的瓶颈可以被看到了buf[256] 是一个固定大小的缓冲区——如果某个参数值超过 254 个字符,snprintf 会截断。这是数据流的一个约束点,在函数级看不到(因为函数内部只管自己的参数),在文件级可以看到(因为可以看到数据从一个函数传到另一个函数时经过了什么)。


五、状态流:从局部变量到函数间状态

函数级的状态流是局部变量的变化。文件级的状态流增加了新的维度:函数之间通过参数和全局变量共享的状态

这个例子没有全局变量,但有参数传递的「半持久状态」:

1
2
3
4
5
buf[256] 在 print_args 中创建
↓ format_arg 写入内容
↓ print_args 用 printf 输出
↓ 下一次循环:format_arg 覆盖写入新内容
↓ 循环结束:buf 随 print_args 返回而销毁

buf 是一个在循环中被反复覆盖的状态——它的生命周期跨越了多次 format_arg 调用。

如果加一个全局变量:

1
2
3
4
5
6
static int call_count = 0;   // 文件级状态

void format_arg(...) {
call_count++; // 每次调用都修改它
...
}

call_count 就是文件级状态——它的生命周期是整个程序运行期间,不是单次函数调用。

文件级状态流的核心变化:出现了跨函数生命周期的状态。


六、用户流程:一类多条——每条命令一个实例

CLI 阶段(单文件多函数)的用户流程仍然只是一类多条:程序支持几条命令/几组参数,用户输入一条就是一个实例,多条命令是同一个类型的不同实例。

1
2
3
4
5
6
7
8
用户输入 ./args hello world          ← 实例 1
↓ main 获得 argc/argv(用户流程和执行流的交界)
print_args → format_arg → printf ← 纯执行流
↓ 用户看到输出 ← 实例 1 结束:符合预期

用户输入 ./args --help ← 实例 2(同一类型,另一个实例)
↓ main 获得参数
... ← 执行流不同,用户流程相同

每个实例都是「输入命令 → 得到结果」的直线,验证只有一次:结果是否符合预期。用户不关心 print_args 内部怎么格式化——那是执行流。

文件级用户流程的特征:只有 main 感知用户(通过 argc/argv),main 以下的函数对用户完全无感知——因为 CLI 的用户流程止步于 main,不进函数内部。多步操作、逐步验证的完整用户流程(逐个控件)要等 GUI 才出现。


七、业务流:从单函数做什么到协作完成什么

单函数级的业务流是「这个函数做了什么」。文件级的业务流变成了「几个函数协作完成了什么」:

1
2
3
4
5
6
7
8
9
10
业务流:
接收命令行参数
→ 逐个格式化参数
→ 打印格式化结果

对应执行流:
main 接收 argc/argv
→ print_args 循环调用 format_arg
→ format_arg 格式化每个参数
→ print_args 用 printf 输出

业务流和执行流的区别更明显了:

1
2
业务流说:接收 → 格式化 → 打印(三个步骤)
执行流说:main → print_args → format_arg → snprintf → printf(五个函数调用)

业务流不关心有几个函数、怎么调用——它只关心「做了哪些事」。执行流不关心业务语义——它只关心「谁调用了谁」。


八、功能流:仍然是函数内部的事

功能流的定义没有变——它仍然是单个函数内部的逻辑步骤format_arg 的功能流:

1
2
3
4
format_arg(buf, 256, 1, NULL):
value == NULL? → 是
snprintf(buf, 256, "arg[%d] = (null)", 1)
返回

功能流在文件级没有本质变化。它始终是最细粒度的分析维度——函数内部的 if/else/for/while。

功能流是唯一不随组织规模变化的流。


九、控制流:从数据决定到函数间传递

函数级的控制流是「数据决定分支方向」。文件级的控制流增加了一个新维度:调用者决定了被调用者的参数,间接控制了被调用者的分支

1
2
3
4
5
main 决定传 argc-1 给 print_args
↓ 这决定了 print_args 循环几次

print_args 决定传 argv[i] 给 format_arg
↓ 这决定了 format_arg 走 NULL 分支还是非 NULL 分支

控制流的传递链

1
2
3
4
main 的 argc
→ 决定 print_args 循环几次
→ 决定 format_arg 被调用几次
→ 每次的 argv[i] 决定 format_arg 的分支

文件级控制流的核心变化:控制权开始在函数之间传递。main 通过参数间接控制了下游函数的行为。

但有一个关键观察:format_arg 无法拒绝被调用——它没有「判断自己是否该被调用」的能力。控制权完全在调用者手里。这就是「框架 vs 库」区别的雏形:库被调用,框架调用你。


十、错误流:第一次有了传播

1
2
3
4
5
6
7
8
9
10
graph TD
A["format_arg 发现 argv[i]==NULL"] -->|"打印 (null)"| B["返回正常"]
A2["format_arg snprintf 返回 -1"] -->|"返回错误码"| C["print_args 检查返回值"]
C -->|"!= 0"| D["打印错误信息, 跳过"]
C -->|"== 0"| E["printf 输出 buf"]
B --> F["print_args 继续循环"]

style A fill:#ff9f4a,color:#fff
style A2 fill:#f44,color:#fff
style D fill:#ff9f4a,color:#fff

函数级的错误流是「函数内部出错怎么办」。文件级的错误流增加了:错误可以在函数之间传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void format_arg(char *buf, size_t size, int index, const char *value) {
if (value == NULL) {
snprintf(buf, size, "arg[%d] = (null)", index);
return; // ← 容忍错误,继续
}
// ...
}

void print_args(int argc, char **argv) {
char buf[256];
for (int i = 0; i < argc; i++) {
format_arg(buf, sizeof(buf), i, argv[i]);
// ← format_arg 返回后,print_args 继续
// 它不知道 format_arg 内部发生了什么
printf("%s\n", buf);
}
}

错误流的传播模式:format_arg 遇到 NULL → 打印 (null) → 返回 → print_args 继续。错误被 format_arg 吞掉了,print_args 完全不知道发生过异常。

这是文件级错误流的典型模式:底层函数吞掉错误,上层函数浑然不知

1
2
3
format_arg: 发现 NULL → 处理了(打印 (null))→ 返回正常
print_args: 不知道 format_arg 遇到了 NULL
main: 不知道 print_args 内部发生了什么

文件级错误流的核心特征:错误传播是单向的——从被调用者到调用者。调用者可以选择忽略。


十一、反馈流:仍然很弱

反馈流在文件级仍然很弱。print_args 无法说「format_arg 处理失败了,换个值再调一次」。

但有一个微弱的萌芽:如果 format_arg 返回一个错误码,print_args 可以决定重试或跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int format_arg(char *buf, size_t size, int index, const char *value) {
if (value == NULL) {
snprintf(buf, size, "arg[%d] = (null)", index);
return 0; // 成功
}
int ret = snprintf(buf, size, "arg[%d] = %s", index, value);
if (ret < 0) return -1; // 格式化失败
return 0;
}

void print_args(int argc, char **argv) {
char buf[256];
for (int i = 0; i < argc; i++) {
if (format_arg(buf, sizeof(buf), i, argv[i]) != 0) {
printf("error formatting arg %d\n", i);
continue; // ← 发现错误,跳过这一个,继续下一个
}
printf("%s\n", buf);
}
}

这比函数级强了一点——print_args 可以根据 format_arg 的返回值决定下一步。但本质上仍然是线性前进:出错就跳过,不会回到 format_arg 说「换一种方式再试」。

反馈流在文件级的萌芽:调用者可以根据返回值决定「跳过」或「降级处理」,但仍然无法「回退重试」。


十二、十条流在文件级的全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                  文件级的十条流

┌──────────────────────────────────────────────────────┐
│ 启动流 尚未形成(只有 main 一个入口) │
│ 执行流 调用图:main→print_args→format_arg │
│ 数据流 argc/argv → buf → printf → 屏幕 │
│ 状态流 buf 跨循环复用;无全局状态 │
│ 用户流程 一类多条:每条命令一个实例 │
│ 业务流 接收 → 格式化 → 打印 │
│ 功能流 每个函数内部的逻辑步骤(与函数级相同) │
│ 控制流 main 通过参数间接控制下游函数分支 │
│ 错误流 底层吞掉错误,上层浑然不知 │
│ 反馈流 萌芽:返回值可以触发跳过/降级 │
└──────────────────────────────────────────────────────┘

相比函数级的变化:

函数级 文件级 变化
启动流 尚未形成 尚未形成(只有 main 一个入口) 无——调用顺序是执行流,启动流到多系统才形成
执行流 单棵树 调用图 可以看到调用和返回
数据流 输入→输出 多函数转换链 可以看到瓶颈和约束点
状态流 局部变量 局部+跨调用状态 出现了跨函数生命周期的状态
用户流程 一类多条 一类多条(每条命令一个实例) 无实质变化——GUI 才是真正的线
业务流 单函数做什么 多函数协作做什么 业务和实现开始分离
功能流 函数内部逻辑 相同 不随规模变化
控制流 数据决定分支 调用者间接控制 控制权开始在函数间传递
错误流 函数内部处理 错误开始传播 底层吞掉,上层不知
反馈流 不存在 萌芽 返回值触发跳过/降级

文件级的核心变化:函数之间有了调用关系,流开始在函数之间流动。 但所有函数都在同一个文件里,共享同一个编译单元——还没有「边界」的概念。

当文件开始拆分、函数分布在不同文件中时,流的形态会再发生一次质变——下一篇看多文件阶段。