- 更清晰的原理与对比
- 现代写法(const、move、override、RAII 等)
- Qt 中的数据封装高级应用(Q_PROPERTY + Q_INVOKABLE)
- 完整 Visual Studio 可运行 Demo 项目(银行账户系统)
- 自动化测试用例(Google Test)
- 常见陷阱与最佳实践
目录(扩展版)
4.1 私有成员变量 + Getter / Setter
4.2 常量成员函数(const 方法)
4.3 友元(friend)与受控访问
4.4 现代封装:Pimpl 模式(Pointer to Implementation)
1. 引言:数据封装的核心价值与必要性
数据封装(Encapsulation) 是面向对象三大特性(封装、继承、多态)中最基础、最核心的一个。
核心价值(为什么必须掌握):
- 保护数据完整性:防止外部直接篡改内部状态
- 隐藏实现细节:外部只需知道“做什么”,无需知道“怎么做”
- 提高可维护性:修改内部实现不影响外部调用代码
- 增强安全性:私有成员无法被意外或恶意修改
- 支持接口隔离:通过有限接口实现“最小知识原则”
不封装的后果(反例):
struct Point {
double x; // 公开
double y; // 公开
};
Point p;
p.x = 999999; // 外部随意破坏坐标
p.y = –infinity(); // 导致后续计算崩溃
2. 数据封装的概念与三大目标
三大目标:
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 项目中的完整实现




