01-单函数的十条流
单函数的十条流
一切从一个函数开始。在所有复杂的软件系统之前,程序只是若干条指令顺序执行。当这些指令被组织成一个函数,十条流就已经同时存在了——只是有几条还很微弱,甚至尚未诞生。这一篇用一个真实的小函数,把十条流全部展开。
一、从一条指令到一个函数
最简单的程序:
1 | printf("hello"); |
只有一条指令。没有分支,没有循环,没有函数调用。十条流在这里几乎全部退化成一个点——指令执行了,结果出现了,结束。
加一个判断:
1 | if (argc > 1) |
有了分支,程序不再只走一条路。控制流开始出现——程序在某个点要「选择」。
加一个循环:
1 | for (int i = 0; i < argc; i++) |
有了循环,同一段代码可以被执行多次。执行流不再是线性的。
把这些组织成一个函数:
1 | void print_args(int argc, char **argv) { |
函数是程序组织的最小单元。到了这一步,十条流已经全部就位——虽然有些还很薄,但它们都存在。
二、十条流的总览
先把十条流的定义列出来,后面逐条展开:
| # | 流 | 问的问题 | 函数级的形态 |
|---|---|---|---|
| 1 | 启动流 | 框架的启动编排 | 尚未形成(只有一个入口,被调用=执行开始) |
| 2 | 执行流 | 谁调用了谁 | 函数内部调用了哪些其他函数 |
| 3 | 数据流 | 数据经过谁被转换 | 输入参数 → 处理 → 输出 |
| 4 | 状态流 | 状态怎么变化 | 局部变量的变化过程 |
| 5 | 用户流程 | 用户操作什么、得到什么 | CLI:一类多条(每条命令一个实例) |
| 6 | 业务流 | 业务要做哪些步骤 | 函数完成的业务语义 |
| 7 | 功能流 | 内部逻辑怎么跑 | 循环、判断、赋值的具体步骤 |
| 8 | 控制流 | 程序走向由谁决定 | if/for/while/return 的分支结构 |
| 9 | 错误流 | 出错时怎么办 | NULL 检查、错误返回 |
| 10 | 反馈流 | 发现问题后回退到哪 | 函数级几乎不存在 |
先注意最后两行:反馈流在函数级几乎不存在——函数内部出了错,只能返回错误码,没有办法「回到上一步重新执行」。反馈流要等到多文件、多层系统时才会真正出现。
三、启动流:尚未形成——被调用是执行流的开始
启动流只有有框架才有——框架(有生命周期、要 start/stop 的组件/子系统)才需要「启动编排」。单函数阶段只有一个入口,没有框架,启动流还没有形成。
函数被调用不是「启动」,而是执行流的开始:
1 | print_args(3, argv); |
1 | 调用者传入 argc = 3, argv = {"cmd", "hello", "world"} |
「参数合法吗」这个问题属于执行流的起点(入口条件),不属于启动流:
1 | void print_args(int argc, char **argv) { |
如果 argc = -1,循环不会执行,函数安静地返回。这不是「启动失败」,是执行流带着非法条件开始。
单函数阶段的结论:启动流尚未形成——只有一个入口,被调用 = 执行开始。启动流要等到多系统阶段(框架化子系统出现)才真正形成。
四、执行流:内部调用了谁
1 | graph TD |
print_args 内部调用了两个函数:
1 | print_args(argc, argv) |
执行流是调用链——从当前函数出发,追踪它调用了谁、被谁调用。
在函数级,执行流通常是树状的:
1 | print_args |
函数级执行流的特点:我们只能看到「往下调了谁」,看不到「谁调了我」。调用者的信息在函数内部是不可见的——函数不知道自己是被 main 调用的还是被另一个函数调用的。
五、数据流:输入经过处理变成输出
1 | graph LR |
print_args 的数据流:
1 | argc (整数) |
函数级数据流的特点:输入是参数,输出是副作用(printf 写屏幕)或返回值。
这个函数没有返回值——它的「输出」是通过 printf 直接写到 stdout 的副作用。这意味着调用者无法拿到函数的计算结果,只能通过观察屏幕来获取。
数据流的一个重要观察:同一个输入 argv[i] 走了两条不同的路径:
1 | argv[i] |
分支处数据分流,这是数据流和控制流重叠的地方。
六、状态流:局部变量怎么变化
1 | stateDiagram-v2 |
print_args 的状态流——追踪局部变量的生命周期:
1 | i = 0 ← 初始化 |
函数级状态流的特点:状态生命周期 = 函数调用周期。函数返回后,所有局部变量消失。
1 | 函数调用开始 |
没有任何状态被持久化——下次调用 print_args 时,i 重新从 0 开始。
函数级状态流的核心特征:状态是临时的、函数返回即销毁。
七、用户流程:CLI 阶段是一类多条
用户流程(User Flow)= 用户的操作序列——用户操作什么、得到什么结果,不关心程序内部怎么执行。它的完整形态来自 GUI:用户逐个操作控件、每步验证符合预期。在 CLI 阶段它只是一类多条:程序是命令式的,用户输入一条命令就是一个实例,很多条命令只是同一个类型的不同实例。
单函数程序 = 一条命令:
1 | 用户打开终端 |
这个阶段用户流程的验证只有一次:输入命令 → 得到结果。函数内部怎么执行,用户不关心——用户流程和执行流在 main 处交界。
函数本身不知道「用户」是谁,它只知道有人传了参数进来。函数只是用户流程的一个片段,不是全部——多步操作、逐步验证的完整用户流程,要等 GUI 才成为一条真正的线。
八、业务流:这个函数做了什么
从业务角度看,print_args 做的事情是:
1 | 接收一组命令行参数 |
业务流关注的是「做什么」(What),不关心内部实现细节。
1 | 业务流:打印命令行参数 |
两者的区别:
1 | 业务流说:打印参数 |
业务流是设计时的视角(我想要什么),执行流是运行时的视角(代码实际怎么跑)。
九、功能流:函数内部的逻辑步骤
功能流是执行流的叶节点展开——执行流说「调用了 print_args」就停了,功能流把这个调用展开成内部的每一步:
1 | print_args(3, {"cmd", "hello", "world"}): |
功能流的图是流程图——只关心函数内部的判断、循环、赋值。
什么时候需要画功能流:函数超过 30 行、逻辑复杂、反复出 Bug、需要向别人解释。
十、控制流:程序走向由谁决定
1 | flowchart TD |
控制流关注的是:在每个分支点,程序往哪走?谁决定的?
print_args 的控制流:
1 | 循环条件: i < argc |
控制流的核心观察:函数内部的控制流由两样东西决定——参数和数据。函数自身不「选择」走哪条路,是数据告诉它走哪条。
但更深层的问题是:谁决定了 argc 和 argv 的值? 函数自己不知道。控制权在函数外面——在调用者那里。
1 | 调用者决定 argc, argv ← 控制权在外部 |
函数级控制流的核心特征:函数内部的控制权由数据掌握,函数外部的控制权由调用者掌握。
十一、错误流:出错时怎么办
print_args 的错误处理:
1 | if (argv[i] == NULL) |
它选择的策略是容忍——遇到 NULL 不崩溃,打印一个标记继续。
但这个函数有一个未处理的错误:argc 为负数时的行为未定义(循环条件 i < argc 在 i = 0, argc = -1 时为假,直接跳过——碰巧不会崩溃,但不是有意设计)。
函数级错误流的特点:
1 | 错误发生在函数内部 |
函数级错误流的核心限制:函数能做的错误处理方式有限——最多返回错误码或打印日志,无法「回到上一步重试」。
十二、反馈流:几乎不存在
反馈流的定义:发现问题后,回到问题产生的那一步,重新执行后续流程。
在函数内部,这几乎不可能。print_args 发现 argv[i] 是 NULL 后,它能做的只是打印 (null) 然后继续——它无法回到调用者说「你传了一个 NULL,换一个再调我」。
1 | 函数内部发现错误 |
反馈流在函数级几乎不存在,是函数作为最小组织单元的结构性限制。 函数是一个「只能前进不能后退」的执行单元。
反馈流要等到多层系统出现时才会真正诞生——那时「上层」可以接住「下层」的错误,回到合适的步骤重试。
十三、十条流在函数级的全景
1 | 函数级的十条流 |
十条流的成熟度在函数级差异很大:
- 成熟的:执行流、数据流、控制流、功能流——函数内部最容易观察到
- 薄的:用户流程、状态流、业务流、错误流——存在但很简单(用户流程在 CLI 阶段只是一类多条)
- 不存在的:反馈流——函数没有「回退重试」的能力
这正是函数作为最小组织单元的特点:它解决了一个具体的计算问题,但对自身的上下游、错误恢复、用户感知都一无所知。
当程序从一个函数增长到多个函数、多个文件、多个层级时,这些流会逐步「浮现」——有的变厚,有的从无到有。下一篇看它们在单文件多函数时长成了什么样。
