18

一些程序员说,“友元函数破坏了 C++ 中的封装”。一些程序员还说,“朋友函数不会破坏封装;相反,它们自然地扩展了封装屏障”

这是什么意思?..

如果友元函数破坏了 C++ 中的封装,那该怎么办?

4

10 回答 10

25

引用自C++ FAQ我认为很好地描述了朋友和封装的情况。

不!如果使用得当,它们会增强封装性。

当两半具有不同数量的实例或不同的生命周期时,您通常需要将一个类分成两半。在这些情况下,两半通常需要直接访问彼此(这两半曾经在同一个类中,因此您没有增加需要直接访问数据结构的代码量;您只是重新洗牌代码分为两个类而不是一个)。实现这一点的最安全方法是让两半成为彼此的朋友。

如果您使用刚刚描述的朋友,您将保持私密的私密性。不理解这一点的人通常会天真地努力避免在上述情况下使用友谊,并且通常他们实际上破坏了封装。它们要么使用公共数据(怪诞!),要么通过公共 get() 和 set() 成员函数使数据在两半之间可访问。只有当私有数据从类外部(从用户的角度)“有意义”时,才能为私有数据使用公共 get() 和 set() 成员函数。在许多情况下,这些 get()/set() 成员函数几乎与公共数据一样糟糕:它们(仅)隐藏了私有数据的名称,但它们不隐藏私有数据的存在。

于 2009-07-07T17:32:19.803 回答
17

有人说“朋友”打破封装的原因是因为封装数据和功能的全部意义在于,没有其他需要看到数据的东西可以,但是朋友让其他一些类看到里面。

我个人认为朋友们不要破坏封装,因为类不应该完全依赖,而是相互依赖。

拿汽车做类比。我们通常将其用作抽象模型,说我们不需要知道引擎是如何工作的,就可以知道踩下油门就可以了。这就是封装的全部思想,其中只有我们需要与类交互的功能才是我们需要了解的唯一功能。

但是,Mechanic 课程肯定需要了解汽车的具体内部工作原理,但是将 Mechanic 构建到汽车中是没有意义的。

这就是朋友进来的地方。你让机械师成为引擎、刹车或任何需要修理的东西的朋友,他可以修理它。如果他所能做的就是按下油门/刹车,他就无法修复它。

于 2009-07-07T17:28:56.953 回答
9

封装意味着你不到里面的东西,也friend意味着你可以看到里面。因此,根据您的观点,friend要么打破封装(让朋友看到内部),要么扩展它(让开发人员只放松对特定朋友的障碍)。

FWIW,一个好的经验法则是说只有嵌套/内部类应该被声明为朋友;这条规则可以概括为“没有远距离的朋友”,即一个类的朋友(如果有的话)应该和类本身在同一个头文件中声明。

于 2009-07-07T17:27:14.103 回答
7

我从 Stroustrup 的The Design and Evolution of C++

2.10 The Protection Model,pg 中 发布了一个片段。53.
[...]

友谊声明被视为一种类似于一个保护域授予另一个保护域读写能力的机制。它是类声明的显式和特定部分。因此,我从来没有看到反复出现的断言,即friend 声明“违反封装”只是无知和与非 C++ 术语混淆的组合。

于 2009-07-07T17:44:27.080 回答
3

您可以将 C++ 视为具有非常原始的访问控制系统 - 您可以访问所有成员(朋友或成员函数),或者您只能访问公共成员(除...之外的所有其他内容),或者您可以访问公共和受保护的成员(...继承的特殊规则)。

抽象的“封装”同样是一个原始的访问控制系统,只是我们通常说“类的一部分”的代码是特权的,可以以语言允许的所有方式操纵对象。不是“类的一部分”的代码是无特权的,必须使用更小的、可能已发布的公共接口。

那么,什么是“朋友”呢?如果你这样想,那是为了编写不是成员函数的特权代码。这是必要的,因为有些东西由于技术原因不能成为成员函数。我能想到的是运算符重载,您需要在 LHS 上进行转换,以及标准算法模板函数的特化,如 std::swap (后者问题较小,因为如果有公共交换函数,它就是公共交换方法也不太可能有害。但是很高兴支持那些希望接口正交的人)。

那么问题是,拥有不是成员函数的特权代码是否会破坏封装?在 Java 中,您可能会说“是”,因为如果 Java 代码在类定义中,它显然是“类的一部分”,如果不是,则不是类的一部分。

在 C++ 中,它有点不太清楚,因为成员函数定义不必在类定义中开始。两者之间几乎没有区别:

// Foo.h
class Foo;
void bar(Foo &);

class Foo {
    friend void bar(Foo &);
public:
    static void baz(Foo &);
};

// Foo.cpp
void bar(Foo &f) {
    // access private members of f
}
void Foo::baz(Foo &f) {
    // access private members of f
}

