C++友元函数、友元类、友元成员函数

来源:互联网 发布:vb编写程序代码 编辑:程序博客网 时间:2024/05/18 06:22

众所周知,C++控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但是有时候这种限制太严格,以至于不适合特定的编程问题。在这种情况下,C++提供了另外一种形式的访问权限:友元,友元有3种:
友元函数、友元类、友元成员函数。
通过让函数成为类的友元(即:友元函数),可以赋予该函数与类的成员函数相同的访问权限。

在介绍如何成为友元前,先介绍为何需要友元。在为类重载二元运算符时(带有两个参数的运算符)常常需要友元。将Time对象乘以实数就属于这种情况,下面来看看。
在一般的实例中,重载乘法运算符可能需要两种不同的类型,这里暂定为一种是Time对象,另一种是double类型。这就限制了该运算符的使用方式。记住,左侧的操作数是调用对象,也就是说,下面的语句:
A=B*2.75;
将会被转换为下边的成员函数调用:
A=B.operator*(2.5);
但是下面的语句又将如何呢?
A=2.75*B;
从概念上来说B* 2.75;,2.75*B;相同,但是第二个表达式不对应于成员函数,因为2.75不是Time类型的对象。记住,左侧的操作数应该是调用对象,但是2.75不是对象。因此,编译器不能使用成员函数调用来替换该表达式。
然而非成员函数可以解决这个问题,非成员函数不是由对象调用的,它使用的所有值包括对象都是显示参数,这样,编译器就可以把下面的表达式:
A=2.75*B;
与下面的非成员函数的调用,匹配:
A= operator*(2.75,B);
该函数的原型如下:
Time operator*(double m,const Time& t);
使用非成员函数可以按照所需的顺序获取操作数,但是引发了一个新的问题,非成员函数不能直接访问类的私有变量,至少常规非成员函数不能访问。但是有一类特殊的成员函数可以访问私有数据,他们被称为友元函数。

创建友元函数的第一步是将其原型放在类声明中,并在其原型声明前加上关键字friend:
friend Time operator*(double m,const Time& t);
该原型意味着下面两点:
虽然operator*()函数是在类声明中声明的,但是他不是成员函数,因此不能使用成员运算符来调用;
虽然operator*()函数不是成员函数,但是它与成员函数的访问权限相同。

第二步是编写函数定义。因为它不是成员函数,所以不要使用Time:: 限定符。另外,不要再定义中使用关键字friend,定义应该如下:
Time operator*(double m,const Time & t)
{
Time result;
long totalminutes=t.hours* mult*60 +t.minutes * mult;
reslut.hours=totalminutes/60;
reslut.minutes=totalminutes%60;
return result;
}

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

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

类并非只能拥有友元函数,也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类成为友元是有类定义的,而不能从外部强加友情。因此,尽管友元被授予从外部访问类的私有部分的权限,但是但它们并不与面向对象的思想相悖;相反,它们提高了共有接口的灵活性。
什么时候希望一个类成为另一个类的友元呢?我们来看一个例子,假设我们需要编写一个模拟电视机和遥控器的简单程序。决定定义一个Tv类和一个Romote类,来分别表示电视机和遥控器。很明显,这两个类之间应该存在某种关系,但是是什么关系呢?遥控器并非电视机,反之亦然,因此包含或着私有继承和保护继承的has-a关系也不适合用。事实上,遥控器可以改变电视机的状态,这表明应将Romote类作为Tv类的一个友元。
首选定义Tv类。可以用一组状态成员(描述电视各个方面的变量)来表示电视机。下面是一些可能的状态:
开关;
频道设置;
音量设置;
有线电视或天线调节模式;
Tv调谐或A/V输入。

下面的语句将Romote声明为友元类:
friend class Remote;

友元声明可以位于公有、私有或保护部分。其所在的位置无关紧要。由于Remote类提到了Tv类,所以编译器必须了解了Tv类后,才能处理Remote类,为此,最简单的方法是首先定义Tv类。也可以使用前向声明(forward delaration)这将稍后介绍。

程序清单:tv.h
这里写图片描述

这里写图片描述
在程序清单中,大多数类方法都被定义为内联的。除构造函数外,所有的Romote方法都将一个Tv对象引用作为参数,这表明遥控器要针对特定的电视机。下列的程序清单将列出其余的定义。音量设置函数将音量成员增减一个单位,除非声音达到最大或者最小。频道选择函数使用循环方式,最低的频道设置为1,它位于最高的频道设置maxchannel之后。

程序清单:Tv.cpp
这里写图片描述
这里写图片描述

下属清单是一个简短的程序,可以测试一些特性。另外,可使用同一个遥控器控制两台不同的电视机。

程序清单:use_tv.cpp
这里写图片描述

友元成员函数

从上一个例子的代码可知,大多数Remote方法都是用Tv类的公有接口实现的。这意味着这些方法不是真正需要作为友元。事实上,直接访问Tv成员的Remote方法是Remote:: set_chan(),因此它是唯一需要作为友元的方法。确实可以选择仅让特定的类成员成为另一个类的友元,但是这样做稍微有点麻烦,必须小心的排列各种声明和定义的顺序。下面介绍这其中的原因。
让Remote:: set_chan()成为Tv类的友元的方法是,在Tv类的声明中将其声明为友元:
class Tv
{
friend void Remote:: set_chan(Tv & t, int c);

};
然而,要让编译器能够处理该条语句,它必须知道Remote的定义。否则,他无法知道Remote是一个类,而set_chan是这个类的方法。这意味着应将Remote的定义放到Tv的定义的前面。然而,Remote的方法提到了Tv的定义,这意味这Tv的定义应将位于Remote的定义之前。从而造成了死循环。
要避开这种死循环的方法是,使用前向声明。为此,需要在Remote的定义前面插入下面的语句:
class Tv; //前向声明
这样,排列的顺序应该如下:
class Tv; //前向声明
class Remote{};
class Tv{};
能否像下面这样排列呢?
class Tv; //前向声明
class Tv{};
class Remote{};

答案是不能
原因在于,编译器在Tv类的声明中看到了Remote的一个方法是被声明为Tv类的友元之前,应该先看到remote 类的声明和set_chan()方法的声明。

还有一个麻烦,上述程序清单的Remote声明包含了内联代码,例如
void onoff(Tv & t){ t.onoff (); }
由于这将调用Tv的一个方法,所以这时编译器必须看到了Tv类的声明,这样才能知道Tv有哪些方法,但正如看到的,该声明位于Remote声明的后面。这种问题的解决办法是,使Remote的声明中只包含方法声明,并将实际的定义放在Tv类之后。这样,排列的顺序将如下:

class Tv; //前向声明
class Remote{… }; // Tv- using methods as prototypes only
class Tv{… };
// put Remote method definitions here

Remote 方法的原型与下面的类似:
void onoff(Tv & t);

检查该原型时,所有的编译器都需要知道Tv是一个类,而前向声明提供了这样的信息。当编译器到达真正的方法定义时,他已经读取了Tv类的声明,并拥有了编译这些方法所需要的信息。通过在方法定义中使用inline关键字,仍然可以使其成为内联方法。、

在前面介绍过,内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中。在这个例子中,内联函数的定义应该位于头文件中,因此在使用函数的文件中包含头文件可确保将定义放在正确的位置。也可以将定义放在实现文件中,但是必须删除关键字inline,那么这样函数的链接性将是外部的。

顺便说一句,让整个Remote类成为友元并不需要前向声明,因为友元语句本身已经指出Remote是一个类:
friend class Remote;

原创粉丝点击