- 内容提要
- 前言
- 第 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 复习题答案
15.1 友元
本书前面的一些示例将友元函数用于类的扩展接口中,类并非只能拥有友元函数,也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类为友元是由类定义的,而不能从外部强加友情。因此,尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相悖;相反,它们提高了公有接口的灵活性。
15.1.1 友元类
什么时候希望一个类成为另一个类的友元呢?我们来看一个例子。假定需要编写一个模拟电视机和遥控器的简单程序。决定定义一个 Tv 类和一个 Remote 类,来分别表示电视机和遥控器。很明显,这两个类之间应当存在某种关系,但是什么样的关系呢?遥控器并非电视机,反之亦然,所以公有继承的 is-a 关系并不适用。遥控器也非电视机的一部分,反之亦然,因此包含或私有继承和保护继承的 has-a 关系也不适用。事实上,遥控器可以改变电视机的状态,这表明应将 Romote 类作为 Tv 类的一个友元。
首先定义 Tv 类。可以用一组状态成员(描述电视各个方面的变量)来表示电视机。下面是一些可能的状态:
- 开/关;
- 频道设置;
- 音量设置;
- 有线电视或天线调节模式;
- TV 调谐或 A/V 输入。
调节模式指的是,在美国,对于有线接收和 UHF 广播接收,14 频道和 14 频道以上的频道间隔是不同的。输入选择包括 TV(有线 TV 或广播 TV)和 DVD。有些电视机可能提供更多的选择,如多种 DVD/蓝光输入,但对于这个示例的目的而言,这个清单足够了。
另外,电视机还有一些不是状态变量的参数。例如,可接收频道数随电视机而异,可以包括一个记录这个值的成员。
接下来,必须给类提供一些修改这些设置的方法。当前,很多电视机都将控件藏在面板后面,但大多数电视机还是可以在不使用遥控器的情况下进行换台等工作的,通常只能逐频道换台,而不能随意选台。同样,通常还有两个按钮,分别用来增加和降低音量。
遥控器的控制能力应与电视机内置的控制功能相同,它的很多方法都可通过使用 Tv 方法来实现。另外,遥控器通常都提供随意选择频道的功能,即可以直接从 2 频道换到 20 频道,并不用逐次切换频道。另外,很多遥控器都有多种工作模式,如用作电视控制器和 DVD 遥控器。
这些考虑因素表明,定义应类似于程序清单 15.1。定义中包括一些被定义为枚举的常数。下面的语句使 Remote 成为友元类:
友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。由于 Remote 类提到了 Tv 类,所以编译器必须了解 Tv 类后,才能处理 Remote 类,为此,最简单的方法是首先定义 Tv 类。也可以使用前向声明(forward delaration),这将稍后介绍。
程序清单 15.1 tv.h
在程序清单 15.1 中,大多数类方法都被定义为内联的。除构造函数外,所有的 Romote 方法都将一个 Tv 对象引用作为参数,这表明遥控器必须针对特定的电视机。程序清单 15.2 列出了其余的定义。音量设置函数将音量成员增减一个单位,除非声音到达最大或最小。频道选择函数使用循环方式,最低的频道设置为 1,它位于最高的频道设置 maxchannel 之后。
很多方法都使用条件运算符在两种状态之间切换:
如果两种状态值分别为 true(1)和 false(0),则可以结合使用将在附录 E 讨论的按位异或和赋值运算符(^=)来简化上述代码:
事实上,在单个无符号 char 变量中可存储多达 8 个双状态设置,分别对它们进行切换;但现在已经不用这样做了,使用附录 E 中讨论的按位运算符就可以完成。
程序清单 15.2 tv.cpp
程序清单 15.3 是一个简短的程序,可以测试一些特性。另外,可使用同一个遥控器控制两台不同的电视机。
程序清单 15.3 use_tv.cpp
下面是程序清单 15.1~程序清单 15.3 组成的程序的输出:
这个练习的主要目的在于表明,类友元是一种自然用语,用于表示一些关系。如果不使用某些形式的友元关系,则必须将 Tv 类的私有部分设置为公有的,或者创建一个笨拙的、大型类来包含电视机和遥控器。这种解决方法无法反应这样的事实,即同一个遥控器可用于多台电视机。
15.1.2 友元成员函数
从上一个例子中的代码可知,大多数 Remote 方法都是用 Tv 类的公有接口实现的。这意味着这些方法不是真正需要作为友元。事实上,唯一直接访问 Tv 成员的 Remote 方法是 Remote::set_chan( ),因此它是唯一需要作为友元的方法。确实可以选择仅让特定的类成员成为另一个类的友元,而不必让整个类成为友元,但这样做稍微有点麻烦,必须小心排列各种声明和定义的顺序。下面介绍其中的原因。
让 Remote::set_chan( ) 成为 Tv 类的友元的方法是,在 Tv 类声明中将其声明为友元:
然而,要使编译器能够处理这条语句,它必须知道 Remote 的定义。否则,它无法知道 Remote 是一个类,而 set_chan 是这个类的方法。这意味着应将 Remote 的定义放到 Tv 的定义前面。Remote 的方法提到了 Tv 对象,而这意味着 Tv 定义应当位于 Remote 定义之前。避开这种循环依赖的方法是,使用前向声明(forward declaration)。为此,需要在 Remote 定义的前面插入下面的语句:
这样,排列次序应如下:
能否像下面这样排列呢?
答案是不能。原因在于,在编译器在 Tv 类的声明中看到 Remote 的一个方法被声明为 Tv 类的友元之前,应该先看到 Remote 类的声明和 set_chan( ) 方法的声明。
还有一个麻烦。程序清单 15.1 的 Remote 声明包含了内联代码,例如:
由于这将调用 Tv 的一个方法,所以编译器此时必须已经看到了 Tv 类的声明,这样才能知道 Tv 有哪些方法,但正如看到的,该声明位于 Remote 声明的后面。这种问题的解决方法是,使 Remote 声明中只包含方法声明,并将实际的定义放在 Tv 类之后。这样,排列顺序将如下:
Remote 方法的原型与下面类似:
检查该原型时,所有的编译器都需要知道 Tv 是一个类,而前向声明提供了这样的信息。当编译器到达真正的方法定义时,它已经读取了 Tv 类的声明,并拥有了编译这些方法所需的信息。通过在方法定义中使用 inline 关键字,仍然可以使其成为内联方法。程序清单 15.4 列出了修订后的头文件。
程序清单 15.4 tvfm.h
如果在 tv.cpp 和 use_tv.cpp 中包含 tvfm.h 而不是 tv.h,程序的行为与前一个程序相同,区别在于,只有一个 Remote 方法是 Tv 类的友元,而在原来的版本中,所有的 Remote 方法都是 Tv 类的友元。图 15.1 说明了这种区别。
图 15.1 类友元与类成员友元
本书前面介绍过,内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中。在这个例子中,内联定义位于头文件中,因此在使用函数的文件中包含头文件可确保将定义放在正确的地方。也可以将定义放在实现文件中,但必须删除关键字 inline,这样函数的链接性将是外部的。
顺便说一句,让整个 Remote 类成为友元并不需要前向声明,因为友元语句本身已经指出 Remote 是一个类:
15.1.3 其他友元关系
除本章前面讨论的,还有其他友元和类的组合形式,下面简要地介绍其中的一些。
假设由于技术进步,出现了交互式遥控器。例如,交互式遥控器让您能够回答电视节目中的问题,如果回答错误,电视将在控制器上产生嗡嗡声。忽略电视使用这种设施安排观众进入节目的可能性,我们只看 C++的编程方面。新的方案将受益于相互的友情,一些 Remote 方法能够像前面那样影响 Tv 对象,而一些 Tv 方法也能影响 Remote 对象。这可以通过让类彼此成为对方的友元来实现,即除了 Remote 是 Tv 的友元外,Tv 还是 Remote 的友元。需要记住的一点是,对于使用 Remote 对象的 Tv 方法,其原型可在 Remote 类声明之前声明,但必须在 Remote 类声明之后定义,以便编译器有足够的信息来编译该方法。这种方案与下面类似:
由于 Remote 的声明位于 Tv 声明的后面,所以可以在类声明中定义 Remote::volup( ),但 Tv::buzz( ) 方法必须在 Tv 声明的外部定义,使其位于 Remote 声明的后面。如果不希望 buzz( ) 是内联的,则应在一个单独的方法定义文件中定义它。
15.1.4 共同的友元
需要使用友元的另一种情况是,函数需要访问两个类的私有数据。从逻辑上看,这样的函数应是每个类的成员函数,但这是不可能的。它可以是一个类的成员,同时是另一个类的友元,但有时将函数作为两个类的友元更合理。例如,假定有一个 Probe 类和一个 Analyzer 类,前者表示某种可编程的测量设备,后者表示某种可编程的分析设备。这两个类都有内部时钟,且希望它们能够同步,则应该包含下述代码行:
前向声明使编译器看到 Probe 类声明中的友元声明时,知道 Analyzer 是一种类型。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论