前言

本节内容,将会用到前面类的相关知识以及C++基础教程以及C++类&对象,需要你具备前面的知识,如果你还有不会的或者说不熟悉的地方,请重新温习一下

C++ 数据抽象:只展示必要的,隐藏复杂的

数据抽象是面向对象编程(OOP)中的一个基本概念,它有助于使程序更易于管理、更安全且更易于理解。核心思想是只向外界展示对象的必要特性,隐藏内部的实现细节。这使得类的使用者可以通过简单的接口与其交互,而无需了解其内部工作方式。


目录


引言:什么是数据抽象?

在编程中,数据抽象指的是只向外界提供必要的信息,隐藏背景细节。这是一种通过隐藏底层细节来减少编程复杂性和工作量的方法。在 C++ 中,数据抽象是通过类和访问控制符来实现的。


现实生活中的类比

为了更好地理解数据抽象,我们来看一个现实生活中的例子:自动售货机

  • 你需要知道的:

    • 投入硬币或纸币。
    • 按下按钮选择商品。
    • 取走商品和找零。
  • 你不需要知道的:

    • 机器如何识别货币。
    • 机械臂如何将商品推到出口。
    • 内部的库存管理系统如何运作。

自动售货机将复杂的内部流程抽象掉,提供了简单的界面(投币口、按钮和取货口)供你使用。


C++ 中的数据抽象如何实现

在 C++ 中,数据抽象是通过类(class)来实现的。类将数据和操作数据的函数封装在一起。通过控制对类成员的访问,我们可以隐藏内部的工作方式,只暴露必要的部分。

类的结构

1
2
3
4
5
6
class 类名 {
public:
// 公有成员(接口)
private:
// 私有成员(实现细节)
};

C++ 的访问控制符

访问控制符定义了类成员的访问权限。C++ 提供了三个访问控制符:

公有成员(public)

  • 语法: public:
  • 描述:public 下声明的成员可以在任何地方访问,只要对象是可见的。
  • 用途: 定义类的接口,供外部使用的函数和变量。

私有成员(private)

  • 语法: private:
  • 描述:private 下声明的成员只能在类的内部访问。
  • 用途: 存储内部数据和辅助函数,不希望被外部直接访问。

受保护成员(protected)

  • 语法: protected:
  • 描述:protected 下声明的成员可以在类内部和派生类中访问,但不能在其他地方访问。
  • 用途: 当设计一个类的继承体系,需要子类访问某些但不希望公开的成员时使用。

数据抽象的好处

  1. 安全性: 隐藏对象的内部状态,防止未经授权的访问和修改。
  2. 简化: 用户可以通过简单的接口与对象交互,无需关心复杂的内部逻辑。
  3. 可维护性: 内部实现的改变不会影响使用该类的代码。
  4. 模块化: 鼓励关注点分离,使代码更加有组织。
  5. 灵活性: 允许开发者在不改变外部交互方式的情况下更改内部工作方式。

示例:构建一个简单的银行账户类

让我们创建一个 BankAccount(银行账户)类,来演示 C++ 中的数据抽象。

BankAccount 类的规范

  • 公有接口:

    • deposit(double amount):存款
    • withdraw(double amount):取款
    • getBalance():获取余额
  • 私有成员:

    • balance(余额):用于存储账户余额的变量

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <string>

class BankAccount {
public:
// 构造函数,初始化账户,默认余额为 0.0
BankAccount(double initialBalance = 0.0) {
if (initialBalance >= 0.0) {
balance = initialBalance;
} else {
balance = 0.0;
std::cerr << "初始余额无效。已将余额设为 0.0\n";
}
}

// 公有方法:存款
void deposit(double amount) {
if (amount > 0.0) {
balance += amount;
} else {
std::cerr << "存款金额必须为正数。\n";
}
}

// 公有方法:取款
void withdraw(double amount) {
if (amount > 0.0) {
if (amount <= balance) {
balance -= amount;
} else {
std::cerr << "余额不足。\n";
}
} else {
std::cerr << "取款金额必须为正数。\n";
}
}

// 公有方法:获取余额
double getBalance() const {
return balance;
}

private:
// 私有成员变量:余额
double balance;
};

使用 BankAccount 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
BankAccount myAccount(100.0); // 创建一个初始余额为 $100 的账户

myAccount.deposit(50.0); // 存款 $50
myAccount.withdraw(20.0); // 取款 $20

std::cout << "当前余额: $" << myAccount.getBalance() << std::endl;

myAccount.withdraw(150.0); // 尝试取款 $150(应当失败)

std::cout << "最终余额: $" << myAccount.getBalance() << std::endl;

return 0;
}

预期输出:

1
2
3
当前余额: $130
余额不足。
最终余额: $130

代码解析

