代码改变世界

白话C++系列(29) -- 友元函数

2016-06-28 20:34  Keiven_LY  阅读(648)  评论(0编辑  收藏  举报

C++远征之模板篇

C++中的模板体现在函数上叫做模板函数,体现在类上就叫做模板类。由于模板用途广泛,经过前辈们不断的归纳总结,最终发展成一套使用规范,功能强大,性能优良的标准模板类。

在前面的课程中,我们提到过C++中存在一种朋友关系,这种朋友关系如果体现在函数上,那么我们就称之为友元函数;如果体现在类上,我们就称之为友元类。

友元函数

对于我们定义函数的情形来说,一种情况是将函数定义为全局函数,另一种情况是将函数定义在一个类当中,使其成为类的一个成员函数。如果将全局函数声明为友元,则成为友元全局函数;如果将一个类的成员函数声明为另外一个类的友元函数,那么称该成员函数为友元成员函数。

友元全局函数

我们先来看一个例子

我们定义了一个坐标类(Coordinate),那么,如果我们想要定义一个友元,怎么办呢?我们就要使用关键字:friend。要定义一个友元函数,就只需要将关键字friend加在函数声明的前面,最后加上分号即可,同时一定要传入当前这个类的一个对象或者是一个引用或者是指针,总之,能够通过这个函数能够访问到这个对象的私有的成员或者是受保护的成员(如上面的:friend void printXY(Coordinate &c);)。下面我们来看一看,对于Coordinate这个类来说,他的私有的成员有哪些??一个是m_iX代表的是横坐标,一个是m_iY代表的是纵坐标。下面我们来看一看我们提到的使用方法。

在第一个方框中,我们写出的第一段程序叫做printXY函数,其是打印横纵坐标。在打印横纵坐标的时候吗,我们需要传入一个Coordinate的对象或者是引用(这儿传入的是引用),需要给大家指出的是传入引用或者指针,其传递效率更高,执行速度更快,所以在这提倡传入引用或者指针,而不提倡直接传入对象的方式。printXY函数在访问的时候,我们只是使用cout来打印一下横坐标和纵坐标。请大家注意我们使用的访问方法式使用这个对象去直接访问它的私有成员。如果我们没有将printXY声明为Coordinate的友元,那么如果我们这样写的话,编译器一定会报错。但是当前情况下,我们已经将printXY这个函数声明为Coordinate这个类的友元了,所以我们通过这样的直接访问形式是可以顺利编译通过的。当我们在mian函数当中去调用printXY函数的时候,我们需要先实例化一个Coordinate的对象,然后将这个对象传递进去,请大家注意,因为我们需要的参数是一个引用,所以我们传递的时候直接传入对象名就可以了,而不需要在对象名前面再加一个取地址符号(&)了。关于全局友元函数的定义和使用方法就先说这么多,后续通过代码实践进一步加深映像。

友元成员函数

我们还是通过一个例子来说明问题

在这我们还是以Coordinate这个类为例。定义的时候仍然使用关键字friend。请大家注意后面的写法,我们使用的函数仍然叫做printXY,但是此时的printXY函数并不是一个全局函数,而是一个成员函数,其是Circle这个类中的成员函数。所以,我们要将Circle中的成员函数printXY声明为Coordinate这个类的友元,那么,我们就需要将Circle这个类写出来,然后加上(::)再接printXY,这样就可以将一个类的成员函数声明为另外一个类的友元了。

在main函数当中,我们先实例化了一个Coordinate类的对象coor,然后又实例化了一个Circle类的对象circle。在Circle类中的printXY的实现方法与前面所讲到的全局函数的实现方法一样。通过这样的调用,我们可以发现,如果我们将Circle的printXY声明为Coordinate的友元,那么我们在printXY实现的时候就可以直接访问c这个对象下面的m_iX和m_iY,而m_iX和m_iY都是Coordinate下的私有成员,所以通过这样的行为就能够体现出友元给我们带来的方便。当然,友元给我们带来方便的同时,也给我们带来了一定的风险。当我们将Circle中的printXY这个函数声明为Coordinate这个类的友元函数之后,也就破坏了Coordinate这个类的封装性,此时对于数据的直接访问虽然是方便了,但是如果我们不小心改变了这个数据的值也不易擦觉,所以,风险和方便往往是一对相互矛盾。我们除非有特殊的需要,否则一般情况下不建议大家过度使用友元。

友元函数代码实践

题目描述:

/*   ****************************************************   */

/*  友元函数

        1. 友元全局函数

        2. 友元成员函数

/*   ****************************************************   */

程序框架:

 

先来看第一部分:友元全局函数,所用到的类是Time类

头文件(Time.h

#ifndef TIME_H
#define TIME_H

#include<iostream>
using namespace std;

class Time
{
public:
    Time(int hour, int min, int sec);
private:
    int m_iHour;
    int m_iMinute;
    int m_iSecond;
};

#endif

在Time类中,我们声明了它的构造函数Time并传入三个参数:时,分,秒,还有三个私有数据成员,也分别是时,分,秒。接下来我们来看一看它的构造函数是如何实现的。

源程序(Time.cpp

#include"Time.h"

Time::Time(int hour, int min, int sec)
{
    m_iHour = hour;
    m_iMinute = min;
    m_iSecond = sec;
}

我们看到,它的构造函数的实现非常简单,就是将传入的三个参数分别赋值给它的三个数据成员。

那么,接下来我们在主调程序中定义一个全局函数printTime(),在printTime()函数当中有一个参数Time(这里写成引用形式),如下:

void printTime(Time &t)  //pringtTime函数定义
{
    cout <<t.m_iHour <<":"<<t.m_iMinute <<":"<<t.m_iSecond << endl;
}

当我们写完之后,我们发现如果不声明友元,就相当于通过一个对象去直接访问它的私有数据成员,所以是不能够成功的,不妨我们按一下F7看一下编译是否通过??

我们看到编译过程失败,失败提示Time类中的私有数据成员。如果我们想要访问,及必须要将printTime函数声明为Time类的友元函数,如何声明呢??只需要在Time.h文件中加入一行代码:friend void printTime(Time &c);如下所示:

#ifndef TIME_H
#define TIME_H

#include<iostream>
using namespace std;

class Time
{
    friend void printTime(Time &c); //将全局函数printTime声明为Time类的友元函数
public:
    Time(int hour, int min, int sec);
private:
    int m_iHour;
    int m_iMinute;
    int m_iSecond;
};

#endif

这样之后,我们再来按一下F7看一下编译是否能够通过??

我们看到,当我们将printTime函数声明为Time类的友元之后,编译就能够顺利通过了。

接下来我们去到main函数下去使用一下,品尝一下我们做出来的成果。

首先,我们需要定义一个Time类的对象t,并且我们需要给这个对象t传入三个参数:时、分、秒。然后,将t作为参数传入到printTime函数中,看一下t能否打印出我们传入的时、分、秒来???

main函数

int main()
{
    Time t(6,34,25);
    printTime(t);
    system("pause");
    return 0;
}

我们按一下F5,来看一下运行结果:

通过运行结果,我们可以看到,打印出了我们传入的时分秒(6:34:25)。可见,通过声明友元,就能够使得传入进来的对象去访问它的私有的数据成员和成员函数(这里只展示了访问它的私有数据成员的方法)。

下面再来实现另外一个例子:友元成员函数。友元成员函数要求至少有两个类才可以实现,所以这里又定义了另外一个类Match(比赛)。在Match这个类当中,我们声明了一个printTime函数,这里的printTime函数和之前定义的名字是相同的,只不过现在定义在了类的内部,变成了一个成员函数,所以为了不进行互相干扰,我们把之前的printTime函数注释掉。

头文件(Match.h

#ifndef MATCH_H
#define MATCH_H

class Time; //由于printTime函数中要用到Time类,所以这里需要声明一下
class Match
{

public:
    void printTime(Time &t);
}

#endif

我们再来改造一下Time.h文件,如下:

#include<iostream>
#include"Match.h"
using namespace std;

class Time
{
    friend void Match::printTime(Time &c); //将Match的成员函数printTime声明为Time类的友元函数
public:
    Time(int hour, int min, int sec);
private:
    int m_iHour;
    int m_iMinute;
    int m_iSecond;
};

当我们这样写完之后,在Match的printTime中就可以传入一个对象,并且通过这个对象来调用Time中的私有数据成员:时、分、秒了,如下:

源程序(Match.cpp

#include"Match.h"
#include"Time.h"
#include<iostream>
using namespace std;

void Match::printTime(Time &t)
{
    cout <<t.m_iHour <<":"<<t.m_iMinute <<":"<<t.m_iSecond << endl;
}

然后,我们调到demo.cpp中,首先我们需要定义一个Match的对象m,然后通过对象m来调用printTime函数,如下:

#include<iostream>
#include"stdlib.h"
#include"Time.h"
#include"Match.h"

using namespace std;

int main()
{
    Time t(6,34,25);
    Match m;
    m.printTime(t);
    system("pause");
    return 0;
}

然后,我们按一下F7,看一下能否编译通过,如下:

我们看到,编译顺利通过,然后我们再按F5,看一看运行结果:

我们可以看到,打印出的是6点34分25秒(6:34:25),结果也如我们所料。此外,作为printTime这样的友元声明来说,它与访问限定符public、private、protected并不形成交叉关系,也就是说,它们并不构成约束,所以友元函数声明既可以写在访问限定符外面,也可以写在访问限定符里面。通过尝试也验证了这一点。可见,作为友元函数的声明的位置没有约束,可是我们仍然建议大家将其写在类的最前面,也就是说访问限定符的外面,这是因为,作为一个类来说,它对外如何暴露是非常重要的,我们把重要的放在前面有助于编程过程中减小犯错的概率。