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

开发 后端
虚函数和友元是C++中两个很重要的概念,友元和虚函数经常会在同一程序中出现。下面是几点值得注意的知识。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。虚函数必须是基类的非静态成员函数,其访问权限可以是protected或public。

几点基本知识:

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

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

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

来看下面的几段代码:

Code:

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

 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. }  
  6.  

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

Code:

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

 任何一个合格的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. };  
  9.  

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

友元的作用是提高了程序的运行效率,即减少了类型检查和安全性检查等都需要时间开销,但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。而虚函数的作用则是为了实现多态。虚函数和友元总是会同时出现在一段程序中,希望通过本文,能解决你的困惑。

【编辑推荐】

  1. C++友元函数基本应用方式解析
  2. 13.1.7 友元
  3. 9.6.1 虚函数的概念
  4. 实现C++虚函数时相关注意事宜
责任编辑:于铁 来源: 天极网
相关推荐

2024-01-29 16:47:44

函数封装开发

2011-07-15 13:49:30

C++友元函数友元类

2015-07-10 15:31:42

ITIoT物联网

2010-02-02 17:33:17

C++友元函数

2015-09-18 15:22:56

DCIMITSM

2011-03-01 10:58:00

2014-04-14 10:21:15

开发运维DevOps

2010-09-01 15:42:39

DHCP SnoopiARP

2009-06-17 13:26:06

scala继承模型

2022-05-17 14:34:23

元宇宙数字经济商业

2018-02-27 13:59:52

春节互联网直播

2010-04-29 14:33:01

Unix系统

2013-07-26 11:17:34

AdTime游戏大数据

2009-06-19 18:00:05

HibernateSpring

2017-01-13 10:33:39

华为大数据

2010-01-18 17:38:54

C++虚函数表

2024-06-11 12:38:12

2015-12-31 15:19:14

雾霾高性能英特尔

2019-06-24 16:30:33

区块链零知识证明比特币

2011-10-11 15:42:54

大数据数据库
点赞
收藏

51CTO技术栈公众号