01-控制流
控制流——程序走向由谁决定
控制流问的是一个最朴素的问题:程序在每一个分支点往哪走?由谁决定? 这个问题看似简单,却随着程序组织的演化不断变换答案。从单函数到多系统,控制权像接力棒一样,从「数据」手里传到「调用者」手里,再传到「接口」手里,最终分成「框架化 / 接口化」两条路。本文追踪这条流从头到尾的演化。
一、起点:一个分支点
程序一旦出现判断,控制流就诞生了:
1 | if (argv[i] == NULL) |
在这个点,程序要「选择」。控制流关心的就是:这个选择由谁做出?
答案是:数据。
1 | argv[i] == NULL → 走第一条 |
不是代码自己选了路,是数据告诉代码走哪条路。这是控制流最原始、最本质的形态。
二、单函数:数据决定,调用者掌握参数
在一个函数内部,控制流由两样东西决定:
1 | 函数内部的控制流由「参数 + 数据」决定 |
以 print_args 为例:
1 | void print_args(int argc, char **argv) { |
1 | 循环几次? 由 argc 决定(调用者传入) |
函数级的核心特征:函数内部的控制权由数据掌握,函数外部的控制权由调用者掌握。
这里埋着一个深刻的伏笔:函数自己不知道 argc、argv 从哪来——真正的控制权在函数外面。函数只是一个「被数据驱动」的执行单元。
三、单文件多函数:控制权在函数间传递
当多个函数互相调用时,控制流增加了一个新维度:
1 | 调用者决定了被调用者的参数,间接控制了被调用者的分支。 |
1 | main 决定传 argc-1 给 print_args |
控制流形成了传递链:
1 | main 的 argc |
关键观察:format_arg 无法拒绝被调用。 它没有「判断自己是否该被调用」的能力——控制权完全在调用者手里。
1 | 库被调用(库没有控制权) |
这就是「框架 vs 库」区别的雏形——它第一次在控制流这个维度露出苗头。
四、多文件:接口成为控制流的边界
拆成多个文件后,最关键的变化不是「文件变多」,而是头文件(接口)出现了。
1 | main.c 通过 print.h 能看到 print_args |
头文件 = 控制流的边界。
1 | main.c 能控制:print_args(直接) |
控制流出现了分层:
1 | main.c → 控制 print_args (直接下级) |
每一层只控制自己的直接下级,不知道更深层发生了什么。
这就是「封装」在控制流上的体现:每一层只暴露给上层自己愿意暴露的部分。接口不仅分隔了「能做什么」和「怎么做」,也划定了「谁有权控制谁」。
五、多层:组装权决定控制权
分层之后,控制流发生了一次质变——它不再只是「调用链」,而是「谁拥有组装权」的问题。
1 | main 拥有组装权: |
控制权按层分配:
1 | 入口层 掌握程序的控制流(决定调用谁、传什么参数) |
多层控制流的核心变化:控制权按层分配。入口层掌握全局控制流,业务层和基础设施层是被驱动的。
但出现了一个例外——它预告了下一个阶段:
1 | 基础设施层中的某些子系统可能自己掌握控制流。 |
这就是「框架化子系统」的雏形:有的组件被驱动,有的组件自己跑。
六、多系统:框架化 vs 接口化,控制流最终分流
多系统阶段,控制流完成最终成型,明确分成两条路:
1 | graph LR |
1 | 框架化子系统(log_system): |
控制流的最终分配:
1 | main: 全局控制流(组装、启动、关闭) |
多系统控制流的核心变化:控制流开始在子系统之间分配。框架化子系统有独立控制流,接口化子系统没有。
这是「库的两种形态」在控制流维度的完整体现:
1 | 框架化库 = 有状态 + 有生命周期 + 有自己的控制流 |
七、控制流的五阶段演化总表
1 | 控制权由谁掌握 新出现的机制 |
一条主线贯穿始终:
1 | 控制流的问题 = 「控制权在谁手里」 |
八、控制流与其他线的关系
1 | 接口(接口形态演化) 控制流决定接口形态:有控制流→类,无控制流→函数 |
一句话总结:控制流不是「代码怎么跳」,而是「权力怎么分配」——它追踪的是从一条分支指令到一整个系统里,谁说了算的权力变迁史。
