CC++ & Algorithm

封装——把秘密藏在保险箱里

困难20
语言版本:C++Python
概述:通过私有成员和公有接口保护数据安全,就像用密码箱存压岁钱,只能通过输入正确密码才能取钱。

封装——把秘密藏在保险箱里

你肯定有自己的小秘密,比如日记本、压岁钱,你不想让别人随便翻看,对吧?在C++的类里,我们也可以把一些数据“藏”起来,不让外面直接碰,只允许通过特定的“小窗口”来访问。这就叫做封装(Encapsulation)。

可以想象成你有一个密码箱:里面的钱(数据)是私有的,只有你自己知道密码(成员函数)。别人想碰钱,只能通过你设计的操作(比如输入密码打开箱子、按按钮存钱取钱)来进行,不能把箱子砸开直接拿钱。封装就是给代码加上一个“保险箱”,让程序更安全、更可靠。


1. 什么是封装?

封装是面向对象编程的三大特性之一(另外两个是继承和多态)。它的核心思想是:把数据(成员变量)和操作数据的方法(成员函数)打包在一个类里,并对外隐藏内部的实现细节,只暴露必要的接口

就像你用手机:你只需要按屏幕上的“拨号”按钮,不需要知道里面怎么处理电信号、怎么联网。手机内部复杂的设计被封装起来了。

在C++中,我们通过访问权限来控制谁能看、谁能改。主要有两个关键字:

  • private(私有的):只有类内部的成员函数可以访问,类外部(比如main函数)不能直接读写。
  • public(公有的):任何人都能访问,包括外部调用者。

还有一个很少用的protected(受保护的),用于继承,我们以后再说。


2. 用例子理解 private 和 public

2.1 日记本例子

假设你有一个日记本类,里面记录你每天的零花钱。你不想让别人直接看到里面的内容,但可以通过一个“公开的”方法让朋友知道你今天花了多少钱。

#include <iostream>
using namespace std;

class Diary {
private:
    double dailyMoney;   // 今天的零花钱(私有的,别人不能直接改)

public:
    // 设置今天的零花钱(公开的方法)
    void setMoney(double amount) {
        if (amount >= 0) {
            dailyMoney = amount;
        } else {
            cout << "金额不能为负数!" << endl;
        }
    }

    // 查看今天的零花钱(公开的方法,只读)
    double getMoney() const {
        return dailyMoney;
    }
};

int main() {
    Diary myDiary;
    // myDiary.dailyMoney = 100;   // 错误!dailyMoney是私有的,编译会报错
    myDiary.setMoney(20);          // 正确,通过公开方法设置
    cout << "今天零花钱:" << myDiary.getMoney() << "元" << endl;
    return 0;
}

2.2 游戏角色例子

再比如,你玩一个游戏,角色有生命值(HP)。生命值应该是私有的,不能直接设置成负数,也不能让其他玩家随意改成1000。只能通过“回血”或“受伤”操作来改变。

class Hero {
private:
    int hp;               // 生命值,私有的
    int maxHp;            // 最大生命值

public:
    // 构造函数:创建角色时指定初始HP和最大HP
    Hero(int startHp, int max) {
        if (startHp > 0 && startHp <= max) {
            hp = startHp;
        } else {
            hp = max;     // 如果输入不合理,就设为最大
        }
        maxHp = max;
    }

    // 受伤:减少生命值
    void takeDamage(int damage) {
        if (damage > 0) {
            hp -= damage;
            if (hp < 0) hp = 0;   // 不能低于0
            cout << "受伤!当前HP:" << hp << endl;
        }
    }

    // 回血:增加生命值,但不能超过最大值
    void heal(int amount) {
        if (amount > 0) {
            hp += amount;
            if (hp > maxHp) hp = maxHp;
            cout << "回血!当前HP:" << hp << endl;
        }
    }

    // 获取当前生命值(只读)
    int getHp() const {
        return hp;
    }
};

好处:外部想伤害角色,必须通过takeDamage,里面可以检查伤害值是否合法、是否溢出。如果直接把hp公开,别人写hero.hp = -100那角色就成负数了,不合理。


3. 为什么需要封装?——三个好处

3.1 保护数据安全

像银行账户余额、游戏角色的HP,直接暴露在外面很危险。别人可能不小心改成错误的值,甚至恶意破坏。封装就像给数据加了一把锁,只有通过正确的方法才能访问。

3.2 控制访问方式

你可以给数据设计“使用说明书”。比如银行账户只允许存款、取款和查询余额,不允许直接修改余额。这样用起来很清晰,也不容易用错。

3.3 方便后续修改

假如以后你想改变余额的存储方式(比如从double改成int分单位),只要对外接口(deposit、withdraw、getBalance)保持不变,调用者的代码完全不用改。封装把“内部变化”和“外部使用”隔离开了。


4. 新手容易犯的错误

❌ 错误1:忘记写public,导致所有成员都变成private

class Test {
private:    // 注意:class默认是private的,但显式写出来更好
    int data;
public:
    void setData(int d) { data = d; }
};

:如果你写了一个class,里面全是函数和数据,但没有public:,那么所有成员默认都是私有的。外部一个函数也调用不了!所以要养成习惯:把接口方法放在public:后面。

❌ 错误2:在外部直接访问私有成员

BankAccount myAccount(1000);
myAccount.balance = 5000;  // 编译错误,balance是private

