C++中面向对象部分,有一个特性,一直被支持面向对象理念的程序员诟病,这个特性就是“友元”(friendship)。那么,“友元”到底是破坏了对象的封装性,还是保护了对象的封装性?
事情的起因源自于我小侄子这段时间正在学习C++。
昨晚在微信上给我留言,说之前因为我对C++的推崇,一直觉得C++是门伟大而又充满智慧的语言,结果现在感觉大失所望。
他说:一门真正的面向对象的语言,不应该存在多重继承,这样会让事情变得非常复杂,为什么C++会有多重继承这样糟糕的语法。
他说:作为一门面向对象的编程语言,连“接口”的功能都没有,这是最基础的面向对象特征,但是C++好像根本没有“接口类”这一说。
他还说:最重要的是C++还搞了个 “友元”的概念,不明摆着破坏了面向对象中“封装”这一最重要、最基础的特性吗?
所以,他“也”和很多人一样感觉“C++并不是一门纯粹的面向对象的编程语言,有很多不合理的地方。”
这种论调充斥网络几十年了,面向对象流派对C++的攻击并不是攻击的全部,最著名的甚至不是面向对象流派的攻击,C语言派系李纳斯托瓦兹(Linus,Linux之父)的疯狂开怼,才更引起世人瞩目吧!
所以,小侄子有这样的想法也在所难免。但是--
但是,这样的理解是正确的吗?
我首先问他,你才学几天,你就学到了多重继承、接口等概念了?可能吗?
他说这个到没有学到,这两天才初步开始学习继承、派生、虚函数的一些用法。
我说:首先多重继承是好是坏,不是你关心的,你一个连面向对象概念都还没搞清楚的人,没有资格评论这个。而且“多重继承”,正体现了C++的设计原则,我虽然有,但你可以不用。但当你需要用的时候,正好我有。这是一种设计哲学,现在也不是你关注的点。
其次,接口是OO(面向对象的简称)的重要特性,但不代表C++没有“接口”的提法,就没有“接口”的特性。你很快就会学到,我在这里就不啰嗦了。
而且,C++从没有说他是一门纯粹的面向对象的语言,什么是“纯粹的”OO语言?对C++而言,所有的特性都是为了体现某一种思想,解决某些复杂棘手的问题而设计的。从来也不可能为了让自己看起来像是一个“真正的面向对象的编程语言”,或者让了让自己看起来更像是一个“真正的函数式语言”,而进行改造或拖鞋。他永永远不会为了“规则”而丧失对“灵活、性能”的追求。
所以,你既然想学好这门伟大的语言,就压根不用关心他到底是不是什么“纯粹”的“oo语言”,他就是他自己。
最后,我们来说一说“友元(friendship)”这个特性。站在我的角度,
友元恰恰是C++面向对象思想的重要体现,更好的保护了对象的封装性。
(面试的时候,如果面试官问你如何看待友元?你这样回答,面试官肯定对你另眼相看,前提是你能解释清楚为什么)
先来简单的说几句什么是“OO”,OO核心就是万物皆可“类”。
把实际业务场景中的各种对象按照共性程度进行分“类”。然后再根据每个对象的具体情况对这个“类”进行实例化。
那既然是“对象”,就肯定是供其他“对象”使用的。一个不能被其他对象使用的对象,就是一个“废类”。就现实中的“你”和“我”,如果一个人连被人“利用”的价值都没有,那这个人的活着还有什么意义呢?能为他人提供”价值”,就是我们生而为人的最大价值。
比如说,有这样一户家庭,五口人,夫妻两人带着三个小孩,2个男孩,1个女孩。现在我们用面向对象的思维对这个家庭进行简单抽象。
首先,我们发现他们都是人,所以我们定义一个类,类的名字叫“人”(Person)。这5个人拥有一些共同的特性,比如“姓名”、“性别”、“年龄”,”健康状况”,而且他们都有自己的特长(这个案例非常适合讲继承、多态、protected、类的实例化、类成员的初始化、虚函数、纯虚函数等等,但是为了不跑题,暂不展开,以后有机会再展开讲讲)。
其他人想了解这个家庭成员的信息,那么哪些信息允许被了解呢?
比如可以了解这个家庭成员的“姓名”,“性别”,“年龄”,这三个信息都是任何人可以公开访问的。通过提供三个功能(类的public成员函数)来实现,比如getName()、getAge()、getSex()就可以了。如果允许他们的名字可以被修改,还可以提供一个setName(string name)来实现。
但是,现在问题来了。
如果这个家庭成员的“健康状况”有且只能由他们的家庭医生定期体检来确定的,怎么办?(这是合乎逻辑的,你想一想)
这时候如果这个类本身有一个setHealth()的方法,来设置这个对象的“健康状况”,那么应该是private、Protected、还是pbulic?
如果设置为private,那么这个方法就是个废的,没有任何实际用处,因为这类没有提供其它方法来调用这个方法,如果这个类通过它的其他的成员函数,来调用这个方法,也是违反了刚才的约定(这家人的"健康状况"只有他们的家庭医生可以设置)。其他对象,包括对象自身更不能进行设置了。
如果设置为protected,因为这个类并没有派生类,实际上和private没有区别。
如果是public,更糟糕了。任何对象不管自己是不是医生,都可以调用这个方法来设置这个人的“健康状况”了,这肯定不允许发生。
那怎么办呢?
就因为考虑到某个对象,通常会public出来一些方法,供其他任何对象都可以来使用,通过实例化这个类,并调用实例化的对象的public方法,来实现对这个对象的一些操作。
就像真实世界中,一个人的有些情况,所有人都可以知道。但是总有些情况,只能是自己身边的人(亲朋好友)才被允许知道,因此就诞生了“friendship”这个概念,通俗的说,只有这个类的“朋友”才能知道"某些特定的情况"。
因此给这个类声明一个“友元类”:家庭医生,这个“友元类”有一个public方法:setHealth(参数1:Person对象,参数2:health status)(当然也可以有其他实现方式,这里只是其中一种方法),通过这个“家庭医生”类的实例化对象调用setHealth方法来给对应的person对象的私有属性“健康状况”进行设置。
这才是一个合理的面向对象思维,才更加接近现实世界。
再假设一种情况,现在Person类的对象“男主人”,要去入职一家公司Company,对方要求了解“男主人”的健康状况,如果有疾病,就不予录用。
那么,这个getHealth方法该怎么处理?是作为person类的成员函数吗?如果作为person类的成员函数,是private、protected、还是public?(请稍微思考一下)
要知道“健康状况”是一个人的隐私,不可能轻易的公开给任何人知道,所以不能是public。如果是private和protected,那么company类也无法获取啊!(因为Company类在内部即便实例化了Person类,也无法获取健康状况。)
所以,这个时候,就该“友元”正式登场了!
我们把Company类作为Person类的“友元”类。然后company类定义一个public方法:getHealth()方法,把person类的对象引用传入进去,就可以获得了该对象的“健康状况”。
还避免了其他类随便了解Person类对象的“健康情况”。
你看,“友元”(friendship),不但没有破坏对象的封装,反而能够解决很多实际的问题,正是为了维护对象的封装性,避免随意暴露一个方法,被无关的类使用,“友元”才应运而生。包括“友元函数”、“友元类”都是一个道理。
另外,“友元”特性,还有一个非常常见的应用场景。如果一个类A,在某种情况下要对另一个类B的某哥private成员进行频繁的读取(在实际开发中这很常见),如果通过public方法调用,就存在这个方法被其他无关类使用的风险,这是一种危险的行为。通过把类A声明为类B的“友元”,就可以很方便的解决这个问题。
关于“友元”特性的讨论,今天只是非常简单的抛砖引玉,如果有不同的想法,非常欢迎讨论,在编程的路上,感谢有你。
段誉,2024.01.20.深夜,写于合肥。