返回介绍

14.1 包含对象成员的类

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

首先介绍包含对象成员的类。有一些类(如 string 类和第 16 章将介绍的标准 C++类模板)为表示类中的组件提供了方便的途径。下面来看一个具体的例子。

学生是什么?入学者?参加研究的人?残酷现实社会的避难者?有姓名和一系列考试分数的人?显然,最后一个定义完全没有表示出人的特征,但非常适合于简单的计算机表示。因此,让我们根据该定义来开发 Student 类。

将学生简化成姓名和一组考试分数后,可以使用一个包含两个成员的类来表示它:一个成员用于表示姓名,另一个成员用于表示分数。对于姓名,可以使用字符数组来表示,但这将限制姓名的长度。当然,也可以使用 char 指针和动态内存分配,但正如第 12 章指出的,这将要求提供大量的支持代码。一种更好的方法是,使用一个由他人开发好的类的对象来表示。例如,可以使用一个 String 类(参见第 12 章)或标准 C++ string 类的对象来表示姓名。较简单的选择是使用 string 类,因为 C++库提供了这个类的所有实现代码,且其实现更完美。要使用 String 类,您必须在项目中包含实现文件 string1.cpp。

对于考试分数,存在类似的选择。可以使用一个定长数组,这限制了数组的长度;可以使用动态内存分配并提供大量的支持代码;也可以设计一个使用动态内存分配的类来表示该数组;还可以在标准 C++库中查找一个能够表示这种数据的类。

自己开发这样的类一点问题也没有。开发简单的版本并不那么难,因为 double 数组与 char 数组有很多相似之处,因此可以根据 String 类来设计表示 double 数组的类。事实上,本书以前的版本就这样做过。

当然,如果 C++库提供了合适的类,实现起来将更简单。C++库确实提供了一个这样的类,它就是 valarray。

14.1.1 valarray 类简介

valarray 类是由头文件 valarray 支持的。顾名思义,这个类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作。valarray 被定义为一个模板类,以便能够处理不同的数据类型。本章后面将介绍如何定义模板类,但就现在而言,您只需知道如何使用模板类即可。

模板特性意味着声明对象时,必须指定具体的数据类型。因此,使用 valarray 类来声明一个对象时,需要在标识符 valarray 后面加上一对尖括号,并在其中包含所需的数据类型:

第 4 章介绍 vector 和 array 类时,您见过这种语法,它非常简单。这些类也可用于存储数字,但它们提供的算术支持没有 valarray 多。

这是您需要学习的唯一新语法,它非常简单。

类特性意味着要使用 valarray 对象,需要了解这个类的构造函数和其他类方法。下面是几个使用其构造函数的例子:

从中可知,可以创建长度为零的空数组、指定长度的空数组、所有元素度被初始化为指定值的数组、用常规数组中的值进行初始化的数组。在 C++11 中,也可使用初始化列表:

下面是这个类的一些方法。

  • operator :让您能够访问各个元素。
  • size( ):返回包含的元素数。
  • sum( ):返回所有元素的总和。
  • max( ):返回最大的元素。
  • min( ):返回最小的元素。

还有很多其他的方法,其中的一些将在第 16 章介绍;但就这个例子而言,上述方法足够了。

14.1.2 Student 类的设计

至此,已经确定了 Student 类的设计计划:使用一个 string 对象来表示姓名,使用一个 valarray<double>来表示考试分数。那么如何设计呢?您可能想以公有的方式从这两个类派生出 Student 类,这将是多重公有继承,C++允许这样做,但在这里并不合适,因为学生与这些类之间的关系不是 is-a 模型。学生不是姓名,也不是一组考试成绩。这里的关系是 has-a,学生有姓名,也有一组考试分数。通常,用于建立 has-a 关系的 C++技术是组合(包含),即创建一个包含其他类对象的类。例如,可以将 Student 类声明为如下所示:

同样,上述类将数据成员声明为私有的。这意味着 Student 类的成员函数可以使用 string 和 valarray<double>类的公有接口来访问和修改 name 和 scores 对象,但在类的外面不能这样做,而只能通过 Student 类的公有接口访问 name 和 score(请参见图 14)。对于这种情况,通常被描述为 Student 类获得了其成员对象的实现,但没有继承接口。例如,Student 对象使用 string 的实现,而不是 char * name 或 char name [26]实现来保存姓名。但 Student 对象并不是天生就有使用函数 string operator+=( ) 的能力。

图 14.1 对象中的对象:包含

接口和实现

使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是 is-a 关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是 has-a 关系的组成部分。

对于 has-a 关系来说,类对象不能自动获得被包含对象的接口是一件好事。例如,string 类将+运算符重载为将两个字符串连接起来;但从概念上说,将两个 Student 对象串接起来是没有意义的。这也是这里不使用公有继承的原因之一。另一方面,被包含的类的接口部分对新类来说可能是有意义的。例如,可能希望使用 string 接口中的 operator<( ) 方法将 Student 对象按姓名进行排序,为此可以定义 Student::Operator<( ) 成员函数,它在内部使用函数 string::Operator<( )。下面介绍一些细节。

