【 1. 数据抽象 】
- 数据抽象 是指 只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。数据抽象是一种依赖于 接口和实现分离 的编程设计技术。
- 数据抽象的好处:
- 安全。类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
- 可持续。类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。
- 易维护。如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据,如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响;如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。
1.1 访问标签强制抽象
- C++ 程序中,任何带有公有和私有成员的类都可以作为数据抽象的实例。
- 在 C++ 中,我们使用 访问标签(即类成员的访问修饰符public、private、protected) 来定义类的抽象接口。一个类可以包含零个或多个访问标签:
- 使用 公共标签public 定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。
- 使用 私有标签private 定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节。
- 实例
把数字相加,并返回总和;公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是用户不需要了解的,但又是类能正常工作所必需的。
#include <iostream>
using namespace std;
class Adder{
public:
Adder(int i = 0)
{
total = i;
}
void addNum(int number)
{
total += number;
}
int getTotal()
{
return total;
};
private:
int total;
};
int main( )
{
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0;
}
1.2 设计策略
- 抽象把代码分离为接口和实现。所以在设计组件时,必须 保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。
- 在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。
【 2. 数据封装 】
- 封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP面向对象 概念,即 数据隐藏。
- 数据封装 是一种 把数据和操作数据的函数捆绑在一起 的机制。
2.1 private 关键字实现数据封装
- C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。 private 关键字是实现封装的一种方式,默认情况下,在类中定义的所有项目都是私有的。
- 把一个类定义为另一个类的 友元类会暴露实现细节,从而降低封装性(因为友元类可以访问原类的public、private、protected 所有成员)。理想的做法是 尽可能 地对外隐藏每个类的实现细节。
- C++ 程序中,任何带有公有和私有成员的类都可以作为数据封装和数据抽象的实例。
- 实例1
下面代码中,变量 length、breadth 和 height 都是私有的(private)。这意味着它们只能被 Box 类中的其他成员访问,而不能被程序中其他部分访问。
class Box
{
private:
double length;
double breadth;
double height;
public:
double getVolume(void)
{
return length * breadth * height;
}
};
- 实例2
如下所示,把数字相加,并返回总和。公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是对外隐藏的,用户不需要了解它,但它又是类能正常工作所必需的。
#include <iostream>
using namespace std;
class Adder{
public:
Adder(int i = 0)
{
total = i;
}
void addNum(int number)
{
total += number;
}
int getTotal()
{
return total;
};
private:
int total;
};
int main( )
{
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0;
}
2.2 设计策略
- 我们 通常都会 设置类成员状态为私有(private),这样才能保证良好的封装性,这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。
【 3. 接口/抽象类 】
- 接口描述了类的行为和功能,而不需要完成类的特定实现。
- C++ 接口是使用 抽象类(abstract base class,也称为ABC) 来实现的。设计抽象类的目的是为了给其他类提供一个可以继承的适当的基类。
3.1 抽象类
class Box
{
public:
virtual double getVolume() = 0;
private:
double length;
double breadth;
double height;
};
- 抽象类不能被用于实例化对象,否则会导致编译错误。(相对应的,可以用于实例化对象的类被称为 具体类)。
- 如果一个 ABC抽象类 的子类需要被实例化,则必须实现每个纯虚函数,否则会导致编译错误,这也意味着 C++ 支持使用 ABC 声明接口。
- 实例
基类 Shape 提供了一个接口 getArea(),在两个派生类 Rectangle 和 Triangle 中分别实现了 getArea(),我们可以看到:一个抽象类是如何定义一个接口 getArea(),两个派生类是如何通过不同的计算面积的算法来实现这个相同的函数。
#include <iostream>
using namespace std;
class Shape
{
protected:
int width;
int height;
public:
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
};
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}
3.2 设计策略
- 面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口;然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
- 外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。
- 这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。
【 4. 三者的对比 】
| 数据抽象 | 数据封装 | 接口/抽象类 |
---|
定义 | 通过类内的访问修饰符public和private将公有信息即接口公开,私有信息(实现或隐私数据)隐藏,使得接口和实现分离。 | 把数据和实现即操作数据的函数即捆绑在一起,即放在一个类中。 | 将抽象类作为一个模板,进行派生 |
意义 | 保护了数据/实现方法、易维护、可持续 | 易维护 | 便于继承相关操作 |