woaidongmao

文章均收录自他人博客,但不喜标题前加-[转贴],因其丑陋,见谅!~
随笔 - 1469, 文章 - 0, 评论 - 661, 引用 - 0
数据加载中……

当“友元”遇到“虚函数”

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

几点基本知识:

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

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

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

来看下面的几段代码:

Code:

1.      class A; 

2.       

3.      class

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

15. 

16.  private

17.      virtual void output() 

18.      { 

19.          cout << "D::output" << endl; 

20.      }     

21.  }; 

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

Code:

1.      class

2.     

3.      public:     

4.          void test() 

5.          { 

6.              D d; 

7.              d.output(); //编译出错 

8.          } 

9.      }; 

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

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

Code:

1.      class

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,而AB的友元,所以允许它调用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

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

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,那不用就是; 有人坚决认为只要有privatepublic就足够了,那就把protected忘记吧,甚至有人认为virtual也是多余的——很多人就是把C++当成另一种C使用,那都可以接受。friend也一样,如果你不用,它的存在并不会给你带来什么性能损失,你要做的就是用很OO或很不OO的,但是你熟悉的方法去满足友元的需求而已。

九、一定要这个第9点。除了友元类,更常见的其实是友元函数。很多操作符的重载,都需要全局的友元函数来减轻相关类的public出太多成员。这一下扯到操作符重载有用吗?哇,这是另外一个经典的问题了,它曾经引起的纠纷,比这个友元所带来的,要热闹上几倍呢。就此打住。

十、最后一点,C++初学者如何学习这门语言众多的,又容易产生正交效应的特性呢?我有个建议:先有个基本了解,做点练习,但并不需要急着真正使用。

 

posted on 2009-12-02 10:29 肥仔 阅读(919) 评论(2)  编辑 收藏 引用 所属分类: C++ 基础

评论

# re: 当&ldquo;友元&rdquo;遇到&ldquo;虚函数&rdquo;  回复  更多评论   

好文~顶一个
2009-12-02 13:26 | iceshark

# re: 当&ldquo;友元&rdquo;遇到&ldquo;虚函数&rdquo;  回复  更多评论   

好文,学习了
2009-12-02 16:01 | zhaoyg

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理