[14] 友元
FAQs in section [14]:
- [14.1] 什么是友元(
friend
)? - [14.2] 友元破坏了封装吗?
- [14.3] 使用友元函数的优缺点是什么?
- [14.4] “友元关系既不继承,也不传递”是什么意思?
- [14.5] 类应该使用成员函数还是友元函数?
14.1 什么是友元(friend
)?
允许另一个类或函数访问你的类的东西。
友元可以是函数或者是其他的类。类授予它的友元特别的访问权。通常同一个开发者会出于技术和非技术的原因,控制类的友元和成员函数(否则当你想更新你的类时,还要征得其它部分的拥有者的同意)。
14.2 友元破坏了封装吗?
如果被适当的使用,实际上可以增强封装。
当一个类的两部分会有不同数量的实例或者不同的生命周期时,你经常需要将一个类分割成两部分。在这些情况下,两部分通常需要直接存取彼此的数据(这两部分原来在同一个类中,所以你不必增加直接存取一个数据结构的代码;你只要将代码改为两个类就行了)。实现这种情况的最安全途径就是使这两部分成为彼此的友元。
如果你象刚才所描述的那样使用友元,就可以使私有的(private
)保持私有。不理解这些的人在以上这种情形下还天真的想避免使用友元,他们要么使用公有的(public
)数据(罕见!),要么通过公有的 get()
和set()
成员函数使两部分可以访问数据。而他们实际上破坏了封装。只有当在类外(从用户的角度)看待私有数据仍“有意义”时,为私有数据设置公有的get()
和set()
成员函数才是合理的。在许多情况下,这些 get()
/set()
成员函数和公有数据一样差劲:它们仅仅隐藏了私有数据的名称,而没有隐藏私有数据本身。
同样,如果你将友元函数当做一种类的public:
存取函数的语法不同的变种来使用的话,友元函数就和破坏封装的成员函数一样会破坏封装。换一种说法,类的友元不会破坏封装的壁垒:和类的成员函数一样,它们就是封装的壁垒。
14.3 使用友元函数的优缺点是什么?
友元函数在接口设计选择上提供了一定程度的自由。
成员函数和友元函数具有同等的特权(100% 的)。主要的不同在于友元函数象f(x)
这样调用,而成员函数象 x.f()
这样调用。因此,可以在成员函数(x.f()
)和友元函数(f(x)
)之间选择的能力允许设计者选择他所认为更具可读性的语法来降低维护成本。
友元函数主要缺点是需要额外的代码来支持动态绑定时。要得到虚友元(virtual
friend
)的效果,友元函数应该调用一个隐藏的(通常是 protected:
)虚。例如:
class Base {
public:
friend void f(Base& b);
// ...
protected:
virtual void do_f();
// ...
};
inline void f(Base& b)
{
b.do_f();
}
class Derived : public Base {
public:
// ...
protected:
virtual void do_f(); // "覆盖" f(Base& b)的行为
// ...
};
void userCode(Base& b)
{
f(b);
}
在userCode(Base&)
中的f(b)
语句将调用虚拟的 b.do_f()
。这意味着如果b
实际是一个派生类的对象,那么Derived::do_f()
将获得控制权。注意派生类覆盖的是保护的虚(protected:
virtual
)成员函数 do_f()
; 而不是它友元函数f(Base&)
。
14.4 “友元关系既不继承,也不传递”是什么意思?
仅仅因为我承认对你的友情,允许你访问我,并不自动地允许你的孩子访问我,并不自动地允许你的朋友访问我,并不自动地允许我访问你。
- 我不见得信任我朋友的孩子。友元的特权不被继承。友元的派生类不一定是友元。如果
Fred
类声明Base
类是友元,那么Base
类的派生类不会自动地被赋予对于Fred
的对象的访问特权。 - 我不见得信任我朋友的朋友。友元的特权不被传递。友元的友元不一定是友元。如果
Fred
类声明Wilma
类是友元,并且Wilma
类声明Betty
类是友元,那么Betty
类不会自动地被赋予对于Fred
的对象的访问特权。 - 你不见得仅仅因为我声称你是我的朋友就信任我。友元的特权不是自反的。如果
Fred
类声明Wilma
类是友元,则Wilma
对象拥有访问Fred
对象的特权,但Fred
对象不会自动地拥有对Wilma
对象的访问特权。
14.5 类应该使用成员函数还是友元函数?
尽量使用成员函数,不得已时使用友元。
有时在语法上,友元更好(例如,Fred
类中,友元函数允许Fred
参数作为第二个参数,而成员函数必须是第一个)。另一个好的用法是二元中缀运算符。例如,如果你想允许aFloat + aComplex
的话,aComplex + aComplex
应该被定义为友元而不是成员函数。(成员函数不允许提升左边的参数,因为那样会改变成员函数调用对象的类)。
在其他情况下,首选成员函数。