Skip to content

C++ 抽象

数据抽象是编程中的一个基本概念,它涉及只暴露对象的必要特征,同时隐藏复杂的实现细节。它关注的是对象做什么,而不是如何做。

想想日常生活中的物体:当你驾驶汽车时,你与方向盘、踏板和换挡器(接口)交互。你不需要了解发动机、变速箱或燃烧过程(实现)的内部工作原理就能驾驶。

这种接口和实现的分离在软件设计中至关重要。它允许:

  • 简洁性:组件的使用者只需理解其公共接口。
  • 可维护性:内部实现可以更改或改进,而不会影响使用该组件的代码,只要接口保持一致。
  • 模块化:组件可以独立开发和测试。

在 C++ 中,类是实现数据抽象的主要工具。类将数据(成员变量)和操作这些数据的功能(成员函数或方法)捆绑在一起。通过使用访问修饰符(public、private、protected),类控制哪些部分构成公共接口,哪些部分是隐藏的实现细节。

例如,当你使用 std::vector 时,你与 push_back()、size()、empty() 和 operator[] 等方法交互。你不需要知道 vector 在内部如何管理内存分配、重新分配或元素存储。std::vector 类将这些细节抽象化,提供了一个清晰而强大的接口。

类似地,在使用 <iostream> 库中的 std::cout 时:

#include <iostream>
int main() {
std::cout << "Hello, Modern C++!" << std::endl;
return 0;
}

你只需要知道如何使用插入运算符(<<)和 std::endl。格式化输出、与操作系统的控制台缓冲区交互以及在屏幕上显示字符等复杂过程都隐藏(抽象)在 std::cout 的实现中。

C++ 提供访问修饰符来控制类成员的可见性:

  • public:声明为 public 的成员构成类的接口。它们可以在对象可见的任何地方访问。
  • private:声明为 private 的成员是实现细节。它们只能由同一类的成员函数(和友元)访问。这隐藏了复杂性并保护了对象的内部状态。
  • protected:类似于 private,但可以由派生类的成员函数访问(用于继承,这是一个更高级的主题)。

默认情况下,class 的成员是 private 的,而 struct 的成员是 public 的。这种区别通常指导了在两者之间进行选择:当封装和数据隐藏是首要考虑时,通常优先使用 class。

使用数据抽象带来了显著的优势:

  • 封装和保护:隐藏内部数据(private 成员)可以防止外部代码意外损坏,确保对象保持有效状态。
  • 灵活性和可维护性:内部实现可以重构、优化或修复,而不会破坏用户代码,前提是公共接口没有改变。
  • 降低复杂性:用户与简化的视图交互,使系统更容易理解和使用。

如果实现细节(如特定的数据成员)是公开的,那么任何直接访问这些细节的代码都可能在这些细节改变时被破坏。抽象保护用户免受此类实现变化的影响。

让我们重温一下使用现代 C++ 实践的 Adder 示例。这个类提供一个接口来添加数字并获取总和,同时隐藏 total 总和本身。

#include <iostream>
class Adder {
public:
// Constructor: Initializes the total, often to zero.
// 构造函数:初始化总和,通常为零。
// Use member initializer list for efficiency.
// 使用成员初始化列表提高效率。
Adder(int initialValue = 0) : total{initialValue} {
// Constructor body (can be empty if initialization is done)
// 构造函数体(如果初始化已完成,可以为空)
}
// Public interface: Method to add a number.
// 公共接口:添加数字的方法。
void addNum(int number) {
total += number;
}
// Public interface: Method to retrieve the total.
// 公共接口:获取总和的方法。
// Marked 'const' because it doesn't change the Adder's state.
// 标记为 'const',因为它不改变 Adder 的状态。
int getTotal() const {
return total;
}
private:
// Hidden implementation detail: Stores the running total.
// 隐藏的实现细节:存储运行总和。
int total;
};
int main() {
Adder myAdder; // Creates an Adder object, total initialized to 0 // 创建一个 Adder 对象,total 初始化为 0
myAdder.addNum(10);
myAdder.addNum(20);
myAdder.addNum(30);
// Use the public interface to get the result // 使用公共接口获取结果
std::cout << "Total: " << myAdder.getTotal() << std::endl;
Adder anotherAdder{100}; // Initialize with a starting value of 100 // 初始化一个起始值为 100
anotherAdder.addNum(50);
std::cout << "Another Total: " << anotherAdder.getTotal() << std::endl;
return 0;
}

编译并执行后,这段代码会产生:

Total: 60
Another Total: 150

在这个示例中,addNum 和 getTotal 构成了公共接口。用户与这些方法交互。total 变量是 private 的,意味着 Adder 类之外的代码无法直接访问或修改它。这保护了总和的完整性。如果我们后来决定以不同的方式存储 total(例如,使用 long long 表示更大的总和),我们可以更改 private 部分,而不会影响 main.cpp,只要 addNum 和 getTotal 方法保持其签名和行为不变。

在设计类时,仔细考虑哪些构成了必要的接口(public),哪些是实现细节(private)。努力设计最小、完整且稳定的接口。一个精心设计的、独立于其底层实现的接口,能够带来更健壮、更灵活且更易于维护的软件。

对实现的更改可能需要重新编译使用该类的文件,但关键在于,如果公共接口保持稳定,它们不应要求更改这些文件的源代码。