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

当函数被声明为类的友元函数,可以赋予该函数与类的成员函数相同的访问权限。

为什么需要友元函数

在为类重载二元运算符时常常需要用到友元函数。
例如,为一个时间类 Time 重载乘法运算符,使其可以乘以一个实数,部分代码如下所示:

class Time {private:int hours;int minutes;
public:...;Time operator*(double n) const;void show() const;
};
void Time::show() const {std::cout << this->hours << " hours, " << this->minutes << " minutes. Address is " << this << std::endl;
}
Time Time::operator*(double n) const {Time res; long totalMinutes = (long) (n * (hours * 60 + minutes));res.hours = totalMinutes / 60;res.minutes = totalMinutes % 60;
}

这个重载的乘法运算符使用的是两个不同的类型,即乘法运算符将一个 Time 值与一个 double 值结合在一起。这限制了乘法运算符的使用 —— Time 对象必须在乘法运算符的左侧,double 值必须在乘法运算符右侧。

A = B * 2.5; // 正确
A = 2.5 * B; // 错误

2.5 * B 并不对应 operator*() 成员函数,因为 2.5 不是 Time 对象。

解决这个问题的一种方式是,告知每个人,只能按照 B * 2.5 这种格式编写,这是一种对服务器友好-客户警惕的解决方案。
另一种方式则是采用非成员函数来重载乘法运算符。非成员函数不是由对象调用,它使用的所有值都是显式参数。

使用非成员函数重载乘法运算符可以按照所需的顺序来获得操作数(先 double 值,后 Time 对象),但是非成员函数无法直接访问类的私有数据。可以通过提供一系列的公有接口让非成员函数间接执行一些操作,但更常用的是利用一类特殊的非成员函数 —— 友元函数,友元函数可以直接访问类的私有成员。

创建友元函数

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

class Time {private:int hours;int minutes;
public:...;Time operator*(double n) const;friend Time operator*(double, const Time &);void show() const;
};

这个原型意味着两点:

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

创建友元函数的第二步就是编写函数定义。因为友元函数不是类的成员函数,因此在编写函数定义的时候,不必在函数名之前使用 Time:: 限定符。另外,不要在函数定义处使用 friend 关键字。

Time operator*(double n, const Time & t) {Time res;long totalMinutes = (long) (n * (t.hours * 60 + t.minutes));res.hours = totalMinutes / 60;res.minutes = totalMinutes % 60;return res;
}

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

A = 2.5 * B;

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

A = operator*(2.5, B);

乍一看,友元函数违反了 OOP 数据隐藏的原则,因为友元机制允许非成员函数访问私有数据,但这个观点太片面了,应该将友元函数看做类扩展接口的组成部分。只有在类声明中才能够决定那一个函数是本类的友元函数,因此类声明仍然控制了那些函数可以访问私有成员。总之,类方法和友元只是表达类接口的两种不同机制。

扩展:普通的非成员函数的运算符重载演示

上面的例子只是为了演示友元函数,实际上,在定义了 Time 类的成员函数 operator*(double) 的基础下,可以通过普通的非成员函数来重载乘法运算符。

Time operator*(double n, const Time & t) {return t * n;
}

之前的函数定义中显式地访问了 Time 的私有成员,所以该运算符重载必须声明为友元。但这个版本的则是通过调用 Time 类的成员函数 operator*(double) 来实现的,并不需要显式地访问 Time 的私有成员,因此不必声明为友元。不过,将该版本声明为友元也是一个好主意,这样它将成为正式接口的组成部分。

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

常用:重载 << 运算符

一个很有用的类的特性是,可以对 << 运算符重载,使之能与 cout 一起来显示对象的内容。与前面介绍的示例相比,这种重载要更复杂些。
假设 time 是一个 Time 对象,为了显示 Time 的内容,前面使用的是 show() 函数,现在希望可通过下面这样的操作来显示 Time 的内容。

std::cout << time << std::endl;

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

重载<<运算符(Version 1)

要使 Time 类知道使用 cout,必须使用友元函数。这是因为cout << time语句,使用两个对象,并且第一个对象是 ostream 类对象(cout)。如果是声明为成员函数,则第一个对象必须是 Time 对象;如果是声明为普通的非成员函数,可以使得第一个对象为 ostream 类对象,但是不能直接访问 Time 的私有成员。

class Time {private:int hours;int minutes;
public:// ...friend void operator<<(std::ostream & out, const Time &);
};
void operator<<(std::ostream & out, const Time & t) {std::cout << t.hours << " hours, " << t.minutes << " minutes. Address is " << this << std::endl;
}

这样就可以使用下面的语句打印数据了:

std::cout << time;

operator<<() 虽然是友元函数,但是它只在 Time 类声明中被声明,并没有在 ostream 类中声明,因此,它只是 Time 类的友元函数,并不是 ostream 类的友元函数。这也是因为在 operator<<() 的函数定义中直接访问了 Time 类对象的私有数据,而对于 ostream 对象只是当做一个整体来使用,并没有访问私有成员。

重载<<运算符(Version 2)

在 Version 1 中存在一个问题, 虽然cout << time;可以正常使用,但这种实现不允许像通常那样将重新定义的 << 运算符和 cout 一起使用:

cout << "Time is " << time << endl; // 报错

上面的语句,实际上等同于:

((cout << "Time is ") << time) << endl;

