欢迎光临
我们一直在努力

C++ 数据封装(Data Encapsulation)

  • 更清晰的原理与对比
  • 现代写法(const、move、override、RAII 等)
  • Qt 中的数据封装高级应用(Q_PROPERTY + Q_INVOKABLE)
  • 完整 Visual Studio 可运行 Demo 项目(银行账户系统)
  • 自动化测试用例(Google Test)
  • 常见陷阱与最佳实践

目录(扩展版)

  • 引言:数据封装的核心价值与必要性
  • 数据封装的概念与三大目标
  • C++ 中的访问控制机制(public / private / protected / struct)
  • 数据封装的实现方式(现代 C++ 写法)
    4.1 私有成员变量 + Getter / Setter
    4.2 常量成员函数(const 方法)
    4.3 友元(friend)与受控访问
    4.4 现代封装:Pimpl 模式(Pointer to Implementation)
  • Qt 中的数据封装高级实践(Q_PROPERTY + Q_INVOKABLE)
  • 完整 Visual Studio Demo 项目:异常安全 + 数据封装的银行系统
  • 自动化测试用例(Google Test)
  • 常见陷阱与最佳实践
  • 总结与进阶建议
  • 1. 引言:数据封装的核心价值与必要性

    数据封装(Encapsulation) 是面向对象三大特性(封装、继承、多态)中最基础、最核心的一个。

    核心价值(为什么必须掌握):

    • 保护数据完整性:防止外部直接篡改内部状态
    • 隐藏实现细节:外部只需知道“做什么”,无需知道“怎么做”
    • 提高可维护性:修改内部实现不影响外部调用代码
    • 增强安全性:私有成员无法被意外或恶意修改
    • 支持接口隔离:通过有限接口实现“最小知识原则”

    不封装的后果(反例):

    struct Point {
    double x; // 公开
    double y; // 公开
    };

    Point p;
    p.x = 999999; // 外部随意破坏坐标
    p.y = infinity(); // 导致后续计算崩溃

    2. 数据封装的概念与三大目标

    三大目标:

  • 信息隐藏(Information Hiding):只暴露必要接口
  • 数据保护(Data Protection):防止非法状态
  • 接口抽象(Interface Abstraction):外部只依赖行为契约
  • 3. C++ 中的访问控制机制

    访问级别类内部派生类外部(类外)典型用途
    public 可访问 可访问 可访问 接口、对外方法
    protected 可访问 可访问 不可访问 给子类使用的内部接口/数据
    private 可访问 不可访问 不可访问 实现细节、内部状态

    struct vs class 默认区别(最容易忽略):

    • struct:成员默认 public
    • class:成员默认 private

    现代 C++ 推荐:一律用 class,显式写 public / private。

    4. 数据封装的实现方式(现代 C++ 写法)

    4.1 私有成员变量 + Getter / Setter(最常见)

    #include <string>
    #include <stdexcept>

    class Employee {
    private:
    std::string name_;
    int age_ = 0;
    double salary_ = 0.0;

    public:
    // 现代写法:移动语义 + 异常检查
    void setName(std::string name) {
    if (name.empty()) throw std::invalid_argument("姓名不能为空");
    name_ = std::move(name);
    }

    std::string name() const & { return name_; } // 返回 const &
    std::string name() && { return std::move(name_); } // 右值引用优化

    void setAge(int age) {
    if (age < 18 || age > 65) throw std::out_of_range("年龄不合法");
    age_ = age;
    }

    int age() const { return age_; }

    // 只读属性(推荐)
    double salary() const { return salary_; }
    void raiseSalary(double percentage) {
    if (percentage <= 0) throw std::invalid_argument("加薪比例必须大于0");
    salary_ *= (1 + percentage / 100);
    }
    };

    4.2 常量成员函数(const 方法)

    double getSalary() const { return salary_; } // 承诺不修改对象状态

    4.3 Pimpl 模式(Pointer to Implementation)——最高级封装

    隐藏所有私有实现,只暴露接口。

    employee.h(对外头文件)

    #pragma once
    #include <memory>
    #include <string>

    class Employee {
    public:
    Employee(std::string name, int age);
    ~Employee();

    std::string name() const;
    int age() const;

    private:
    class Impl; // 前向声明
    std::unique_ptr<Impl> pImpl; // 指向私有实现
    };

    employee.cpp(实现细节完全隐藏)

    #include "employee.h"

    class Employee::Impl {
    public:
    std::string name_;
    int age_;
    };

    Employee::Employee(std::string name, int age)
    : pImpl(std::make_unique<Impl>()) {
    pImpl->name_ = std::move(name);
    pImpl->age_ = age;
    }

    Employee::~Employee() = default;

    std::string Employee::name() const { return pImpl->name_; }
    int Employee::age() const { return pImpl->age_; }

    优点:

    • 编译隔离:修改 Impl 不需要重新编译使用方
    • 二进制兼容性:接口不变可升级实现
    • 隐藏所有私有细节

    5. Qt 中的数据封装高级实践

    Qt 大量使用数据封装 + 元对象系统。

    5.1 Q_PROPERTY(最经典封装方式)

    class Person : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
    Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)

    public:
    explicit Person(QObject *parent = nullptr) : QObject(parent) {}

    QString name() const { return m_name; }
    void setName(const QString &name) {
    if (m_name == name) return;
    m_name = name;
    emit nameChanged(name);
    }

    int age() const { return m_age; }
    void setAge(int age) {
    if (m_age == age) return;
    m_age = age;
    emit ageChanged(age);
    }

    signals:
    void nameChanged(const QString &name);
    void ageChanged(int age);

    private:
    QString m_name;
    int m_age = 0;
    };

    QML 使用(双向绑定):

    TextField {
    text: person.name
    onAccepted: person.name = text
    }

    5.2 Q_INVOKABLE + 接口封装

    class Device : public QObject {
    Q_OBJECT
    public:
    Q_INVOKABLE virtual QString status() const = 0;
    };

    6. 完整 Visual Studio Demo 项目:异常安全 + 数据封装的银行系统

    项目名称:BankEncapsulationDemo

    bankaccount.h

    #pragma once
    #include <stdexcept>
    #include <string>

    class InvalidAmount : public std::invalid_argument {
    public:
    InvalidAmount(const std::string& msg) : std::invalid_argument(msg) {}
    };

    class InsufficientFunds : public std::runtime_error {
    public:
    InsufficientFunds(const std::string& msg) : std::runtime_error(msg) {}
    };

    class BankAccount {
    private:
    double balance_ = 0.0;

    public:
    BankAccount() = default;

    // 封装:只允许通过方法修改
    void deposit(double amount) {
    if (amount <= 0) throw InvalidAmount("存款金额必须大于 0");
    balance_ += amount;
    }

    void withdraw(double amount) {
    if (amount <= 0) throw InvalidAmount("取款金额必须大于 0");
    if (amount > balance_) throw InsufficientFunds("余额不足");
    balance_ -= amount;
    }

    double balance() const { return balance_; } // 只读接口
    };

    main.cpp

    #include "bankaccount.h"
    #include <iostream>
    #include <memory>

    int main() {
    try {
    BankAccount acc;
    acc.deposit(1000);
    std::cout << "初始余额: " << acc.balance() << "\\n";

    acc.withdraw(300);
    std::cout << "取款后余额: " << acc.balance() << "\\n";

    // 故意触发异常
    acc.withdraw(1000);
    }
    catch (const InvalidAmount& e) {
    std::cerr << "参数错误: " << e.what() << "\\n";
    }
    catch (const InsufficientFunds& e) {
    std::cerr << "业务异常: " << e.what() << "\\n";
    }
    catch (const std::exception& e) {
    std::cerr << "未知异常: " << e.what() << "\\n";
    }

    std::cout << "程序结束。\\n";
    return 0;
    }

    运行结果(故意触发异常):

    初始余额: 1000
    取款后余额: 700
    业务异常: 余额不足
    程序结束。

    7. 自动化测试用例(Google Test)

    test_bank.cpp

    #include <gtest/gtest.h>
    #include "bankaccount.h"

    TEST(BankTest, DepositPositive) {
    BankAccount acc;
    acc.deposit(500);
    EXPECT_DOUBLE_EQ(acc.balance(), 500.0);
    }

    TEST(BankTest, WithdrawInsufficientThrows) {
    BankAccount acc;
    acc.deposit(100);
    EXPECT_THROW(acc.withdraw(200), InsufficientFunds);
    }

    TEST(BankTest, NegativeDepositThrows) {
    BankAccount acc;
    EXPECT_THROW(acc.deposit(50), InvalidAmount);
    }

    8. 常见陷阱与最佳实践

    陷阱后果最佳实践
    public 成员变量 破坏封装、易被误改 一律 private + Getter/Setter
    Getter 返回引用 外部可修改内部状态 返回 const & 或值(C++17 可考虑 std::string_view)
    Setter 无校验 数据不一致 校验合法性 + 抛异常或返回 bool
    Qt 中直接暴露 QObject 子对象 生命周期混乱 使用 Q_PROPERTY + Q_INVOKABLE 封装
    Pimpl 滥用 增加一层指针开销 只在需要编译隔离或二进制兼容时使用

    现代最佳实践总结:

    • 成员变量一律 private
    • 接口一律 const 方法 + 值/引用返回
    • 写操作加校验 + 异常
    • Qt 项目优先 Q_PROPERTY + 信号通知变化
    • 大型项目推荐 Pimpl + 接口隔离

    如果您需要:

    • Qt 中更复杂的 Q_PROPERTY + QML 绑定示例
    • Pimpl 模式在 Qt 项目中的完整实现
    赞(0)
    未经允许不得转载:171主机测评 » C++ 数据封装(Data Encapsulation)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址