🔷 第九章 面向对象编程基础
📄 一句话概括

面向过程编程(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 岁可能是李四的。

这就是"数据敞开"最典型的翻车方式:不报错的错。 面向对象要解决的,正是这个——把"这条记录"整体保护起来,让交换必须整条进行,改字段的时候外面不用跟着改。

七、常见误区

  1. "面向过程就是写得烂的代码"——完全不是。它是一套成熟、有效、被大量工业级系统使用的思路。烂代码的问题不在思路,在"数据敞开又没有约定"。
  2. "全局变量方便,就都用全局变量"——本节 Demo 里的"改动扩散"就是全局数据的代价。全局数据意味着所有函数都是它的知情者;知情者越多,改动越贵。
  3. "函数多了就是面向对象"——不是。把函数拆得很细,仍然可以是标准的面向过程(数据还是敞开的)。面向对象的分界线是"数据有没有和方法绑在一起并被保护"。
  4. "面向过程不能写大程序"——能,但要付出代价:靠严格的分层约定、命名规范、文档来约束"谁知道什么"。面向对象是用语言机制(private)替你把这件事管起来,所以大程序更省心。
自测三问(点开对答案) 1. 面向过程编程的"组织中心"是什么?——**步骤(函数)**。程序像一张作业指导书,按顺序推进。 2. 它最大的软肋是什么?具体表现为什么?——**数据和函数两分离、数据敞开**;表现是**一处数据结构的改动会沿着"所有知道它的函数"扩散**,而且有些错误(比如排序时漏换某个字段)**编译不报错、运行时才出乱子**。 3. 什么样的任务适合面向过程?——流程清晰、变化少、规模小或对性能很敏感的场景:**算法题、脚本小工具、系统底层与嵌入式、一次性数据处理**。

相关笔记