这是封装的目的:禁止这样做。所以别想着“直接赋值方便”,要习惯用方法。

❌ 错误3:getter和setter不检查数据合法性

void setAge(int age) {
    age = age;   // 这里没写参数名区分?实际上直接写了age=age,没用!应该用this->age = age;
    // 而且没有检查age是否合理,比如年龄不能是负数。
}

正确做法

class Person {
private:
    int age;
public:
    void setAge(int a) {
        if (a > 0 && a < 150) {
            age = a;
        } else {
            cout << "年龄输入错误,设为默认18" << endl;
            age = 18;
        }
    }
};

❌ 错误4:忘记写const成员函数

getBalance() const中的const表示这个函数不会修改对象。如果不写,编译器可能不允许在const对象上调用它。养成好习惯:只读的函数加上const


5. 完整可运行示例:银行账户(含更多注释)

下面我们把原文的银行账户例子稍微完善,增加更详细的注释和检查。

#include <iostream>
using namespace std;

class BankAccount {
private:
    double balance;   // 余额,私有数据

public:
    // 构造函数:开户时设定初始余额,并进行合法性检查
    BankAccount(double initial) {
        if (initial >= 0) {
            balance = initial;
        } else {
            balance = 0;               // 负金额就当成0
            cout << "初始金额不能为负,已设为0" << endl;
        }
    }

    // 存款:公开方法,只有正数才能存
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            cout << "存款成功,当前余额:" << balance << endl;
        } else {
            cout << "存款金额必须大于0" << endl;
        }
    }

    // 取款:检查余额是否足够,且金额为正
    void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            cout << "取款成功,当前余额:" << balance << endl;
        } else if (amount > balance) {
            cout << "余额不足!当前余额只有:" << balance << endl;
        } else {
            cout << "取款金额必须大于0" << endl;
        }
    }

    // 查询余额(只读)—— const 表示不修改对象
    double getBalance() const {
        return balance;
    }
};

int main() {
    BankAccount myAccount(1000);   // 开户,存入1000元

    // myAccount.balance = 9999;   // 错误!balance是私有的,直接访问会编译报错

    myAccount.deposit(500);        // 存款500
    myAccount.withdraw(200);       // 取款200
    myAccount.withdraw(2000);      // 超余额取款,会提示余额不足
    cout << "最终余额:" << myAccount.getBalance() << endl;

    return 0;
}

运行结果

存款成功,当前余额:1500
取款成功,当前余额:1300
余额不足!当前余额只有:1300
最终余额:1300

6. 封装与日常生活

想一想你平时使用的东西:

  • 自动售货机:你只能通过投币、按按钮来买饮料,不能直接伸手进去拿。里面的库存是私有的,只有机器内部知道。
  • 手机屏保密码:你的相册、聊天记录是私有的,只有输入正确密码(公开接口)才能解锁查看。
  • 学校图书馆系统:你可以借书、还书,但不能直接修改数据库里的记录。数据封装在系统里,只通过借书还书接口操作。

封装就是这个道理:公开方法就是“操作窗口”,私有成员就是“内部秘密”


7. 相关知识点指引

学会封装之后,你可以继续学习:

  • 继承:子类可以继承父类的公有和保护成员,但私有成员不能直接继承。
  • 多态:通过虚函数实现“同一接口,不同行为”,但封装仍然保护了数据。
  • 友元:有时候需要让某个外部函数访问私有成员,可以用friend关键字,但慎用,会破坏封装。
  • 构造函数与析构函数:它们通常放在public中,用来初始化对象和清理资源。

封装是面向对象编程的基础,就像盖房子要先打地基。以后写代码时,多想想“这个数据应该藏起来吗?用户应该怎么操作它?”养成好习惯,你的程序会越来越强大又安全。

例题精讲

1单选题

关于C++中的封装,以下哪个比喻最准确地描述了其核心思想?

A把秘密写在纸条上塞进书里
B把压岁钱放进保险箱,只有输对密码才能取出
C把玩具放在开放架子上,随时可取
D把钥匙挂在门口,方便所有人使用
2判断题

在C++中,使用class关键字定义的类,其成员的默认访问权限是private。

3填空题
下面是一个银行账户类,请在填空处补充代码,使deposit函数能将金额添加到balance,getBalance函数返回当前余额。

class BankAccount {
private:
    double balance;
public:
    BankAccount() : balance(0) {}
    void deposit(double amount) {
        ___;  // 第一空
    }
    double getBalance() const {
        ___;  // 第二空
    }
};
4单选题

下列关于私有成员(private)的说法,哪一项是不正确的?

A私有成员可以隐藏类的实现细节
B私有成员可以防止外部代码意外修改数据
C私有成员可以被类的成员函数访问
D私有成员可以被类外部的普通函数通过对象直接访问
5填空题
模拟一个保险箱类,只有输入正确密码才能设置和获取内部物品。请在填空处补全代码,完成密码验证。

#include <string>
using namespace std;
class SafeBox {
private:
    int content;
    string password;
public:
    SafeBox(string pwd) : content(0), password(pwd) {}
    bool setContent(int value, string pwd) {
        if (___ == pwd) {
            content = value;
            return true;
        }
        return false;
    }
    int getContent(string pwd) const {
        if (___ == pwd) {
            return content;
        }
        return -1; // 密码错误返回-1
    }
};