- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
13.6 抽象基类
至此,介绍了简单继承和较复杂的多态继承。接下来更为复杂的是抽象基类(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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论