搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第13章 封装与访问控制

第13章 封装与访问控制

入门 👤 已了解类的基本概念,想写出更健壮代码的初学者 🔧 设计安全的类接口,保护核心数据不被随意修改
本章你将学到
  • 封装的核心理念与三大访问权限
  • 使用getter/setter方法安全地管理数据
  • 设计类接口的最佳实践与常见误区
核心概念封装访问控制

想象一下你的手机:你可以通过触摸屏(接口)打电话、拍照,但无法直接拆开外壳去摆弄里面的电路板(内部实现)。C++的封装就是为你的代码穿上这样一个“外壳”,把数据和操作数据的方法捆绑在一起,并对外隐藏复杂的实现细节。

封装是什么?

生活类比: 自动售货机。你只需投币、按按钮(公共接口),就能获得商品,而无需了解内部的找零计算、货品推送等复杂机械结构(私有实现)。
技术定义: 封装是将数据(成员变量)和操作这些数据的函数(成员函数)捆绑在一个类中,并通过访问权限控制,对外隐藏对象的内部细节,仅暴露必要的接口。

访问权限的三道门:public, private, protected

访问权限决定了类的外部代码能“看到”和“操作”哪些成员。你可以把它们想象成公司不同区域的门禁权限。

访问权限谁能访问类比典型用途
public (公有)任何代码公司前台、公共会议室,所有人都能进类提供给外界的接口函数
private (私有)仅限本类内部的成员函数核心机房、财务室,只有授权员工能进隐藏的内部数据和辅助函数
protected (保护)本类及派生类的成员函数部门内部资料室,本部门和下属部门可访问为继承预留的接口(本章暂不展开)

在项目中,我见过太多因为把所有成员都设为public而导致的bug。一个典型的例子是,某个模块直接修改了另一个模块对象的内部状态,导致状态不一致,排查了一整天。所以,默认将所有数据成员设为private,这是一个必须养成的好习惯。

如何实现封装:Getter与Setter

数据成员设为private后,外部如何安全地读取或修改它们?答案是提供公有的成员函数作为通道,这就是Getter(获取器)和Setter(设置器)。

// 一个封装良好的银行账户类示例
class BankAccount {
private: // 私有区域:藏起来的“金库”
    std::string accountHolder;
    double balance; // 余额,必须被保护
    int passwordHash; // 密码哈希,绝不能被直接访问

public: // 公有区域:对外的“服务窗口”
    // 构造函数
    BankAccount(const std::string& name, double initBalance, int pwd);

    // Getter:只读访问,通常加const
    std::string getHolderName() const { return accountHolder; }
    double getBalance() const { return balance; }

    // Setter:可控的写访问,可加入验证逻辑
    bool deposit(double amount) {
        if (amount <= 0) {
            printf("存款金额必须为正数!\n");
            return false;
        }
        balance += amount;
        return true;
    }

    bool withdraw(double amount, int inputPwd) {
        if (!verifyPassword(inputPwd)) {
            printf("密码错误!\n");
            return false;
        }
        if (amount <= 0 || amount > balance) {
            printf("取款金额无效或余额不足!\n");
            return false;
        }
        balance -= amount;
        return true;
    }

private: // 另一个私有区域:内部工具函数
    bool verifyPassword(int inputPwd) const {
        // 简化示例,实际应使用加密比较
        return passwordHash == inputPwd;
    }
};

关键点:Setter不是简单的赋值

Setter的核心价值在于控制。它允许你在数据被修改前插入验证、日志记录、触发事件等逻辑。上面代码中的deposit和withdraw就是强化版的Setter,它们确保了存款为正、取款不超余额且密码正确。永远不要写一个像void setBalance(double b) { balance = b; }这样危险的公有函数,那等于又把金库钥匙交给了外人。

外部代码 (main等)
→
公有接口 (public)
→
私有数据 (private)
↓
getBalance()
↘
balance 变量
↖
withdraw(...)
↓ (包含验证逻辑)

这个架构清晰地展示了封装的数据流:外部只能通过有限的、受控的公有接口来间接影响私有数据,无法直接触碰。

封装的优势与设计原则

封装带来的好处是实实在在的,尤其在多人协作或维护大型项目时。

