🏠

类与封装

菜谱只是张纸,厨师才真会做菜——把"数据 + 操作"打包成一盒。

💡 打个比方

以前写程序像按菜谱炒菜:数据(食材)散在厨房各处,函数(步骤)一个个调用——这叫面向过程。现在想象一位厨师:他有属性(名字、工龄),会做事(切菜、炒菜、上菜)——数据和操作被打包在一个人身上,这叫面向对象。"类"(class)是设计图纸,"对象"(object)是按图纸造出的真房子:同一张图纸能盖很多栋房子,每栋都有自己独立的房间(数据)。

术语:类 class(图纸/蓝图);对象 object(按图纸造出的实体,也叫实例);属性(对象"有"什么:名字、血量);方法(对象"会"做什么:攻击、治疗);构造器 constructor(对象出生时自动调用的初始化函数);封装 encapsulation(藏起内部零件,只露接口)。

🖼️ 大图看明白

左边:面向过程——数据散落,函数独立 name[] 名字表 hp[] 血量表 mp[] 魔法表 ……更多数据 attack(i, j) heal(i) show(i) "这份 hp 是谁的?"要靠在参数里传下标 i 来暗示 右边:面向对象——数据 + 操作打包 class Player(图纸) 属性 + 方法的设计 照图纸盖房 🏠 Player a(对象) 属性:name="小明" hp=100 方法:attack() heal() show() 数据和操作住"同一栋房" 再盖一栋 Player b:它有自己的 name 和 hp,互不串门

🧩 子任务 1:面向过程 vs 面向对象

🎯 小目标:用"游戏角色"的例子,说清两种风格的区别。

管理 100 个游戏角色,两种写法对比:

// 面向过程:数据是"表",函数靠参数 i 知道在操作谁
string name[100];
int hp[100];
void attack(int i, int j) { hp[j] -= 10; }   // i 打 j,靠参数 i 暗示"是谁"

// 面向对象:每个角色是一个对象,数据和能力打包在一起
class Player {
    string name;        // 属性:我叫什么
    int hp;             // 属性:我有多少血
    void attack(Player &other) { other.hp -= 10; }  // 方法:我打他
    void heal() { hp += 20; }                       // 方法:我回血
};

面向对象里,"谁"不用传:a.attack(b) 读起来就是"小明打小强",数据和动作天然绑在一起。

✅ 小检查:① 面向过程版里,"这份 hp 属于哪个角色"写在哪里?② 面向对象版里,a.hp 属于谁?③ 同一张"图纸"(类)能造出几个对象?
看看答案
① 只写在函数参数里(下标 i),数据表本身不记"归属"——容易传错。 ② 属于对象 a:a.hp 是 a 自己的血量,b 的血量是 b.hp,各住各的房。 ③ 任意多个:类是图纸,对象是房子;Player a, b, c; 就是按同一张图纸盖了 3 栋,每栋的 name/hp 独立。

🧩 子任务 2:class = 属性 + 方法,还有"出生礼"构造器

🎯 小目标:会写类的骨架(属性 + 方法 + 构造器),理解构造器何时被调用。

C++ 里 struct 是"纯数据袋子"(只有变量),class 是"数据 + 操作的完整包装"。进化过程:

// 老式:struct 只有数据,操作散在外面
struct Student {
    string name;
    int score;
};
void study(Student &s) { s.score += 1; }   // 操作在外面,靠参数找 s

// 新式:class 把数据和方法都装进去
class Student {
    string name;                       // 属性
    int score;                         // 属性
    void study() { score += 1; }       // 方法:不用传"是谁"
    string who() { return name; }      // 方法
    // 构造器:和类同名、无返回值,对象"出生"时自动调用
    Student(string n, int s) {
        name = n;
        score = s;
    }
};

用构造器造对象,就像"出生时填好档案":

Student a("小明", 90);   // a 出生:name=小明, score=90(构造器自动执行)
Student b("小红", 85);   // b 出生:name=小红, score=85
b.study();               // b 的学习能力直接调用,不用告诉它"b 是谁"
✅ 小检查:Student b("小红", 85); 之后连续调用两次 b.study();,此时 b.score 等于多少?构造器在这过程中被调用了几次?
看看答案
b.score = 85 + 1 + 1 = 87。构造器只被调用 1 次(b 出生那一刻);study() 是普通方法,调几次执行几次。
易错:构造器没有返回值(连 void 都不写),名字必须和类名一模一样;它不是"被调用"的,是对象诞生时自动执行的。

🧩 子任务 3:封装——水管藏墙里,只露开关

🎯 小目标:理解 private / public 的分工,说出"为什么数据要藏起来"。

房子装修时,水管电线都砌进墙里,外面只留一个开关。你按开关就能开灯,完全不用知道电线怎么走——这就是封装:内部零件藏起来(private),只留接口给外人用(public)。

