返回介绍

11.3 友元

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

您知道,C++控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但是有时候这种限制太严格,以致于不适合特定的编程问题。在这种情况下,C++提供了另外一种形式的访问权限:友元。友元有 3 种:

  • 友元函数;
  • 友元类;
  • 友元成员函数。

通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。下面介绍友元函数,其他两种友元将在第 15 章介绍。

介绍如何成为友元前,先介绍为何需要友元。在为类重载二元运算符时(带两个参数的运算符)常常需要友元。将 Time 对象乘以实数就属于这种情况,下面来看看。

在前面的 Time 类示例中,重载的乘法运算符与其他两种重载运算符的差别在于,它使用了两种不同的类型。也就是说,加法和减法运算符都结合两个 Time 值,而乘法运算符将一个 Time 值与一个 double 值结合在一起。这限制了该运算符的使用方式。记住,左侧的操作数是调用对象。也就是说,下面的语句:

将被转换为下面的成员函数调用:

但下面的语句又如何呢?

从概念上说,2.75 * B 应与 B *2.75 相同,但第一个表达式不对应于成员函数,因为 2.75 不是 Time 类型的对象。记住,左侧的操作数应是调用对象,但 2.75 不是对象。因此,编译器不能使用成员函数调用来替换该表达式。

解决这个难题的一种方式是,告知每个人(包括程序员自己),只能按 B * 2.75 这种格式编写,不能写成 2.75 * B。这是一种对服务器友好-客户警惕的(server-friendly, client-beware)解决方案,与 OOP 无关。

然而,还有另一种解决方式——非成员函数(记住,大多数运算符都可以通过成员或非成员函数来重载)。非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数。这样,编译器能够将下面的表达式:

与下面的非成员函数调用匹配:

该函数的原型如下:

对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数,也就是说,double 值乘以 Time 值。

使用非成员函数可以按所需的顺序获得操作数(先是 double,然后是 Time),但引发了一个新问题:非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。

11.3.1 创建友元

创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend:

该原型意味着下面两点:

  • 虽然 operator *( ) 函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
  • 虽然 operator *( ) 函数不是成员函数,但它与成员函数的访问权限相同。

第二步是编写函数定义。因为它不是成员函数,所以不要使用 Time::限定符。另外,不要在定义中使用关键字 friend,定义应该如下:

有了上述声明和定义后,下面的语句:

将转换为如下语句,从而调用刚才定义的非成员友元函数:

总之,类的友元函数是非成员函数,其访问权限与成员函数相同。

友元是否有悖于 OOP

乍一看,您可能会认为友元违反了 OOP 数据隐藏的原则,因为友元机制允许非成员函数访问私有数据。然而,这个观点太片面了。相反,应将友元函数看作类的扩展接口的组成部分。例如,从概念上看,double 乘以 Time 和 Time 乘以 double 是完全相同的。也就是说,前一个要求有友元函数,后一个使用成员函数,这是 C++句法的结果,而不是概念上的差别。通过使用友元函数和类方法,可以用同一个用户接口表达这两种操作。另外请记住,只有类声明可以决定哪一个函数是友元,因此类声明仍然控制了哪些函数可以访问私有数据。总之,类方法和友元只是表达类接口的两种不同机制。

实际上,按下面的方式对定义进行修改(交换乘法操作数的顺序),可以将这个友元函数编写为非友元函数:

原来的版本显式地访问 t.minutes 和 t.hours,所以它必须是友元。这个版本将 Time 对象 t 作为一个整体使用,让成员函数来处理私有值,因此不必是友元。然而,将该版本作为友元也是一个好主意。最重要的是,它将该作为正式类接口的组成部分。其次,如果以后发现需要函数直接访问私有数据,则只要修改函数定义即可,而不必修改类原型。

提示:

如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。

11.3.2 常用的友元:重载<<运算符

一个很有用的类特性是,可以对<<运算符进行重载,使之能与 cout 一起来显示对象的内容。与前面介绍的示例相比,这种重载要复杂些,因此我们分两步(而不是一步)来完成。

假设 trip 是一个 Time 对象。为显示 Time 的值,前面使用的是 Show( )。然而,如果可以像下面这样操作将更好:

之所以可以这样做,是因为<<是可被重载的 C++运算符之一。实际上,它已经被重载很多次了。最初,<<运算符是 C 和 C++的位运算符,将值中的位左移(参见附录 E)。ostream 类对该运算符进行了重载,将其转换为一个输出工具。前面讲过,cout 是一个 ostream 对象,它是智能的,能够识别所有的 C++基本类型。这是因为对于每种基本类型,ostream 类声明中都包含了相应的重载的 operator<<( ) 定义。也就是说,一个定义使用 int 参数,一个定义使用 double 参数,等等。因此,要使 cout 能够识别 Time 对象,一种方法是将一个新的函数运算符定义添加到 ostream 类声明中。但修改 iostream 文件是个危险的主意,这样做会在标准接口上浪费时间。相反,通过 Time 类声明来让 Time 类知道如何使用 cout。

1.<<的第一种重载版本

要使 Time 类知道使用 cout,必须使用友元函数。这是什么原因呢?因为下面这样的语句使用两个对象,其中第一个是 ostream 类对象(cout):

如果使用一个 Time 成员函数来重载<<,Time 对象将是第一个操作数,就像使用成员函数重载*运算符那样。这意味着必须这样使用<<:

这样会令人迷惑。但通过使用友元函数,可以像下面这样重载运算符:

这样可以使用下面的语句:

按下面这样的格式打印数据:

友元还是非友元?

