当“友元”遇到“虚函数” - 笔记 - 南郁 - CSDN学生大本营 - Powered ...

来源:百度文库 编辑:神马文学网 时间:2024/04/19 05:09:48

前些天d2school QQ群里有网友在讨论这一内容,我试图做个整理。

几点基本知识:

1、如果类A是类B的友元,则类A(的成员函数)可以直接访问类B的私有成员。

2、友元不能继承。也就是说,类A是类B的友元,类D是类B的派生类,则类A并不会直接是类D的友元。通俗一点,父亲的朋友,并不天生就是儿子的朋友。

3、虚函数的基本知识就不说了。

来看下面的几段代码:

Code:
  1. class A;  
  2.   
  3. class B  
  4. {  
  5. private:  
  6.     virtual void output()  
  7.     {  
  8.         cout << "B::output" << endl;  
  9.     }  
  10.       
  11.     friend class A;       
  12. };  
  13.   
  14. class D : public B  
  15. {  
  16. private:  
  17.     virtual void output()  
  18.     {  
  19.         cout << "D::output" << endl;  
  20.     }      
  21. };  

A 是 B 的友元类, 而D是B的派生类。 所以,若想在A中直接访问D的代码,则编译不过:

Code:
  1. class A  
  2. {  
  3. public:      
  4.     void test()  
  5.     {  
  6.         D d;  
  7.         d.output(); //编译出错  
  8.     }  
  9. };  

这一点大家都没觉得有问题,毕竟书上写得都明白直观:父类的友元,并不会因为继承,而成为派生类的友元。

但若代码改成这样,编译器似乎就被欺骗了:

Code:
  1. class A  
  2. {  
  3. public:      
  4.     void test()  
  5.     {  
  6.         D d;  
  7.         B* pb = &d;     
  8.         pb->output(); //编译通过  
  9.     }  
  10. };  

没错,很多人会认为这种代码,就算能通过编译器,也很可能是一种不好的代码,因为它怎么看都像是在欺骗编译器。是这样吗?先不讨论。先问一个问题: 上面的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:
  1. A::test_2(B* pb)  
  2. {  
  3.     pb->output(); //很难在编译期反查出pb的实际类型。  
  4.                  //因为调用test_2()的代码,可能无处不在,甚至可能在未来的代码  
  5. }  

四、要理解有时候,人们其实就是在故意做这种事,最典型的做法,就是通过非虚函数调用虚函数:  

Code:
  1. class B  
  2. {  
  3. public:  
  4.     void Action()  
  5.     {  
  6.              this->DoAction();  
  7.     }  
  8. private:  
  9.       virtual void DoAction() = 0; //一个私有的纯虚函数  
  10.   
  11.     //  friend class A; 
  12. };  

 任何一个合格的C++程序员,都应该学会这种作法。DoAction是一个纯虚函数,这里我们更决绝一点,干脆让它是私有的,这就是逼着派生类自己去实现一个完全自我的DoAction(),假设有个class D : public B,并且听话地实现了DoAction。具体D的定义,为节省点篇幅,不写了。

 注意到第11行的注释, class A 现在已经不是 B 的友员了,但不要紧,我们只是想在新版的类A中,调用Action函数,而它是public的,所以这里不需要友元来搅和。

Code:
  1. class A  
  2. {  
  3.       void test()  
  4.       {  
  5.                B* pb = new D; //pb 实际指向一个D对象。  
  6.                pb->Action(); // Action 是 公开的,所以可以调用  
  7.       }  
  8. };  

 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++初学者如何学习这门语言众多的,又容易产生正交效应的特性呢?我有个建议:先有个基本了解,做点练习,但并不需要急着真正使用。

----------------------------------------------------------------

对于这篇小文提到的知识点,你理解得怎样?想知道别人对友元与虚函数的关系理解程度吗?

当“友元”遇到“虚函数” - 笔记 - 南郁 - CSDN学生大本营 - Powered ... 学习C语言的必备基础知识 - 笔记 - 徐名峰 - CSDN学生大本营 - Powered... 如何有效地记忆与学习 - 笔记 - 编程之美 - CSDN学生大本营 - Powered ... 关于程序控制逻辑的讨论 - 笔记 - 肖舸 - CSDN学生大本营 - Powered b... 设计,是一种态度 - 笔记 - 斌斌 - CSDN学生大本营 - Powered by U... 职场随笔-走好那段路! - 笔记 - 李天平 - CSDN学生大本营 - Powered by UCenter Home 学习编程需要什么英语基础? - 笔记 - 南郁 - CSDN学生大本营 6.3.4 使用MFC::CArchive - 笔记 - 白乔 - CSDN学生大本营 -... [转]VC _T的用途 - 笔记 - summer - CSDN学生大本营 - Power... jiushi世情熟,则人情易流; 世情疏,则交情易阻。 - 笔记 - 潘勇 - CSDN学生大本营 我还会再回来的——计算机达人成长之路(19) - 笔记 - 朱云翔 - CSDN学生大本营... 就要离开人人网的工作了,总结来北京的这两年! - 笔记 - 迟宏泽 - CSDN学生大本营... Spring事务管理 - Java - 课堂 - 话题 - 迟宏泽 - CSDN学生大本营... 当你遇到愤怒的学生 友元函数和友元类 c++中的友元函数 友元函数和友元类 c++中的友元函数(new) “一吨热水66元”拿学生当摇钱树? 当佛教遇到伊斯兰教|西学-宗教 - 启蒙历史网 - Powered by PHPWind.... 当佛遇到魔 当和尚遇到钻石 当会计准则遇到公司法 当苏格拉底遇到孔子