由于 Version 1 中定义的 operator<<() 返回值是 void,因此执行到 ((cout << "Time is ") << time) 时将返回 void,导致最后的 << 运算符左侧是 void,因此报错。

class Time {private:int hours;int minutes;
public:// ...friend std::ostream & operator<<(std::ostream & out, const Time &);
};
std::ostream & operator<<(std::ostream & out, const Time & t) {std::cout << t.hours << " hours, " << t.minutes << " minutes. Address is " << &t << std::endl;return out;
}

总结

  1. 友元函数的函数原型位于在类声明中,但并不是类的成员函数,在声明函数原型时,需要使用 friend 关键字。
  2. 友元函数的函数定义不需要 friend 关键字和 :: 运算符,和普通非成员函数是一样的。
  3. 友元函数可以直接访问类的私有成员。
  4. 通常在运算符重载中使用友元函数的情况是:重载的运算符的第一个参数不是该类的对象。

C++ —— 友元函数相关推荐

  1. C++ 笔记(30)— 友元函数与友元类

    我们知道类的私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行.这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦. ...

  2. C++ primer 第七章之 友元函数与友元类

    1.为什么需要友元函数?  类具有封装和信息隐藏的特性: 只有类的成员函数才能访问类的私有成员,程序中的其他函数是无法访问私有成员的 ; 非成员函数可以访问类中的公有成员,但是如果将数据成员都定义为公 ...

  3. 友元函数、类的非静态成员函数、静态成员函数的区别

    类中申明的函数相对于类来说有三层意思: 1.有this指针 2.函数在类的作用区域中 3.可以访问类中私有部分 4.可以被继承 非静态成员函数具有1234 静态成员函数具有234 友元函数具有3 静态 ...

  4. C++——运算符的重载---以成员函数方式重载---以友元函数方式重载

    一.运算符的重载 1.运算符的重载 允许把标准运算符(如+ - * /等运算符)应用于自定义数据类型的对象,可以提高程序的可读性,运算符的重载本质上还是函数重载.运算符仅仅是语法上的方便,它是另一种函 ...

  5. C++运算符重载形式--成员函数or友元函数?

    1.C++操作符重载形式-成员函数or友元函数 1.对运算符重载,需要坚持四项基本原则: 不可臆造运算符: 运算符原有操作数的个数.优先级和结合性不能改变: 操作数中至少一个是自定义类型: 保持重载运 ...

  6. C++中的友元函数friend

    1.C++中的友元函数 类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员.尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数.友 ...

  7. C++中友元函数和友元类

    友元函数 友元函数是可以直接访问类的私有成员的非成员函数.它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下: frien ...

  8. 运算符中的二元重载,为什么要调用友元函数而不是全局函数的问题

    #include <iostream>using namespace std; //实现运算符的重载 class A { public:A(int real=0,int imaginary ...

  9. C++友元函数和友元类(C++ friend)详解

    在看VISP视觉库的时候遇到友元函数: Friends void swap (vpDetectorAprilTag &o1, vpDetectorAprilTag &o2) 在定义一个 ...

  10. 第七周项目一-成员函数、友元函数和一般函数有区别(1)

     /**Copyright(c)2016,烟台大学计算机与控制工程学院*All rights reserved*文件名称:123.cpp*作 者:王蕊*完成日期:2016年4月12日*版 本 号: ...

最新文章

  1. 阿里达摩院再造AI抗疫技术:20秒判读CT影像,识别准确率达96%,河南率先启用...
  2. JDBC之应用程序分层
  3. jar包打补丁 jar -uf_maven项目引入本地jar包的方法
  4. java 年历_逆转监督漫画
  5. Mycat安全_SQL拦截黑名单---MyCat分布式数据库集群架构工作笔记0034
  6. 小白重装系统教程_小白重装系统使用教程
  7. java怎么编程class,JAVA Class种
  8. 【postgresql 数据库运维文档】
  9. 57之最,来找找你的之最吧
  10. position的8种定位方式
  11. 《软件测试》第十二章 文档测试
  12. socket 读、写字节流数据
  13. 关于ubuntu上,usb设备编号
  14. 【CSS】css文字超出显示省略号/文字超过三行显示省略号..
  15. ele-ui 里面的分页操作
  16. 为什么不建议用Executors?
  17. Atcoder 4244 AtCoder Express 2 暴力
  18. 1个球从100m落下,每次时,反跳原高度的一半,再落,再反弹,求第10次落地共经过多少m,第10次反弹多高。 谭浩强《c语言程序设计》第五章第十一题
  19. 全球及中国中药行业发展机遇及投资价值评估报告2021-2027年
  20. 2014cad运行要计算机内,win7系统2014cad出现致命错误的解决方法

热门文章

  1. hmailserver php mail,HMAILSERVER集成WEB邮件系统(ROUNDCUBE WEBMAIL)
  2. 华为防火墙的OSPF配置实验
  3. struts.properties在什么地方,struts.properties在哪里,怎么修改struts.properties
  4. ORIN ubuntu20.04解析编译视觉遇到的问题解决方法
  5. 从零开始的Linux学习日志
  6. Qt+腾讯IM开发笔记(一):腾讯IM介绍、使用和Qt集成腾讯IM-SDK的工程模板Demo
  7. word删除目录、封面页码
  8. 思维方式-《游戏改变世界》书中的精髓:如何把游戏融入现实生活,从而让我们的生活变得更美好。
  9. 华为手机怎么录屏?分享2个好用的手机录屏方法!
  10. PSE认证该怎么做?