新的 Time 类声明使 operatro<<( ) 函数成为 Time 类的一个友元函数。但该函数不是 ostream 类的友元(尽管对 ostream 类并无害处)。operator<<( ) 函数接受一个 ostream 参数和一个 Time 参数,因此表面看来它必须同时是这两个类的友元。然而,看看函数代码就会发现,尽管该函数访问了 Time 对象的各个成员,但从始至终都将 ostream 对象作为一个整体使用。因为 operator<<( ) 直接访问 Time 对象的私有成员,所以它必须是 Time 类的友元。但由于它并不直接访问 ostream 对象的私有成员,所以并不一定必须是 ostream 类的友元。这很好,因为这就意味着不必修订 ostream 的定义。

注意,新的 operator<<( ) 定义使用 ostream 引用 os 作为它的第一个参数。通常情况下,os 引用 cout 对象,如表达式 cout << trip 所示。但也可以将这个运算符用于其他 ostream 对象,在这种情况下,os 将引用相应的对象。

不知道其他 ostream 对象?

另一个 ostream 对象是 cerr,它将输出发送到标准输出流——默认为显示器,但在 UNIX、Linux 和 Windows 命令行环境中,可将标准错误流重定向到文件。另外,第 6 章介绍的 ofstream 对象可用于将输出写入到文件中。通过继承(参见第 13 章),ofstream 对象可以使用 ostream 的方法。这样,便可以用 operator<<( ) 定义来将 Time 的数据写入到文件和屏幕上,为此只需传递一个经过适当初始化的 ofstream 对象(而不是 cout 对象)。

调用 cout << trip 应使用 cout 对象本身,而不是它的拷贝,因此该函数按引用(而不是按值)来传递该对象。这样,表达式 cout << trip 将导致 os 成为 cout 的一个别名;而表达式 cerr << trip 将导致 os 成为 cerr 的一个别名。Time 对象可以按值或按引用来传递,因为这两种形式都使函数能够使用对象的值。按引用传递使用的内存和时间都比按值传递少。

2.<<的第二种重载版本

前面介绍的实现存在一个问题。像下面这样的语句可以正常工作:

但这种实现不允许像通常那样将重新定义的<<运算符与 cout 一起使用:

要理解这样做不可行的原因以及必须如何做才能使其可行,首先需要了解关于 cout 操作的一点知识。请看下面的语句:

C++从左至右读取输出语句,意味着它等同于:

正如 iosream 中定义的那样,<<运算符要求左边是一个 ostream 对象。显然,因为 cout 是 ostream 对象,所以表达式 cout << x 满足这种要求。然而,因为表达式 cout << x 位于<< y 的左侧,所以输出语句也要求该表达式是一个 ostream 类型的对象。因此,ostream 类将 operator<<( ) 函数实现为返回一个指向 ostream 对象的引用。具体地说,它返回一个指向调用对象(这里是 cout)的引用。因此,表达式(cout << x) 本身就是 ostream 对象 cout,从而可以位于<<运算符的左侧。

可以对友元函数采用相同的方法。只要修改 operator<<( ) 函数,让它返回 ostream 对象的引用即可:

注意,返回类型是 ostream &。这意味着该函数返回 ostream 对象的引用。因为函数开始执行时,程序传递了一个对象引用给它,这样做的最终结果是,函数的返回值就是传递给它的对象。也就是说,下面的语句:

将被转换为下面的调用:

而该调用返回 cout 对象。因此,下面的语句可以正常工作:

我们将这条语句分成多步,来看看它是如何工作的。首先,下面的代码调用 ostream 中的<<定义,它显示字符串并返回 cout 对象:

因此表达式 cout << “Trip time:”将显示字符串,然后被它的返回值 cout 所替代。原来的语句被简化为下面的形式:

接下来,程序使用<<的 Time 声明显示 trip 值,并再次返回 cout 对象。这将语句简化为:

现在,程序使用 ostream 中用于字符串的<<定义,来显示最后一个字符串,并结束运行。

有趣的是,这个 operator<<( ) 版本还可用于将输出写入到文件中:

其中最后一条语句将被转换为这样:

另外,正如第 8 章指出的,类继承属性让 ostream 引用能够指向 ostream 对象和 ofstream 对象。

提示:一般来说,要重载<<运算符来显示 c_name 的对象,可使用一个友元函数,其定义如下:

程序清单 11.10 列出了修改后的类定义,其中包括 operator*( ) 和 operator<<( ) 这两个友元函数。它将第一个友元函数作为内联函数,因为其代码很短。(当定义同时也是原型时,就像这个例子中那样,要使用 friend 前缀。)

警告:

只有在类声明中的原型中才能使用 friend 关键字。除非函数定义也是原型,否则不能在函数定义中使用该关键字。

程序清单 11.10 mytime3.h

程序清单 11.11 列出了修改后的定义。方法使用了 Time::限定符,而友元函数不使用该限定符。另外,由于在 mytime3.h 中包含了 iostream 并提供了 using 声明 std::ostream,因此在 mytime3.cpp 中包含 mytime3.h 后,便提供了在实现文件中使用 ostream 的支持。

程序清单 11.11 mytime3.cpp

程序清单 11.12 是一个示例程序。从技术上说,在 usetime3.cpp 中不必包含头文件 iostream,因为在 mytime3.h 中已经包含了该文件。然而,作为 Time 类的用户,您并不知道在类代码文件中已经包含了哪些文件,因此您应负责将您编写的代码所需的头文件包含进来。

程序清单 11.12 usetime3.cpp

下面是程序清单 11.10~程序清单 11.12 组成的程序的输出:

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

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

发布评论

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