C++ 学习笔记(四):面向对象编程:多态,虚函数,数据抽象,数据封装,抽象类

来源:互联网 发布:剑灵龙男捏脸数据大全 编辑:程序博客网 时间:2024/06/05 02:08

C++ 多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时会根据调用函数的对象的类型来执行不同的函数。

下面的实例中,基类 Shape 被派生为两个类,如下所示:

#include <iostream> using namespace std;class Shape {   protected:      int width, height;   public:      Shape( int a=0, int b=0)      {         width = a;         height = b;      }      int area()      {         cout << "Parent class area :" <<endl;         return 0;      }};class Rectangle: public Shape{   public:      Rectangle( int a=0, int b=0):Shape(a, b) { }      int area ()      {          cout << "Rectangle class area :" <<endl;         return (width * height);       }};class Triangle: public Shape{   public:      Triangle( int a=0, int b=0):Shape(a, b) { }      int area ()      {          cout << "Triangle class area :" <<endl;         return (width * height / 2);       }};// 程序的主函数int main( ){   Shape *shape;   Rectangle rec(10,7);   Triangle  tri(10,5);   // 存储矩形的地址   shape = &rec;   // 调用矩形的求面积函数 area   shape->area();   // 存储三角形的地址   shape = &tri;   // 调用三角形的求面积函数 area   shape->area();   return 0;}

当上面的代码被编译和执行时,它会产生下列结果:

Parent class areaParent class area

导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,如下所示:

class Shape {   protected:      int width, height;   public:      Shape( int a=0, int b=0)      {         width = a;         height = b;      }      virtual int area()      {         cout << "Parent class area :" <<endl;         return 0;      }};

修改后,当编译和执行前面的实例代码时,它会产生以下结果:

Rectangle class areaTriangle class area

此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

正如您所看到的,每个子类都有一个函数 area() 的独立实现。

这就是多态的一般使用方式。

有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的

虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

纯虚函数

您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数

我们可以把基类中的虚函数 area() 改写如下:

class Shape {   protected:      int width, height;   public:      Shape( int a=0, int b=0)      {         width = a;         height = b;      }      // pure virtual function      virtual int area() = 0;};

= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数

C++ 数据抽象

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

数据抽象是一种依赖于接口和实现分离的编程(设计)技术

让我们举一个现实生活中的真实例子,比如一台电视机,您可以打开和关闭、切换频道、调整音量、添加外部组件(如喇叭、录像机、DVD 播放器),但是您不知道它的内部实现细节,也就是说,您并不知道它是如何通过缆线接收信号,如何转换信号,并最终显示在屏幕上。

因此,我们可以说电视把它的内部实现和外部接口分离开了,您无需知道它的内部实现原理,直接通过它的外部接口(比如电源按钮、遥控器、声量控制器)就可以操控电视。

现在,让我们言归正传,就 C++ 编程而言,C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法,也就是说,外界实际上并不清楚类的内部实现。

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

在 C++ 中,我们使用类来定义我们自己的抽象数据类型(ADT)。您可以使用类 ostream 的 cout 对象来输出数据到标准输出,如下所示:

#include <iostream>using namespace std;int main( ){   cout << "Hello C++" <<endl;   return 0;}

在这里,您不需要理解 cout 是如何在用户的屏幕上显示文本。您只需要知道公共接口即可,cout 的底层实现可以自由改变。

访问标签强制抽象

在 C++ 中,我们使用访问标签来定义类的抽象接口。一个类可以包含零个或多个访问标签

  • 使用公共标签定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。

  • 使用私有标签定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节。

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

数据抽象的好处

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

  • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。

  • 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。

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

数据抽象的实例

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;}

当上面的代码被编译和执行时,它会产生下列结果:

Total 60

面的类把数字相加,并返回总和。公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是用户不需要了解的,但又是类能正常工作所必需的。

设计策略

抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变

在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。

C++ 数据封装

所有的 C++ 程序都有以下两个基本要素:

  • 程序语句(代码):这是程序中执行动作的部分,它们被称为函数。
  • 程序数据:数据是程序的信息,会受到程序函数的影响。

封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。

数据封装引申出了另一个重要的 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++ 接口(抽象类)

接口描述了类的行为和功能,而不需要完成类的特定实现

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;}

当上面的代码被编译和执行时,它会产生下列结果:

Total Rectangle area: 35Total Triangle area: 17

从上面的实例中,我们可以看到一个抽象类是如何定义一个接口 getArea(),两个派生类是如何通过不同的计算面积的算法来实现这个相同的函数。

设计策略

面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。

外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。

这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。

0 0
原创粉丝点击