// 银行账户:余额藏在"墙里"
class BankAccount {
    int balance;                   // private(class 默认):藏在墙里的水管
public:
    BankAccount(int x) { balance = x; }          // 出生时设初始余额
    void deposit(int x) { balance += x; }        // 接口:存钱
    void withdraw(int x) {
        if (x <= balance) balance -= x;          // 规则在接口里把关
    }
    int query() { return balance; }              // 接口:查余额
};

为什么不让 balance 直接 public?因为谁都能写 acc.balance = -100——余额变成负数,银行直接破产!把 balance 藏起来后,外人只能走 deposit / withdraw 这两个"开关",而规则(不能取超过余额)就写在开关里面。

✅ 小检查:① 若 balance 是 public,有人写 acc.balance = -10000; 会怎样?② balance=50 时调用 withdraw(200),之后 query() 返回几?③ 一句话:封装把"规则"放在哪里把关?
看看答案
① 编译都过(public 允许直接改),余额变 -10000,没人拦——规则失效。若 balance 是 private,这行直接编译报错,逼你走接口。 ② 200 > 50,if 不成立,什么都不做 → query() 返回 50。 ③ 规则放在public 接口内部把关:每次进门(调接口)都先过一道检查,比"大门敞开自己管自己"可靠得多。

🧩 子任务 4:创建对象——栈上造、堆上造(new 与 ->)

🎯 小目标:分清"栈对象"和"堆对象(new)",会用 -> 访问,知道何时 delete。

造对象有两个"地方":

p 本身只是地址(一张写着房子位置的纸条)。想通过 p 用对象里的东西,就用 ->:"顺着地址进去"——p->study() 等价于"(*p).study()"。

生命周期口诀:new 出生 → 使用 → delete 归还 → 别再用。delete 之后还碰 p,p 就成了野指针(纸条还在,房子已拆)。

Student* p = new Student("小红", 85);
p->study();        // 顺着地址进去,调方法
cout << p->who() << endl;   // 小红
delete p;           // 拆房归还
p->study();          // ← 危险:野指针!房子拆了还进去住
✅ 小检查:上面代码哪一行是"野指针使用"?如果把 delete p; 删掉,程序会出什么问题?
看看答案
最后一行 p->study(); 是野指针使用(delete 之后)。若删掉 delete p;,对象永远不会被释放——每造一个都漏一间房,程序越跑占的内存越多(内存泄漏),但当下不报错,更难发现。

💻 完整代码:银行账户(封装 + 两种创建方式)

// 输出:balance: 120 / balance: 100
#include <iostream>
#include <string>
using namespace std;

class BankAccount {
    int balance;                        // private:藏起来
public:
    BankAccount(int x) { balance = x; } // 构造器
    void deposit(int x)  { balance += x; }
    void withdraw(int x) { if (x <= balance) balance -= x; }
    int query()          { return balance; }
    string report()      { return "balance: " + to_string(balance); }
};

int main() {
    BankAccount acc(100);        // 栈对象:出生 balance=100
    acc.deposit(50);             // 150
    acc.withdraw(30);            // 120
    acc.withdraw(200);           // 200 > 120,规则挡住,不变
    cout << acc.report() << endl; // balance: 120

    BankAccount* p = new BankAccount(10);  // 堆对象
    p->deposit(90);                         // 100
    cout << p->report() << endl;            // balance: 100
    delete p;                              // 释放,之后别再碰 p
    return 0;
}

✋ 你来试试

第 1 题:类和对象各是什么?给出一对具体例子(类名 + 一个对象)。
类 = 图纸/蓝图(设计,描述"有什么属性、会做什么");对象 = 按图纸造出的实体(每份数据独立)。例子:类 Player,对象 Player a("小明", 100);——a 是按 Player 图纸造的一个具体角色。
第 2 题:BankAccount acc(50); 里的构造器起什么作用?如果这个类没写构造器,balance 会是什么状态?
构造器在 acc 出生那一刻自动执行,把 balance 初始化为 50。若没写构造器,balance 是未初始化的随机值——账户"生来"就带着一笔不确定的钱,非常危险。
第 3 题:为什么把 balance 设为 private、只留 deposit / withdraw 作 public?举一个"直接写会破产"的具体例子。
封装让外人只能走接口,规则(如"不能取超过余额")在接口内部把关。若 balance 是 public,任何代码都能写 acc.balance = -10000;——绕过所有检查,余额直接负一万,银行破产;private 时这行连编译都过不了。
第 4 题(加餐):Student* p = new Student("小红", 85); delete p; p->study();——最后一行为什么危险?"野指针"像什么?
delete 后对象已被释放,p->study() 是"房子拆了还进去住"(use-after-free),可能读到垃圾数据甚至崩溃。野指针 = 一张地址还写着、但房子已拆的纸条。
🎯 一句话记住:类 = 图纸,对象 = 真房子;类把"属性 + 方法"打包,构造器在出生时初始化;封装 = 水管藏墙里(private)、只露开关(public);new 出生、-> 使用、delete 归还。