C++之数据封装、接口详解
一、数据封装:
所有的 C++ 程序都有以下两个基本要素:
1、程序语句(代码):这是程序中执行动作的部分,它们被称为函数。
2、程序数据:数据是程序的信息,会受到程序函数的影响。
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。例如:
class Box{ public: double getVolume(void) { return length * breadth * height; } private: double length; // 长度 double breadth; // 宽度 double height; // 高度};
变量 length、breadth 和 height 都是私有的(private)。这意味着它们只能被 Box 类中的其他成员访问,而不能被程序中其他部分访问。这是实现封装的一种方式。
为了使类中的成员变成公有的(即,程序中的其他部分也能访问),必须在这些成员前使用 public 关键字进行声明。所有定义在 public 标识符后边的变量或函数可以被程序中所有其他的函数访问。
把一个类定义为另一个类的友元类,会暴露实现细节,从而降低了封装性。理想的做法是尽可能地对外隐藏每个类的实现细节。
数据封装的实例
C++ 程序中,任何带有公有和私有成员的类都可以作为数据封装和数据抽象的实例。请看下面的实例
#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;}
当上面的代码被编译和执行时,它会产生下列结果:
Total60
上面的类把数字相加,并返回总和。公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是对外隐藏的,用户不需要了解它,但它又是类能正常工作所必需的。
设计策略
通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。
这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。
二、接口(抽象类)
**
**
接口描述了类的行为和功能,而不需要完成类的特定实现。
C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 "= 0" 来指定的,如下所示:
class Box{ public: // 纯虚函数 virtual double getVolume() = 0; private: double length; // 长度 double breadth; // 宽度 double height; // 高度};
**
**
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类。
抽象类的实例
请看下面的实例,基类 Shape 提供了一个接口 getArea() ,在两个派生类 Rectangle 和 Triangle 中分别实现了 getArea() :
#include <iostream>
using namespace std;
// 基类class Shape {public: // 提供接口框架的纯虚函数 virtual int getArea() = 0; void setWidth(int w){ width = w; } void setHeight(int h){ height = h; }protected: int width; int height;};
// 派生类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;}
当上面的代码被编译和执行时,它会产生下列结果:
TotalRectangle area:35
TotalTriangle area:17
从上面的实例中,我们可以看到一个抽象类是如何定义一个接口 getArea(),两个派生类是如何通过不同的计算面积的算法来实现这个相同的函数。
设计策略
面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。
这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。