14.1.3 Student 类示例

现在需要提供 Student 类的定义,当然它应包含构造函数以及一些用作 Student 类接口的方法。程序清单 14.1 是 Student 类的定义,其中所有构造函数都被定义为内联的;它还提供了一些用于输入和输出的友元函数。

程序清单 14.1 studentc.h

为简化表示,Student 类的定义中包含下述 typedef:

这样,在以后的代码中便可以使用表示 ArrayDb,而不是 std::valarray<double>,因此类方法和友元函数可以使用 ArrayDb 类型。将该 typedef 放在类定义的私有部分意味着可以在 Student 类的实现中使用它,但在 Student 类外面不能使用。

请注意关键字 explicit 的用法:

本书前面说过,可以用一个参数调用的构造函数将用作从参数类型到类类型的隐式转换函数;但这通常不是好主意。在上述第二个构造函数中,第一个参数表示数组的元素个数,而不是数组中的值,因此将一个构造函数用作 int 到 Student 的转换函数是没有意义的,所以使用 explicit 关闭隐式转换。如果省略该关键字,则可以编写如下所示的代码:

在这里,马虎的程序员键入了 doh 而不是 doh[0]。如果构造函数省略了 explicit,则将使用构造函数调用 Student(5)将 5 转换为一个临时 Student 对象,并使用“Nully”来设置成员 name 的值。因此赋值操作将使用临时对象替换原来的 doh 值。使用了 explicit 后,编译器将认为上述赋值运算符是错误的。

C++和约束

C++包含让程序员能够限制程序结构的特性——使用 explicit 防止单参数构造函数的隐式转换,使用 const 限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。

1.初始化被包含的对象

构造函数全都使用您熟悉的成员初始化列表语法来初始化 name 和 score 成员对象。在前面的一些例子中,构造函数用这种语法来初始化内置类型的成员:

上述代码在成员初始化列表中使用的是数据成员的名称(qsize)。另外,前面介绍的示例中的构造函数还使用成员初始化列表初始化派生对象的基类部分:

对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数。对于成员对象,构造函数则使用成员名。例如,请看程序清单 14.3 的最后一个构造函数:

因为该构造函数初始化的是成员对象,而不是继承的对象,所以在初始化列表中使用的是成员名,而不是类名。初始化列表中的每一项都调用与之匹配的构造函数,即 name(str) 调用构造函数 string(const char *),scores(pd, n) 调用构造函数 ArrayDb(const double *, int)。

如果不使用初始化列表语法,情况将如何呢?C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。

初始化顺序

当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。例如,假设 Student 构造函数如下:

则 name 成员仍将首先被初始化,因为在类定义中它首先被声明。对于这个例子来说,初始化顺序并不重要,但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。

2.使用被包含对象的接口

被包含对象的接口不是公有的,但可以在类方法中使用它。例如,下面的代码说明了如何定义一个返回学生平均分数的函数:

上述代码定义了可由 Student 对象调用的方法,该方法内部使用了 valarray 的方法 size( ) 和 sum( )。这是因为 scores 是一个 valarray 对象,所以它可以调用 valarray 类的成员函数。总之,Student 对象调用 Student 的方法,而后者使用被包含的 valarray 对象来调用 valarray 类的方法。

同样,可以定义一个使用 string 版本的<<运算符的友元函数:

因为 stu.name 是一个 string 对象,所以它将调用函数 operatot<<(ostream &, const string &),该函数位于 string 类中。注意,operator<<(ostream & os, const Student & stu) 必须是 Student 类的友元函数,这样才能访问 name 成员。另一种方法是,在该函数中使用公有方法 Name( ),而不是私有数据成员 name。

同样,该函数也可以使用 valarray 的<<实现来进行输出,不幸的是没有这样的实现;因此,Student 类定义了一个私有辅助方法来处理这种任务:

通过使用这样的辅助方法,可以将零乱的细节放在一个地方,使得友元函数的编码更为整洁:

辅助函数也可用作其他用户级输出函数的构建块——如果您选择提供这样的函数的话。

程序清单 14.2 是 Student 类的类方法文件,其中包含了让您能够使用[ ]运算符来访问 Student 对象中各项成绩的方法。

程序清单 14.2 student.cpp

除私有辅助方法外,程序清单 14.2 并没有新增多少代码。使用包含让您能够充分利用已有的代码。

3.使用新的 Student 类

下面编写一个小程序来测试这个新的 Student 类。出于简化的目的,该程序将使用一个只包含 3 个 Student 对象的数组,其中每个对象保存 5 个考试成绩。另外还将使用一个不复杂的输入循环,该循环不验证输入,也不让用户中途退出。程序清单 14.3 列出了该测试程序,请务必将该程序与 Student.cpp 一起进行编译。

程序清单 14.3 use_stuc.cpp

下面是程序清单 14.1~程序清单 14.3 组成的程序的运行情况:

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

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

发布评论

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