返回介绍

13.6 抽象基类

发布于 2024-10-08 23:14:10 字数 5552 浏览 0 评论 0 收藏 0

至此,介绍了简单继承和较复杂的多态继承。接下来更为复杂的是抽象基类(abstract base class,ABC)。我们来看一些可使用 ABC 的编程情况。

有时候,使用 is-a 规则并不是看上去的那样简单。例如,假设您正在开发一个图形程序,该程序会显示圆和椭圆等。圆是椭圆的一个特殊情况——长轴和短轴等长的椭圆。因此,所有的圆都是椭圆,可以从 Ellipse 类派生出 Circle 类。但涉及到细节时,将发现很多问题。

首先考虑 Ellipse 类包含的内容。数据成员可以包括椭圆中心的坐标、半长轴(长轴的一半)、短半轴(短轴的一半)以及方向角(水平坐标轴与长轴之间的角度)。另外,还可以包括一些移动椭圆、返回椭圆面积、旋转椭圆以及缩放长半轴和短半轴的方法:

现在假设从 Ellipse 类派生出一个 Circle 类:

虽然圆是一种椭圆,但是这种派生是笨拙的。例如,圆只需要一个值(半径)就可以描述大小和形状,并不需要有长半轴(a)和短半轴(b)。Circle 构造函数可以通过将同一个值赋给成员 a 和 b 来照顾这种情况,但将导致信息冗余。angle 参数和 Rotate( ) 方法对圆来说没有实际意义;而 Scale( ) 方法(顾名思义)会将两个轴作不同的缩放,将圆变成椭圆。可以使用一些技巧来修正这些问题,例如在 Circle 类中的私有部分包含重新定义的 Rotate( ) 方法,使 Rotate( ) 不能以公有方式用于圆。但总的来说,不使用继承,直接定义 Circle 类更简单:

现在,类只包含所需的成员。但这种解决方法的效率也不高。Circle 和 Ellipse 类有很多共同点,将它们分别定义则忽略了这一事实。

还有一种解决方法,即从 Ellipse 和 Circle 类中抽象出它们的共性,将这些特性放到一个 ABC 中。然后从该 ABC 派生出 Circle 和 Ellipse 类。这样,便可以使用基类指针数组同时管理 Circle 和 Ellipse 对象,即可以使用多态方法)。在这个例子中,这两个类的共同点是中心坐标、Move( ) 方法(对于这两个类是相同的)和 Area( ) 方法(对于这两个类来说,是不同的)。确实,甚至不能在 ABC 中实现 Area( ) 方法,因为它没有包含必要的数据成员。C++通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0,参见 Area( ) 方法:

当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的 ABC,必须至少包含一个纯虚函数。原型中的=0 使虚函数成为纯虚函数。这里的方法 Area( ) 没有定义,但 C++甚至允许纯虚函数有定义。例如,也许所有的基类方法都与 Move( ) 一样,可以在基类中进行定义,但您仍需要将这个类声明为抽象的。在这种情况下,可以将原型声明为虚的:

这将使基类成为抽象的,但您仍可以在实现文件中提供方法的定义:

总之,在原型中使用=0 指出类是一个抽象基类,在类中可以不定义该函数。

现在,可以从 BaseEllipse 类派生出 Ellips 类和 Circle 类,添加所需的成员来完成每个类。需要注意的一点是,Circle 类总是表示圆,而 Ellipse 类总是表示椭圆——也可以是圆。然而,Ellipse 类圆可被重新缩放为非圆,而 Ciecle 类圆必须始终为圆。

使用这些类的程序将能够创建 Ellipse 对象和 Circle 对象,但是不能创建 BaseEllipse 对象。由于 Circle 和 Ellipse 对象的基类相同,因此可以用 BaseEllipse 指针数组同时管理这两种对象。像 Circle 和 Ellipse 这样的类有时被称为具体(concrete)类,这表示可以创建这些类型的对象。

总之,ABC 描述的是至少使用一个纯虚函数的接口,从 ABC 派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。

13.6.1 应用 ABC 概念

您可能希望看到一个完整的 ABC 示例,因此这里将这一概念用于 Brass 和 BrassPlus 账户,首先定义一个名为 AcctABC 的 ABC。这个类包含 Brass 和 BrassPlus 类共有的所有方法和数据成员,而那些在 BrassPlus 类和 Brass 类中的行为不同的方法应被声明为虚函数。至少应有一个虚函数是纯虚函数,这样才能使 AcctABC 成为抽象类。

程序清单 13.11 的头文件声明了 AcctABC 类(ABC)、Brass 类和 BrassPlus 类(两者都是具体类)。为帮助派生类访问基类数据,AcctABC 提供了一些保护方法;派生类方法可以调用这些方法,但它们并不是派生类对象的公有接口的组成部分。AcctABC 还提供一个保护成员函数,用于处理格式化(以前是使用非成员函数处理的)。另外,AcctABC 类还有两个纯虚函数,所以它确实是抽象类。

程序清单 13.11 acctabc.h

接下来需要实现那些不是内联函数的方法,如程序清单 13.12 所示。

程序清单 13.12 acctABC.cpp

保护方法 FullName( ) 和 AcctNum( ) 提供了对数据成员 fullName 和 acctNum 的只读访问,使得可以进一步定制每个派生类的 ViewAcct( )。

这个版本在设置输出格式方面做了两项改进。前一个版本使用两个函数调用来设置输出格式,并使用一个函数调用来恢复格式:

这个版本定义了一个结构,用于存储两项格式设置;并使用该结构来设置和恢复格式,因此只需两个函数调用:

因此代码更整洁。

旧版本存在的问题是,setFormat( ) 和 restore( ) 都是独立的函数,这些函数与客户定义的同名函数发生冲突。解决这种问题的方式有多种,一种方式是将这些函数声明为静态的,这样它们将归文件 brass.cpp 及其继任 acctabc.cpp 私有。另一种方式是,将这些函数以及结构 Formatting 放在一个独立的名称空间中。但这个示例探讨的主题之一是保护访问权限,因此将这些结构和函数放在了类定义的保护部分。这使得它们对基类和派生类可用,同时向外隐藏了它们。

对于 Brass 和 BrassPlus 账户的这种新实现,使用方式与旧实现相同,因为类方法的名称和接口都与以前一样。例如,为使程序清单 13.10 能够使用新的实现,需要采取下面的步骤将 usebrass2.cpp 转换为 usebrass3.cpp:

  • 使用 acctabc.cpp 而不是 brass.cpp 来链接 usebrass2.cpp。
  • 包含文件 acctabc.h,而不是 brass.h。
  • 将下面的代码:

替换为:

程序清单 13.13 是修改后的文件,并将其重命名为 usebrass3.cpp。

程序清单 13.13 usebrass3.cpp

该程序本身的行为与非抽象基类版本相同,因此如果输入与给程序清单 13.10 提供的输入相同,输出也将相同。

13.6.2 ABC 理念

在处理继承的问题上,RatedPlayer 示例使用的方法比较随意,而 ABC 方法比它更具系统性、更规范。设计 ABC 之前,首先应开发一个模型——指出编程问题所需的类以及它们之间相互关系。一种学院派思想认为,如果要设计类继承层次,则只能将那些不会被用作基类的类设计为具体的类。这种方法的设计更清晰,复杂程度更低。

可以将 ABC 看作是一种必须实施的接口。ABC 要求具体派生类覆盖其纯虚函数——迫使派生类遵循 ABC 设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用 ABC 使得组件设计人员能够制定“接口约定”,这样确保了从 ABC 派生的所有组件都至少支持 ABC 指定的功能。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文