跳到主要内容

C++ 数据抽象

什么是数据抽象

数据抽象是一种编程思想,即只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。

让我们举一个现实生活中的例子:

比如一辆汽车,你可以启动、加速、刹车、转向、添加额外的配件(如摄像头、导航、车载多媒体终端等),但是你不知道这辆车的内部实现细节。也就是说,你并不知道它是如何通过汽油或电能启动的,传动机构如何实现调速,如何监测车速和续航里程并显示在中控屏幕。而且通常情况下,你并不想了解这些细节,你只需要知道如何驱动它、使用它即可。

因此,我们可以说汽车把它的内部实现和外部接口分离开了,实现了解耦合。你无需知道它的内部实现原理,直接通过它的外部接口(如方向盘、踏板、变速杆等)就可以操控汽车。

在 C++ 编程中,数据抽象同时也是一种依赖于接口和实现分离的编程(设计)技术,通常使用类(class)来抽象数据类型(ADT)。

抽象数据类型

抽象数据类型(Abstract Data Type,简写 ADT)是计算机科学中具有类似行为的特定类别的数据结构的数学模型;或者具有类似语义的一种或多种程序设计语言的数据类型。抽象数据类型需要通过固有数据类型来实现,比如 C++ 的各种基本数据类型。

ADT 抽象数据类型的定义只需要提到可以执行什么操作,而不需要提到如何实现这些操作。也不需要指定如何在内存中组织数据,以及将使用什么算法来实现这些操作。正因如此,所以它被称为“抽象”,实际上,它提供的是一个与实现无关的蓝图(或称为模板)。

只提供设计要点而隐藏实现细节的过程被称为抽象。C++ 面向对象编程思想中,类(class)正是定义抽象数据类型的一大利器!

数据抽象类型 Abstract Data Type

ADT 抽象数据类型的描述包括给出抽象数据类型的名称、数据的集合、数据之间的关系和操作的集合等方面的描述。抽象数据类型的设计者根据这些描述给出操作的具体实现,抽象数据类型的使用者依据这些描述使用抽象数据类型。

抽象数据类型描述的一般形式如下:

ADT 抽象数据类型名称 {
数据对象:
......
数据关系:
......
   操作集合:
操作名1
......
操作名n:
} ADT抽象数据类型名称

例如,线性表这样的抽象数据类型,其数学模型是数据元素的集合,该集合内的元素有这样的关系:除第一个和最后一个外,每个元素有唯一的前趋和唯一的后继。可以有这样一些操作:插入一个元素、删除一个元素等。

实际上,C++ 标准库中定义的 ArrayListMapQueueSetStackTableTreeVector 等类型都是 ADT,而这些 ADT 中的每一个都具有许多实现,即 CDT(Concrete Data Type,具体数据类型)。

抽象数据类型总结:

  • 定义:一个数学模型以及定义在该模型上的一组操作。在 C++ 中作为一种数据类型,只定义行为但不实现。
  • 作用:抽象数据类型可以使我们更容易描述现实世界。例:用线性表描述学生成绩表,用树或图描述遗传关系。
  • 关键:使用它的人可以只关心它的逻辑特征,不需要了解它的存储方式。定义它的人同样不必要关心它如何存储。

类和数据抽象

作为一门面向对象的编程语言,C++ 的类(class)为数据抽象提供了可能。它们可以向外界提供了大量用于操作对象数据的公共方法,并把内部实现细节隐藏起来,因此外界实际上并不清楚类的内部实现。

例如,我们的程序可以调用 sort() 函数,而不需要知道函数中排序数据所用到的算法。实际上,函数排序的底层实现会因库的版本不同而有所差异,但只要接口不变,函数调用就可以照常工作。

我们可以将 ADT 看作一个黑盒,它隐藏了数据类型的内部结构和设计。

C++ 使用访问标签来定义类的抽象接口,访问标签就是指 private:protected:public:

一个类可以包含零个或多个访问标签:

  • 使用公共标签定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。
  • 使用私有标签定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节。

访问标签出现的频率没有限制。 每个访问标签指定了紧随其后的成员定义的访问级别。指定的访问级别会一直有效,直到遇到下一个访问标签或者遇到类主体的关闭右括号为止。

C++ 程序中,任何带有公有和私有成员的类都可以视为数据抽象,即使用访问标签强制抽象

示例

#include <iostream>
using namespace std;

class Muler
{
public:
// 构造函数
Muler(int i = 1) {
total = i;
}

// 对外的接口
void mulNum(int number) {
total *= number;
}

// 对外的接口
int getTotal() {
return total;
};

private:
// 对外隐藏的数据
int total;
};

int main(void)
{
Muler a;

a.mulNum(3);
a.mulNum(7);
a.mulNum(11);

cout << "Total " << a.getTotal() << endl;
return 0;
}

执行 $ g++ main.cpp && ./a.out 编译运行以上示例,输出结果如下:

Total 231

在该示例中,Muler 类的 mulNum()getTotal() 是对外的接口,调用者需要知道它们以便使用类。私有成员 total 是调用者不需要了解的,但又是类能正常工作所必需的。

数据抽象的好处

数据抽象有两个重要的优势:

  1. 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
  2. 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求。

如果只在类的私有部分定义数据成员,该类的开发者就可以随意更改数据。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。而如果数据是公有的,那么任何直接访问旧表示形式的数据成员的函数都可能受到影响。

因此,在使用 C++ 编程时,应当注意以下几点:

  • 使用抽象把代码分离为接口和实现。
  • 在设计组件时,一定要保持接口独立于实现。这样,如果改变底层实现,接口也可以保持不变,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。