面向过程编程(POP)= 以"步骤"为中心组织程序:先做什么、再做什么、最后做什么,一条流水线走到底。数据是素材,函数是工序——两者分开放。它直白、高效、贴近机器,是算法题、脚本、系统底层的正路;软肋只有一个——程序长大后,一处数据格式的改动会牵动一大片函数。
上一节(9.2)把两种方式并排比过,本节单独把面向过程剖开看——因为前面八章写的代码全是面向过程,回头看自己的"旧写法",才能明白面向对象到底"多包了一层什么"。重点是记住它的那条软肋(改动扩散),因为下一节 9.2.2 要讲的面向对象,恰好就是冲着这条软肋来的。
一、核心画面:一条流水线
面向过程的世界观极其简单:程序 = 数据 + 函数,一条主线按顺序推进。
写一个"把学生成绩算平均分":
int scores[30]; // 数据:一堆分数
int n = 30;
int sumAll() { ... } // 工序①:求和
double average() { ... } // 工序②:除以人数
void printOne(...) { } // 工序③:打印
main 里的主线像一张作业指导书:
int main() {
readScores(); // ① 读数
double avg = average(); // ② 算平均
printResult(avg); // ③ 输出
return 0;
}
这就是最朴素的"面向过程"——像做菜的步骤:备料 → 下锅 → 调味 → 出锅。每一步是一个函数,谁在前谁在后,一看就懂。
二、它的三个优势(别小看)
| 优势 | 说明 |
|---|---|
| 直白好懂 | 读代码像读步骤,顺着一行行往下看就行,不用先搞清楚"有哪些对象、谁在调用谁" |
| 高效 | 少一层抽象,写起来快、跑起来也快(没有对象带来的额外开销) |
| 贴近机器 | 这一节的思路跟 CPU "取指令—执行—取下一条"的方式高度一致,所以系统底层、驱动、嵌入式几乎全是这个思路 |
对零基础的人来说,这是最自然的思考方式——我们平时说话就是"我先做这个,然后做那个"。所以前面八章先教面向过程,是对的。
三、"数据 + 函数"两分离,到底什么样
面向过程最标志性的特征:数据和操作数据的函数,是两个分开的阵营。
// ===== 阵营一:数据(全局敞开) =====
char names[30][20];
int ages[30];
double scores[30];
// ===== 阵营二:函数(工序) =====
void addStudent(int i, const char *n, int a, double s) { ... }
void printStudent(int i) { ... }
double classAverage() { ... }
void sortByScore() { ... }
看着挺整齐,但有一个隐藏的约定:
"函数们必须知道数据长什么样" —— 比如
printStudent必须知道分数存在scores[i]里、姓名存在names[i]里。这个约定没有写在任何地方,只存在于写代码的人脑子里。
而这个"只存在于脑子里的约定",就是将来出乱子的地方。
四、软肋:一改牵一身
接着上面的例子。需求变了:学生要加"手机号",于是:
char phones[30][15]; // 新增一列数据
看起来只加了一行,但所有"知道学生数据长什么样"的函数都要跟着动:
| 函数 | 为什么必须改 |
|---|---|
addStudent() |
要多填一个手机号参数 |
printStudent() |
要决定要不要打印手机号 |
sortByScore() |
交换时姓名、年龄、分数、手机号必须整条一起换(漏了就错配) |
| 任何"遍历学生"的地方 | 要检查有没有漏掉新字段的逻辑 |
这就是"数据和方法各站一队"的代价:数据是公开的,每个函数都是它的"知情者",知情者越多,改一处就要通知一大片。
一句话总结这条软肋:数据的变化,会沿着"所有知道这个数据结构的函数"扩散出去。
再想一个更严重的:如果某天有人绕过 addStudent(),直接写 scores[5] = 999;——没有任何机制拦得住。数据敞开着,规则就只是"君子协定"。
五、什么时候该用它
不要因为学了面向对象就抛弃面向过程。 下面这些场景,面向过程往往就是最优解:
| 场景 | 为什么面向过程合适 |
|---|---|
| 算法题 / 数学计算 | 输入 → 处理 → 输出,流程天然清晰;包成类是画蛇添足 |
| 脚本 / 小工具 | 几十行干一件事,用完就扔 |
| 系统底层 / 驱动 / 嵌入式 | 要贴硬件、要可控、要极致效率 |
| 一次性的数据处理 | 流程固定、不打算长期维护 |
判据:"这个程序的流程会不会变?规模会不会涨?" 都不会 → 面向过程,痛快直接。
六、动手 Demo:完整体验一次"改动扩散"
写一个小程序,然后故意改一次需求,亲身感受那条软肋。
第一版(只有姓名和分数):
#include <iostream>
#include <cstring>
using namespace std;
const int N = 3;
// —— 阵营一:数据 ——
char names[N][20];
double scores[N];
// —— 阵营二:函数(都得"知道"上面数据的排布) ——
void setStudent(int i, const char *n, double s) {
strcpy(names[i], n);
scores[i] = s;
}
void printAll() {
for (int i = 0; i < N; i++)
cout << names[i] << "\t" << scores[i] << endl;
}
void sortByScore() { // 冒泡:注意交换要成对进行
for (int i = 0; i < N - 1; i++)
for (int j = 0; j < N - 1 - i; j++)
if (scores[j] < scores[j+1]) {
double ts = scores[j]; scores[j] = scores[j+1]; scores[j+1] = ts;
char tn[20]; strcpy(tn, names[j]);
strcpy(names[j], names[j+1]); strcpy(names[j+1], tn);
// ⚠️ 姓名和分数必须一起换,漏一个就错配
}
}
int main() {
setStudent(0, "张三", 92.5);
setStudent(1, "李四", 85.0);
setStudent(2, "王五", 78.5);
sortByScore();
printAll();
return 0;
}
看到什么算成功:输出按分数从高到低排好的三行。
现在做"改动实验"——加"年龄"字段:在数据阵营加 int ages[N];,然后逐个函数去改:setStudent 多一个参数、printAll 多打一列、sortByScore 交换时再加一组 ages 的交换。
30 秒实验(关键):上面 sortByScore 里的交换代码,故意漏掉"年龄"那一组交换(setStudent 里年龄照填)。程序照样编译通过、照样能跑,但排序后你会看到——姓名和分数是配套的,年龄却留在了原位。比如"张三 92.5 18岁"排到了第一行,但 18 岁可能是李四的。
这就是"数据敞开"最典型的翻车方式:不报错的错。 面向对象要解决的,正是这个——把"这条记录"整体保护起来,让交换必须整条进行,改字段的时候外面不用跟着改。
七、常见误区
- "面向过程就是写得烂的代码"——完全不是。它是一套成熟、有效、被大量工业级系统使用的思路。烂代码的问题不在思路,在"数据敞开又没有约定"。
- "全局变量方便,就都用全局变量"——本节 Demo 里的"改动扩散"就是全局数据的代价。全局数据意味着所有函数都是它的知情者;知情者越多,改动越贵。
- "函数多了就是面向对象"——不是。把函数拆得很细,仍然可以是标准的面向过程(数据还是敞开的)。面向对象的分界线是"数据有没有和方法绑在一起并被保护"。
- "面向过程不能写大程序"——能,但要付出代价:靠严格的分层约定、命名规范、文档来约束"谁知道什么"。面向对象是用语言机制(
private)替你把这件事管起来,所以大程序更省心。
自测三问(点开对答案)
1. 面向过程编程的"组织中心"是什么?——**步骤(函数)**。程序像一张作业指导书,按顺序推进。 2. 它最大的软肋是什么?具体表现为什么?——**数据和函数两分离、数据敞开**;表现是**一处数据结构的改动会沿着"所有知道它的函数"扩散**,而且有些错误(比如排序时漏换某个字段)**编译不报错、运行时才出乱子**。 3. 什么样的任务适合面向过程?——流程清晰、变化少、规模小或对性能很敏感的场景:**算法题、脚本小工具、系统底层与嵌入式、一次性数据处理**。- ☐ 9.2.2 面向对象编程(下一节)
相关笔记
- 面向对象编程与面向过程编程 —— 上一节:两种方式总对比
- 面向对象编程 —— 下一节:冲着这条软肋来的
- 函数概述 —— 函数:面向过程的"工序"
- 结构体数组 —— 用结构体数组写"表格"时,改动扩散会更明显
- 🔷 C++基础 · 索引 —— 全课路线图