我无法说服自己在任何有意义的意义上 bar “打破封装”,而 baz “保留封装”。即使是很小程度的实用主义也表明 bar 和 baz 做的是完全相同的事情,而且尽管 bar 不是成员函数,但它显然和 baz 一样是“类的一部分”。唯一的区别是语法,与封装无关。

另一方面,显然“朋友”可以用来完全打破封装,通过命名你可以认为是你的朋友的所有其他类(比如让你的所有成员在 Java 中公开或包保护,或者在 C++ 中让你的所有成员公开)。“朋友”确实需要使用类而不仅仅是方法,以便您可以在适当的情况下将嵌套类声明为朋友,并拥有不嵌套的紧密耦合类的小集群(也就是说:绘制边界比单个类更高级别的封装)。如果你用它来紧密耦合你所有的类,那么你最终可能会得到糟糕的代码。

这并不是真的因为“朋友”,因为所做的一切都给了我一个新的语法来公开我本来可以公开为“公共”的成员。但是,如果我将“朋友”作为一种变通方法,并用它来突破我自己不完善的公共接口,那么“朋友”实际上给了我一个糟糕设计的借口。将来我可能会发誓戒酒,并建议其他人也这样做,但这与我每次醒来时都发誓戒酒的原因相同。其他人可能足够聪明或幸运地享受这些好处而不会产生重大副作用。

因此,“朋友”本身就允许破坏封装,就像指针算术一样,就像酒精允许在凌晨 3 点落入阴沟一样。然而,通过谨慎和体面的设计,friend 的特定用途不应该也不需要破坏封装。而且,如果任何潜在的未来雇主正在阅读这篇文章,当我喝酒时,我通常不会挂在上面;-)

最终的问题是,在我所知道的每一种语言中,接口的行为都类似于继承,因为类的公共接口包含了它实现的所有接口的所有成员。因此,类接口比任何其他接口都大。这不是一件坏事,因为“is-a”关系是 OO 的关键。此外,它与您发布接口时发生的情况非常吻合,即任何客户端都可能使用它。它只是不能自然地适应许多设计所要求的,即默认情况下类具有接口,但也为“超级用户”提供更大的接口。因此,大界面是默认的,“子用户”坚持无聊的部分。

因此,friend 是一种为“超级用户”提供更大界面而不影响“子用户”界面的生硬工具。但是,由于它太直率了,它实际上只有在相关类都设计在一起时才有效,而关于类应该如何耦合的争论会导致关于什么是“破坏封装”的分歧。请记住,语言中的访问级别不应该强制封装,特别是 C++ 是一种多范式语言。所以C++在帮助程序员强制封装方面做了一些努力,但程序员还是要好好设计,按照自己的编程原则使用可用的特性。

于 2009-07-07T18:22:44.490 回答
3

你有两种不同的思想流派。

第一个(Java 和 C# ...):: 在类的公共区域中抛出每个函数,无论该函数是否“需要”访问“私有”区域。

第二个(C++ ...)::“仅”提供所需的函数,以便此类可以生存,并在创建类型的不相关集合中提供更高级别的函数。

恕我直言::C++ 清楚地满足了 OOP 目标。

于 2009-07-07T17:26:09.300 回答
0

friend 关键字允许其他类访问 C++ 中的私有和受保护数据成员。有些人认为这是破坏封装,其他人认为它将封装扩展到少数需要访问私有/受保护数据的外部类。“破坏封装”意味着成员和方法通常是私有的,并且friend关键字破坏了该封装。

在代码中我看到friend关键字通常会破坏封装并降低代码质量,但偶尔也会出现使用得当的情况。

如果您以前从未见过,这里有一个关于类友谊和继承的简单教程:

友谊与传承

于 2009-07-07T17:22:53.940 回答
0

一旦你声明了一个友元函数,访问说明符,因此,你的类的封装细节将可以被那个友元函数访问,这不是你的类的一部分/方法。因此,您基本上将对内部数据结构和方法的一些控制权委托给您“信任”的外部函数

于 2009-07-07T17:24:19.430 回答
0

友谊以牺牲一些封装为代价来促进凝聚力。您仍然有一个封装单元(类及其朋友),尽管它的粒度较小。但是当替代方案是为类添加额外的功能时,这不一定属于,使用友谊可以让您获得所需的访问权限,同时仍然保持类的凝聚力。封装不是 OOP 的唯一,甚至不是最重要的原则。

于 2009-07-07T17:40:24.200 回答
-2

如果你在每个可以访问私有数据的人周围画一条线,那就是数据被封装的程度。

如果您使用friend关键字,则必须围绕更多事物绘制这条线。因此,根据您的观点,封装被扩展或破坏。

一般来说,我不鼓励使用friend. 而是考虑为私有数据添加访问器。

于 2009-07-07T17:24:55.340 回答