友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。虚函数必须是基类的非静态成员函数,其访问权限可以是protected或public。
几点基本知识:
1、如果类A是类B的友元,则类A(的成员函数)可以直接访问类B的私有成员。
2、友元不能继承。也就是说,类A是类B的友元,类D是类B的派生类,则类A并不会直接是类D的友元。通俗一点,父亲的朋友,并不天生就是儿子的朋友。
3、虚函数的基本知识就不说了。
来看下面的几段代码:
Code:
- class A;
- class B
- {
- private:
- virtual void output()
- {
- cout << "B::output" << endl;
- }
- friend class A;
- };
- class D : public B
- {
- private:
- virtual void output()
- {
- cout << "D::output" << endl;
- }
- };
A 是 B 的友元类,而D是B的派生类。 所以,若想在A中直接访问D的代码,则编译不过:
Code:
- class A
- {
- public:
- void test()
- {
- D d;
- d.output(); //编译出错
- }
- };
这一点大家都没觉得有问题,毕竟书上写得都明白直观:父类的友元,并不会因为继承,而成为派生类的友元。
但若代码改成这样,编译器似乎就被欺骗了:
Code:
- class A
- {
- public:
- void test()
- {
- D d;
- B* pb = &d;
- pb->output(); //编译通过
- }
- };
没错,很多人会认为这种代码,就算能通过编译器,也很可能是一种不好的代码,因为它怎么看都像是在欺骗编译器。是这样吗?先不讨论。先问一个问题: 上面的08行代码,output调用的是B类的那个output,还是D类的那个呢?
回答正确并不难——既然会认定这段代码带有“欺骗”性质,而且又注意到output是一个“虚函数”的话——就能能正确地解答: 调用的是D类的。A明明只是B的友元,但却通过一个简单的类型转换,就访问了D类的那个私有函数,所以会觉得这是一种“欺骗”。
如果这是一种欺骗,那我们先来回答这个骗局为什么能成立:因为“友元”的判断(resolve),在编译期决定;而虚函数在运行期去resolve。在编译08行代码时,编译器看到*pb的类型是B,而A是B的友元,所以允许它调用output(它认为是B::output);而在运行时,由于output是虚函数,所以最终被决定到D::output头上。
没时间细查手头的《The Design and Evolution fo C++》,但不管这样的设计是有意为之,还是无奈之举,或者仅仅是C++众多的特性“正交”现象之一,我个人觉得这个特性其实正是我们想要的。
一、首先要理解为什么派生类不应该继承基类的"友元",这一点很多C++的书讲到了
二、其次要理解它的语法机制:前面讲的,一个编译期属性与一个运行期属性相遇了……
三、要理解如果想关掉这一类欺骗,其实做不到。且来看看,该法之一是在编译期,也检查实际调用对象的类型,前面的示例代码不难做到这一点,但下面的代码中,pb来自一个形参:
Code:
- A::test_2(B* pb)
- {
- pb->output(); //很难在编译期反查出pb的实际类型。
- //因为调用test_2()的代码,可能无处不在,甚至可能在未来的代码
- }
四、要理解有时候,人们其实就是在故意做这种事,最典型的做法,就是通过非虚函数调用虚函数:
Code:
- class B
- {
- public:
- void Action()
- {
- this->DoAction();
- }
- private:
- virtual void DoAction() = 0; //一个私有的纯虚函数
- // friend class A;
- };
任何一个合格的C++程序员,都应该学会这种作法。DoAction是一个纯虚函数,这里我们更决绝一点,干脆让它是私有的,这就是逼着派生类自己去实现一个完全自我的DoAction(),假设有个class D : public B,并且听话地实现了DoAction。具体D的定义,为节省点篇幅,不写了。
注意到第11行的注释, class A 现在已经不是 B 的友员了,但不要紧,我们只是想在新版的类A中,调用Action函数,而它是public的,所以这里不需要友元来搅和。
Code:
- class A
- {
- void test()
- {
- B* pb = new D; //pb 实际指向一个D对象。
- pb->Action(); // Action 是 公开的,所以可以调用
- }
- };
pb 调用了非虚的B::Action函数,但在Action内调用了虚函数DoAction,再由于pb实际指向的是D对象,所以最终调用的是D::DoAction()——这了无新意对不对?只要学过一点C++的多态,都会懂这一点。没错,它太司空见惯了,基本上所有C++程序员每天都会在写类似的代码——这就是我想说的,有时候,看起来在调用基类的代码,但实际上在调用派生类的代码。假设我们修改了语法规则,逼着虚函数在遇上友元之后失效,那就是逼着程序员不去用friend,去将更多本来应该是private的成员,用各种该法写成public的。
五、接着,是一个看起来很简单,但却被很多人误解的概念:友元是破坏了封装了吗?错,友元其实是促进了更好的封装。它基于这样的需求:有一个类,它有那么几个成员(数据或函数),它只能对个别的其它类公开,这时,你可以考虑使用友元这项技术。如果不用,会有很多人就把那些成员直接修改为public,结果:原本应只对个别类开放的属性,变成对所有类开放了。俗气一点,法律规定老婆可以在私有场合下看老公的屁屁,如果法律强制规定不允许有这种例外,那很可能会有一些哥们,直接把屁股public出来就上街了——你若问他为什么,他也很无辜:我不过是想让我老婆方便一些。
六、读了第五点,对OO有一套的C++程序员要“笔试/鄙视”我了,好,好,我知道既使不用friend的属性,也可以美满地实现前述的,类似老婆看老公屁股的问题——我是说,通过OO技术,避开需要只对部分类开放权限的需求,而转化为第三方(比如某个接口及它的实现类)中——不管如何,你必须承认友元没有破坏封装性,因为其它解决这一问题的的,似乎更纯粹的OO技术,它们美好的地方是在于更细的类颗粒,以及更好的类组织(不美好地方是,效率差了点,以及对OO思想非得有点水准,否则会绕晕掉,为了不让Cer笑话,我们不提太多)。
七、不过,纵算如此(第六点),我还是是狡辩一句:就算是在那些看起来很纯的OO语言里,其实也有友元的影子啊。比如Java,是没有friend关键字,可以它的内部类(非静态的内部类),可以直接访问外部类,难道不是友元吗?——事实上,这也正是在C++使用friend最主旋律的用法(我甚至不用写“之一”)。再如Object Pascal(Delphi),是没有friend关键字,可是只要是位于同一个代码单元(就是同一个.pas文件),则其中所有类天生就是可以互相访问啊(当然,需首先满足可见性)——这是当年我用Delphi时觉得最爽的地方之一了,既然号称更OO的语言都留了一手,为何C++不能呢? :)
八、第七点明显带有情绪化,这不符合C++之父对我们的期望: “ 在C++设计中有一条指导原则,那就是,无论做什么事情,都必须相信程序员。与可能出现什么样的错误相比,更重要得多的是能做出什么好事情。C++程序员总被看作是成年人……” 。在C++的大千世界,差异性永远被尊重, 有人不爱用template,那不用就是; 有人坚决认为只要有private和public就足够了,那就把protected忘记吧,甚至有人认为virtual也是多余的——很多人就是把C++当成另一种C使用,那都可以接受。friend也一样,如果你不用,它的存在并不会给你带来什么性能损失,你要做的就是用很OO或很不OO的,但是你熟悉的方法去满足友元的需求而已。
九、一定要这个第9点。 除了友元类,更常见的其实是友元函数。很多操作符的重载,都需要全局的友元函数来减轻相关类的public出太多成员。这一下扯到“操作符重载”有用吗?哇,这是另外一个经典的问题了,它曾经引起的纠纷,比这个“友元”所带来的,要热闹上几倍呢。就此打住。
十、***一点,C++初学者如何学习这门语言众多的,又容易产生正交效应的特性呢?我有个建议:先有个基本了解,做点练习,但并不需要急着真正使用。
友元的作用是提高了程序的运行效率,即减少了类型检查和安全性检查等都需要时间开销,但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。而虚函数的作用则是为了实现多态。虚函数和友元总是会同时出现在一段程序中,希望通过本文,能解决你的困惑。
【编辑推荐】