C++ 中的友元

友元函数的简单介绍

除了private, public, protected访问权限外, C++ 提供了另外一种形式的访问权限: 友元. 友元有三种:

  • 友元函数
  • 友元类
  • 友元成员函数
    注意, 指定那些类, 函数, 或成员函数为友元 只能从类内部定义, 而不能从外部强加友情. 因此, 尽管友元被授予从外部访问类的私有部分的权限, 但它们并不与面向对象的编程思想相悖, 相反, 它们提高了公有接口的灵活性.

    友元函数

为什么需要友元?

在为类重载二元运算符时(带两个参数的运算符), 常常需要友元. 例如对于某自定义类Time来说, 重载加法和减法运算符时, 两个操作数都是Time类型的, 因此并无太大影响. 但是当重载乘法运算符时, 有可能一个操作数类型为Time, 而另一个类型为double, 这样一来, 就会限制运算符的使用方式. 由于 运算符左侧的操作数是调用对象, 重载后的乘法运算符不再符合交换律, 如下所示:

1
2
A = B * 2.75 // 通过, 相当于, A = B.operator*(2.75)
A = 2.75 * B // 失败, 2.75 没有重载运算符

解决上面问题的一个办法就是使用 非成员函数, 因为非成员函数不是由对象调用的, 因此它使用的所有元素(包括对象)都是显式参数. 当乘法是非成员函数时, 函数调用形式如下所示:

1
2
A = B * 2.75 // 通过, 相当于, A = B.operator*(2.75)
A = 2.75 * B // 通过, 相当于, A = operator*(2.75, B)

对于非成员重载运算符来说, 运算符表达式左边的操作数对应运算符函数的第一个参数, 运算符表达式右边的操作数对应运算符函数的第二个参数. 但是这里又存在一个问题, 那就是非成员函数不能直接访问类的私有数据, 为此, 就引出了友元函数.

创建友元函数

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

    1
    2
    3
    4
    5
    friend Time operator*(double m, const Time & t);
    //可以解决2.75*time的乘法重载的问题,2.75不是Time对象,因此无法调用成员重载函数,需要借助友元非成员函数实现。
    //该声明意味着:
    // 1、虽然该函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
    // 2、虽然该函数不是成员函数,但它与成员函数的访问权限相同。
  • 编写友元函数的定义。因为它不是成员函数,所以不能使用类名::限定符,另外,定义时不要在函数头使用关键字friend。p392

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

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

cout是一个ostream对象,对于每种基本类型ostream类声明中都包含了相应的重载的operator<<()定义。因此,对于不同的基本类型,<<运算符在cout对象中可以表现出不同行为。要让cout能够识别自定义的类型, 不建议修改iostream文件的重载定义, 反向, 我们通过类声明来让类知道如何使用cout.p392

  • <<的第一种重载版本 如果直接通过类声明来重载operator<<()函数,那么在使用时就会像这样,time<<cout;,其中,time是Time类的实例,而cout是Time类重载函数的参数,为了看起来不那么迷惑,利用友元函数,使其第一个参数为ostream对象,这样一来,就可以使用cout<<time的形式(运算符左侧操作数是第一个参数)。p393

    1
    2
    3
    4
    5
    6
    void operator<<(ostream &os, const Time &t){
    os<<t.hours<<t.minutes; //os是cout的引用,别名,省去了拷贝副本的时间
    }

    cout<<time1; //等价于下式
    operator<<(cout,time1);
  • <<的第二种重载版本 上面的重载方法有一些问题,那就是无法使用cout<<time1<<time2<<endl;这样的形式,解决方法如下:p394

    1
    2
    3
    4
    ostream & operator<<(ostream & os, const Time & t){
    os<<t.hours<<t.minutes;
    return os; //返回os的引用,以便实现连续使用<<的操作。
    }

警告: 只有在类声明中的原型中才能使用friend关键字, 除非函数定义也是原型, 否则不能再函数定义中使用该关键字. 另外注意, 在成员函数和友元函数都可以的情况下, 只能二选一, 否则如果都进行定义, 编译器无法确定到底要使用哪种定义, 造成二义性错误.

友元类

类并非只能拥有友元函数, 也可以将类作为友元, 在这种情况下, 友元类的所有方法都可以访问原始类的私有成员和保护成员.

在一个类的内部声明中之前加上friend关键字, 可以使该类称为原始类的友元:

1
2
3
4
class A {
public:
friend class B; // 注意这里是类型 class B, 而不能是 B b 这种声明变量的形式, B 的定义仍然在外部
};

友元声明可以位于公有, 私有或保护部分, 其所在的位置都无关紧要. 最终的效果都是使得类 B 可以访问类 A 的所有成员.

友元类成员函数

如果对友元进行更进一步的限制, 我们可以只将特定的成员函数指定为另一个类的友元, 而不必让整个类成为友元, 但是这样做必须小心排列各种生命和定义的顺序.
举例: 让Remote::set_chan()成为Tv类的友元的方法是, 在Tv类声明中将其声明为友元:

1
2
3
4
class Tv {
friend void Remote::set_chan(Tv& t, int c);
...
};

然而, 要使编译器能够处理这条语句, 它必须知道Remote的定义. 否则, 它无法知道Remote是一个类, 更不能知道set_chan是这个类的方法. 这意味着我们应该将Remote的定义放到Tv的定义前面. 但是Remote的方法中又提到了Tv对象, 而这意味着Tv定义应当位于Remote定义之前. 避开这种循环依赖的方法是, 使用 前向声明(forward declaration), 为此, 需要在Remote定义的前面插入下面的语句:

1
2
3
class Tv; // foward declaration
class Remote {...};
class Tv {...};

不能使用下面的排列顺序:

1
2
3
class Remote;
class Tv {...};
class Remote {...};

原因在于, 在编译器在Tv类的声明中看到Remote的一个方法被声明为Tv类的友元之前, 应该先看到Remote类的声明和set_chan()方法的声明.

其他友元关系

  • 两个类互为友元
  • 一个非成员函数作为两个类共同的友元.