面向对象编程(OOP)= 以"对象"为中心组织程序:先找出系统里有哪几个角色,把每个角色的数据(属性)和本事(方法)装进同一个"壳"里,再让这些角色互相配合。它最实在的好处是:改动被锁在对象内部——谁的需求变了就改谁,外面用不着跟着动。"类"是图纸,"对象"是按图纸造出来的实物。
上一节 9.2.1 讲清了面向过程的那条软肋(数据敞开 → 改动扩散)。本节正面回应它:对象把数据和方法打包,并给数据上一把锁。用的还是第八章的结构体打底——结构体是"只打包数据"的半成品,对象是"连本事一起打包、还上锁"的成品。本节里出现的 class 与 private,会在下一章正式展开语法,这里只要看懂"它在解决什么问题"就够了。
一、思考顺序的翻转:从"动词"回到"名词"
对比一下面对同一个需求,两种思路最先想到的东西:
| 面向过程 | 面向对象 | |
|---|---|---|
| 先想什么 | 要干哪些事(动词) | 有哪些角色(名词) |
| 借书 | ①检查证件 ②查在架 ③登记 ④改状态 ⑤打凭条 | 读者、图书、图书馆 |
| 打游戏 | ①读按键 ②算位置 ③画画面 ④判断碰撞 | 玩家、敌人、子弹、地图 |
| 网店下单 | ①验库存 ②扣款 ③生成订单 ④发货 | 用户、商品、购物车、订单、库存 |
面向对象的第一步,是把需求里的"名词"圈出来。 圈出来的这些名词,就是"对象"的候选。
为什么"先找名词"更扛改?因为名词(角色)比动词(流程)稳定。"借书"的流程可能从"手写登记"换成"扫二维码"(动词全变了),但"读者、图书、图书馆"这三个角色不会变。围着稳定的东西组织程序,程序自然更稳。
二、对象 = 属性 + 方法
一个对象自己带着两样东西:
| 叫什么 | 是什么 | 图书的例子 | |
|---|---|---|---|
| 数据 | 属性(成员变量) | 这个对象"现在是什么状态" | 书名、作者、ISBN、是否在架上 |
| 行为 | 方法(成员函数) | 这个对象"能做什么" | 借出、归还、查位置 |
面向对象真正的关键动作不是"打包",而是"分派职责":
"这件事该谁干?" —— 只要一个动作主要是在改自己的数据,这个动作就应该写在自己身上。
"借出"这件事改的是书自己的状态(在架 → 已借出),所以 borrow() 写在书身上。这样一来:
- 别人要用,就喊一声(调
book.borrow()),不用知道它内部怎么改状态; - 以后要改"借出"的规则(比如加个"借期 30 天"),只改书自己的方法,喊它的人一行都不用动。
这就是"改动范围收敛"的来源:因为数据和方法在同一个壳里,改内部的实现不会漏到外面去。
三、类与对象:图纸 vs 实物
这是 OOP 最基础的一对概念,务必分清:
| 类(class) | 对象(object / 实例) | |
|---|---|---|
| 是什么 | 图纸 / 模板 / 定义 | 按图纸造出来的实物 |
| 职业 | 只描述"有什么属性和方法" | 真正占有内存、有具体状态 |
| 数量 | 一般只写一份 | 想造多少造多少 |
| 例子 | "图书"这个概念 | 《零基础学C++》这本具体的书 |
| 类比 | 月饼模子 | 压出来的每个月饼 |
| 类比② | 建筑设计图 | 照图盖出的每一套房 |
class Book { // 图纸:凡是"图书",都有书名和"是否在架"
// ...属性与方法(下一章详细讲写法)
};
Book b1; // 按图纸造出一个对象
Book b2; // 再造一个;两个对象各自独立
一句话:类是"概念",对象是"实体"。 第八章的结构体也是这个套路——
struct Student是图纸,Student s1;才是实物(8.1 里那句"定义只是画图纸"在这里得到了呼应)。
四、对象之间靠"消息"配合
对象不是孤岛,它们互相喊话。这个"喊话"在代码里就是调用对方的方法:
读者.borrow(图书) → 读者向图书发出"借出"的请求
↓
图书.borrow() 自己做:
检查是否在架上 → 改自己的状态 → 回话"成功 / 失败"
↓
图书馆.record(借阅记录) → 图书馆登记一笔
要点:
- 发消息的人只关心"能不能借到",不关心书内部怎么改状态(这就是"封装"的好处)。
- 对象只管自己的事:图书不用知道"读者证怎么验",图书馆不用知道"书怎么改状态"。各守本分,互相调用,整体流程自然跑起来。
- 这套配合关系,正是 9.3 要用图画出来的东西(UML 类图 + 时序图)。
五、"谁变了就改谁"
把 9.2.1 那张"改动账"翻过来看新写法:
| 需求变化 | 面向对象怎么改 |
|---|---|
| 学生加"手机号" | 只在 Student 类内部加一个属性;外面调用的人不受影响 |
| 新增"研究生" | 用继承:研究生"是一种"学生,只写多出来的部分(9.2.3) |
| "借出"要加借期 | 只改 Book::borrow() 一个方法的内部;调用点一行不动 |
| 有人想乱改余额 | private 直接挡住,编译期就报错 |
这就是面向对象买到的那个东西:把"变化"关进一个盒子里。 代价是——你得先想清楚"盒子里该放什么"(这就是 9.3 建模要干的活)。
六、动手 Demo:把"账户"封装起来
先用第八章的结构体 + 函数拼出一个"半成品对象",看清它还差什么:
#include <iostream>
using namespace std;
struct Account {
double balance; // 数据(还敞着)
};
// 方法写在结构体外面,"和别的函数挤在一起"
void deposit(Account *a, double v) { a->balance += v; }
void withdraw(Account *a, double v) {
if (v > a->balance) { cout << "余额不足" << endl; return; }
a->balance -= v;
}
void show(const Account *a) { cout << "余额:" << a->balance << endl; }
int main() {
Account acc;
acc.balance = 100;
deposit(&acc, 50);
withdraw(&acc, 30);
show(&acc); // 余额:120
acc.balance = -9999; // ⚠️ 直接从外面捅进去,没人拦
show(&acc); // 余额:-9999
return 0;
}
看到什么算成功:最后两行打出 余额:-9999——数据敞开的后果。
现在对比"真正的对象写法"(下一章的语法,先看效果):
class Account {
private: // ← 钥匙在这里
double balance;
public:
Account() : balance(0) {} // 出生时余额为 0
void deposit(double v) { balance += v; }
void withdraw(double v) {
if (v > balance) { cout << "余额不足" << endl; return; }
balance -= v;
}
void show() const { cout << "余额:" << balance << endl; }
};
int main() {
Account acc;
acc.deposit(100);
acc.withdraw(150); // 余额不足 → 被挡
acc.show(); // 余额:100
// acc.balance = -9999; // ❌ 编译错误:private
return 0;
}
三处关键差别:
| 结构体 + 函数版 | 类版 | |
|---|---|---|
| 数据 | 敞开(acc.balance 谁都能写) |
锁起来(private) |
| 方法在哪 | 散在外面,靠命名(deposit)暗示"是给 Account 用的" |
写在类里面,一看就知道属于谁 |
| 乱改的后果 | 运行时才出问题(余额成了负数) | 编译期就报错 |
30 秒实验:把类版里 acc.balance = -9999; 的注释去掉,编译。编译器会报"balance 是私有成员,无法访问"。记住这个报错——它不是刁难你,是替你把一个将来才会爆的雷,提前到编译阶段给排掉了。这就是"封装"最直接的价值。
七、常见误区
- "面向对象就是多写几个
class"——不是。核心是"数据和方法绑在一起、数据被保护"。写一堆成员全公开的类,等于没封装。 - "类和对象是一回事"——图纸 vs 实物。
class Book只有一份定义;Book b1, b2;是两个互相独立的对象(各自有自己的onShelf)。 - "对象之间可以直接改对方的数据"——不该(也常常做不到)。对象之间应该通过方法(消息)配合:想改我的状态,请调用我的方法,不要伸手到我的口袋里。
- "属性越多越好"——属性是"这个对象自己的状态",就该属于它。把"全局都用得到的东西"硬塞给某个对象,是常见的坏设计(比如把"当前登录用户"塞进"图书"对象)。
- "先写类再想设计"——顺序反了。先想清楚有哪些角色、各自职责,再落成类(9.3 建模就是干这个)。直接开写类,最后往往得到一堆"什么都干"的怪物类。
自测三问(点开对答案)
1. 面向对象编程的第一步该做什么?——**把需求里的"名词"圈出来,找出系统中有哪些角色(对象候选)**,再想每个角色该有哪些数据、该会哪些本事。 2. 类和对象的区别是什么?——**类是图纸/模板**(描述有什么属性和方法,一般不占对象的内存),**对象是按图纸造出来的实物**(有自己的状态、占内存,可造多个)。 3. 面向对象凭什么能"把改动锁在内部"?——因为**数据和方法在同一个壳里,且数据通常被保护起来**;外部只能通过**方法(接口)**来操作,**改内部实现不会影响调用方**。- ☐ 9.2.3 面向对象的特点(下一节)