1. 数据保护与一致性

  • 防止非法或无效数据被设置。
  • 确保对象状态始终符合业务规则(如余额不为负)。

2. 实现细节隐藏

  • 内部数据结构的改变(如数组改链表)不影响外部调用代码。
  • 降低模块间的耦合度。

3. 便于调试与维护

  • 所有对数据的修改都通过特定函数,易于设置断点跟踪。
  • 修改验证逻辑只需在一处进行。

编者提示: 封装与之前学习的结构体(第10章)形成鲜明对比。结构体的所有成员默认是公开的,像一个开放的数据容器;而类通过访问控制,将数据和行为绑定,并构建了安全的访问边界。这是面向对象与面向过程思维的关键区别之一。

常见误区

  • 把所有成员都写成public: 这完全失去了封装的意义,类退化成结构体,数据可以被任意篡改,是项目中的“定时炸弹”。
  • 为每个private成员都提供getter/setter: 这被称为“伪封装”。如果一个数据不需要被外部知道或修改,就不要提供它的访问接口。思考这个成员是否真的需要暴露。
  • 在getter中返回私有成员的非常量引用或指针: 例如std::string& getName() { return name; },这相当于给外部代码开了一个“后门”,他们可以通过这个引用直接修改你的私有数据,封装形同虚设。正确的做法是返回值或常量引用(const std::string&)。

类内定义与类外定义

成员函数可以在类内部直接定义(如上例),也可以在类内部声明,在类外部定义。对于复杂的函数,通常采用后者,使类声明更清晰。

// 在头文件(.hpp)中声明类
class Sensor {
private:
    int sensorId;
    double currentValue;
    static const int MAX_READING = 1024; // 私有常量
public:
    Sensor(int id);
    double readValue(); // 声明
    void calibrate(double offset); // 声明
    int getId() const { return sensorId; } // 类内定义
};

// 在源文件(.cpp)中定义函数
double Sensor::readValue() { // 使用作用域解析运算符 ::
    // 模拟读取传感器硬件,实际项目可能涉及I2C/SPI操作
    currentValue = someHardwareRead(sensorId);
    if (currentValue > MAX_READING) currentValue = MAX_READING;
    return currentValue;
}
void Sensor::calibrate(double offset) {
    // 校准逻辑...
}

实际开发中,大部分团队会约定:短小的、简单的函数(如Getter、简单的Setter)在类内定义;逻辑复杂的函数在类外定义,以保持头文件的简洁和编译速度。

动手试一试

1. 改造一个类: 假设你有一个未封装的Rectangle类,其width和height都是public。请将其封装,将数据成员设为private,并提供相应的Getter和Setter。在Setter中,确保宽度和高度不能设置为负数。
2. 设计一个“用户”类(User): 包含私有数据:用户名(std::string username)、密码哈希(int passwordHash)、邮箱(std::string email)。设计公有接口:构造函数、修改密码(需验证旧密码)、验证登录、获取用户名(只读)。思考:邮箱是否需要提供Setter?如果需要,应如何验证邮箱格式?

检验你的理解

  1. 一个类的private成员函数,能否被另一个同类型对象的成员函数调用?(例如,在BankAccount::transferTo(BankAccount& other)内部,能否调用other.verifyPassword(...)?)
  2. 为什么说“返回私有数据成员的非const引用”会破坏封装?请举例说明。
  3. 判断:如果一个类的所有数据成员都是private,并且没有任何公有成员函数,那么这个类可以被实例化并使用吗?

本章小结

  • 封装是面向对象三大特性之一,它将数据和对数据的操作捆绑,并通过访问权限隐藏实现细节。
  • 三种访问权限: public(对外接口)、private(隐藏内部,默认选择)、protected(为继承预留)。
  • 实现封装的关键工具是Getter和Setter: Getter提供只读访问,Setter提供受控的写访问,并可在其中加入业务逻辑验证。
  • 封装的核心优势在于数据保护、降低耦合、易于维护。 设计类时应遵循“最小暴露原则”,不要过度提供访问接口。
  • 成员函数可在类内或类外定义,类外定义需使用ClassName::functionName的语法。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注