7.1 公有接口

  • 构造函数 BankAccount(double initialBalance = 0.0)

    • 用于初始化账户,允许指定初始余额。
    • 验证初始余额是否为非负数。
    • 如果初始余额无效,设置余额为 0.0,并输出错误信息。
  • deposit(double amount)

    • 允许用户向账户存款。
    • 验证存款金额为正数。
    • 更新余额。
    • 如果金额无效,输出错误信息。
  • withdraw(double amount)

    • 允许用户从账户取款。
    • 检查取款金额为正数且余额足够。
    • 如果取款成功,更新余额。
    • 如果金额无效或余额不足,输出错误信息。
  • getBalance() const

    • 返回当前余额。
    • 使用 const,表示此方法不会修改对象的状态。

7.2 私有成员

  • double balance
    • 存储账户的当前余额。
    • 声明为 private,防止外部代码直接修改。
    • 确保所有对 balance 的更改都通过受控的方法进行。

7.3 为什么要使用数据抽象?

  • 封装余额:

    • 通过将 balance 设为私有,防止外部代码将其设置为无效值(例如负数)。
    • 用户不能直接操作余额,必须通过提供的方法,这些方法包含了必要的验证。
  • 受控访问:

    • 所有对 balance 的操作都通过 depositwithdrawgetBalance 进行。
    • 确保数据完整性和对象状态的一致性。
  • 灵活性:

    • 如果将来需要更改 balance 的内部表示方式(例如从 double 改为更精确的类型),只要公有接口不变,外部代码就无需修改。

数据抽象的设计策略

  1. 将数据成员设为私有:

    • 始终将数据成员声明为 privateprotected
    • 防止从类外部直接访问和修改,保护数据完整性。
  2. 提供公有方法进行交互:

    • 使用 public 方法提供必要的功能。
    • 这些方法应包含必要的验证和错误处理。
    • 保持接口简洁易用。
  3. 接口与实现分离:

    • 类的使用者只需与接口(公有方法)交互,无需了解实现细节。
    • 这允许在不影响外部代码的情况下更改内部实现。
    • 增强了模块化和可维护性。
  4. 正确使用 const

    • 对于不修改对象状态的方法,使用 const 修饰。
    • 防止意外修改,提高代码安全性。
    • 允许在 const 对象上调用这些方法。
  5. 为接口提供文档:

    • 为公有方法提供清晰的注释和文档。
    • 解释方法的功能、参数、返回值以及可能的错误情况。
    • 提高代码的可读性和可用性。

常见错误及如何避免

9.1 将数据成员设为公有

  • 错误: 将数据成员声明为 public,允许外部代码直接访问和修改。
  • 问题: 可能导致数据处于无效状态,难以维护数据完整性。
  • 解决方案: 始终将数据成员声明为 privateprotected,通过受控的公有方法访问。

9.2 未对输入进行验证

  • 错误: 在公有方法中未检查输入,可能导致对象处于无效状态(例如负余额)。
  • 问题: 会引发错误和意外行为。
  • 解决方案: 在所有修改对象状态的方法中实现输入验证,并提供有意义的错误信息。

9.3 暴露内部实现

  • 错误: 设计的方法暴露或依赖于内部数据结构(例如返回私有成员的指针或引用)。
  • 问题: 打破了封装性,可能导致意外的副作用。
  • 解决方案: 保持内部实现的隐藏,方法应在更高的抽象层次上操作。

9.4 忽略 const 的使用

  • 错误: 未在适当的地方使用 const,导致方法可能意外修改对象。
  • 问题: 可能引发副作用,使代码难以理解。
  • 解决方案: 使用 const 标记不修改对象状态的方法,提高代码安全性和清晰度。

9.5 接口过于复杂

  • 错误: 提供过多的公有方法,或暴露不必要的功能。
  • 问题: 使类难以使用和理解。
  • 解决方案: 保持公有接口的简洁,专注于核心功能。

总结

  • 数据抽象是面向对象编程中的关键原则,允许开发者通过隐藏内部细节来简化复杂系统。
  • 是 C++ 中实现数据抽象的主要方式,将数据和函数封装在一起。
  • 访问控制符publicprivateprotected)控制类成员的访问方式,是实现抽象的关键。
  • 数据抽象的好处包括提高安全性、简化接口、增强可维护性和灵活性。
  • 设计策略包括仔细规划类的接口,将数据成员设为私有,提供公有方法进行必要的交互,以及正确使用 const
  • 避免常见错误,遵循最佳实践,如正确使用访问控制符和输入验证。

最后的思考

理解并有效地实现数据抽象,对于编写健壮、可维护和安全的 C++ 程序至关重要。通过精心设计类,只暴露必要的部分并隐藏其余内容,可以使代码更易于使用,减少错误的可能性。

在设计类时,始终自问:

  • 使用者需要知道什么?
  • 应该隐藏什么以防止误用或意外错误?

这种思维方式将指导你创建有效的抽象,使你的代码库更加强大,能够适应变化。