什么时候应该在C ++中使用“朋友”?


354

我一直在阅读C ++常见问题解答,并对friend声明感到好奇。我个人从未使用过它,但是我对探索这种语言很感兴趣。

使用的一个好例子是friend什么?


再读一遍FAQ,我喜欢<< >>运算符重载的想法,并添加为这些类的朋友。但是我不确定这不会破坏封装。这些例外什么时候可以保持在OOP的严格程度之内?


5
尽管我同意朋友类不一定是坏事的答案,但我确实倾向于将其视为小代码。尽管并非总是如此,但它经常表明类层次结构需要重新考虑。
Mawg说

1
您将使用已经存在紧密耦合的朋友类。这就是它的目的。例如,数据库表及其索引紧密耦合。当表更改时,必须更新其所有索引。因此,类DBIndex会将DBTable声明为好友,以便DBTable可以直接访问索引内部。但是,没有DBIndex的公共接口。甚至读一个索引也没有意义。
shawnhcorey

缺乏实践经验的OOP“纯粹主义者”认为朋友违反了OOP原则,因为一个阶级应该是其私有国家的唯一维护者。这很好,除非遇到两种情况都需要维护一个共享私有状态的常见情况。
kaalus

Answers:


335

首先(IMO)不要听别人说friend没有用。它是有益的。在许多情况下,您将拥有对象,这些对象具有不打算公开提供的数据或功能。对于大型代码库尤其如此,其中许多作者可能只对不同领域有初步的了解。

朋友说明符有ARE替代项,但它们通常很麻烦(cpp级具体类/屏蔽的typedef)或不十分可靠(注释或函数名称约定)。

回答问题;

friend说明符允许指定的类访问在创建该Friend语句的类中受保护的数据或功能。例如,在下面的代码中,任何人都可以问孩子一个名字,但是只有母亲和孩子可以更改名字。

您可以通过考虑更复杂的类(例如Window)来进一步介绍这个简单的示例。Window很可能将具有许多不应公开访问的功能/数据元素,但相关类(例如WindowManager)需要使用这些功能/数据元素。

class Child
{
//Mother class members can access the private parts of class Child.
friend class Mother;

public:

  string name( void );

protected:

  void setName( string newName );
};

114
需要特别注意的是,C ++ FAQ提到了friend 增强封装的功能。friend授予对成员的选择性访问权限,就像这样protected做一样。任何细粒度的控制都比授予公共访问权要好。其他语言也定义了选择性访问机制,请考虑使用C#internal。有关使用的大多数负面批评friend都与紧密耦合有关,这通常被认为是一件坏事。但是,在某些情况下,更紧密的耦合正是您想要的,并friend为您提供了强大的功能。
安德烈·卡隆(AndréCaron)2010年

5
安德鲁,您能否详细介绍一下(cpp级具体类)和(掩码的typedef)?
奥马尔·奥斯曼(OmarOthman)2011年

18
这个答案似乎更侧重于解释什么friend是什么,而不是提供一个激励性的例子。Window / WindowManager示例比所示示例要好,但是太模糊了。此答案也没有解决问题的封装部分。
bames53

4
如此有效地存在“朋友”是因为C ++没有一个包概念,所有成员都可以在其中共享实现细节?我会对一个真实的示例非常感兴趣。
weberc2

1
@OMGtechy如果C ++具有包概念,则不必这样做,因此它与我先前的声明一致。这是Go中使用包而不是朋友访问私有成员的示例
weberc2 2014年

162

在工作中,我们广泛使用朋友来测试代码。这意味着我们可以为主要应用程序代码提供适当的封装和信息隐藏。但是我们也可以有单独的测试代码,该代码使用朋友检查内部状态和数据以进行测试。

可以说我不会将friend关键字用作您设计的必要组成部分。


那正是我用的目的。那或者只是将成员变量设置为protected。遗憾的是,它不适用于C ++ / CLI :-(
Jon Cage

12
我个人不建议这样做。通常,您正在测试接口,即一组输入是否给出了预期的一组输出。为什么需要检查内部数据?
Graeme 2012年

55
@Graeme:因为好的测试计划包括白盒测试和黑盒测试。
Ben Voigt 2013年

1
我倾向于同意@Graeme,正如在此答案中完美解释的那样。
亚历克西斯·勒克莱尔

2
@Graeme,它可能不是直接内部数据。我可能是在该数据上执行特定操作或任务的方法,其中该方法是该类专用的,不应公开访问,而其他某些对象可能需要使用其自己的数据来提供或植入该类的受保护方法。
弗朗西斯·库格勒

93

friend关键字有许多很好的用途。这是我立即可以看到的两种用途:

朋友定义

Friend定义允许在类范围内定义一个函数,但是该函数不会被定义为成员函数,而是被定义为封闭名称空间的自由函数,并且除了依赖于参数的查找之外,通常不会可见。这使得它对于运算符重载特别有用:

namespace utils {
    class f {
    private:
        typedef int int_type;
        int_type value;

    public:
        // let's assume it doesn't only need .value, but some
        // internal stuff.
        friend f operator+(f const& a, f const& b) {
            // name resolution finds names in class-scope. 
            // int_type is visible here.
            return f(a.value + b.value);
        }

        int getValue() const { return value; }
    };
}

int main() {
    utils::f a, b;
    std::cout << (a + b).getValue(); // valid
}

私人CRTP基本类别

有时,您发现策略需要访问派生类的需求:

// possible policy used for flexible-class.
template<typename Derived>
struct Policy {
    void doSomething() {
        // casting this to Derived* requires us to see that we are a 
        // base-class of Derived.
        some_type const& t = static_cast<Derived*>(this)->getSomething();
    }
};

// note, derived privately
template<template<typename> class SomePolicy>
struct FlexibleClass : private SomePolicy<FlexibleClass> {
    // we derive privately, so the base-class wouldn't notice that, 
    // (even though it's the base itself!), so we need a friend declaration
    // to make the base a friend of us.
    friend class SomePolicy<FlexibleClass>;

    void doStuff() {
         // calls doSomething of the policy
         this->doSomething();
    }

    // will return useful information
    some_type getSomething();
};

您将在答案中找到一个人为的示例。答案中使用的另一个代码。CRTP基础强制转换了此指针,以便能够使用数据成员指针访问派生类的数据字段。


嗨,我尝试使用CRTP时收到语法错误(在xcode 4中)。Xcode认为我正在尝试继承类模板。P<C>template<template<typename> class P> class C : P<C> {};指出“使用类模板C需要模板参数” 时发生错误。您是否遇到过同样的问题,或者知道解决方案?
bennedich 2012年

乍一看,@ bennedich看起来像是您在C ++功能支持不足的情况下遇到的那种错误。这在编译器中很常见。采用FlexibleClassFlexibleClass应隐指的是其自己的类型。
Yakk-Adam Nevraumont 2012年

@bennedich:使用C ++ 11更改了类主体中类模板名称的使用规则。尝试在编译器中启用C ++ 11模式。
Ben Voigt

在Visual Studio 2015中,添加以下公共元素:f(){}; f(int_type t):值(t){};为防止出现此编译器错误:错误C2440:'<函数样式广播>':无法从'utils :: f :: int_type'转换为'utils :: f'注意:任何构造函数都不能采用源类型或构造函数重载解析模棱两可
达米安

41

@roo:封装在这里没有中断,因为类本身规定了谁可以访问其私有成员。如果封装可能是由班级以外的人引起的,例如,如果您operator <<宣称“我是班级的朋友” ,则封装将被破坏foo

friend代替使用public,不使用private

实际上,C ++ FAQ 已经回答了这个问题。


14
“!朋友内容替换使用公共的,未使用的私有”,我第二是
瓦利德伊萨

26
@阿萨夫:是的,但在大多数情况下,FQA都是很多不连贯的愤怒胡言乱语,没有任何实际价值。该部分friend也不例外。唯一真正的观察结果是C ++仅在编译时确保封装。而且您无需再说任何话。剩下的就是胡扯。因此,总而言之:FQA的这​​一部分不值得一提。
康拉德·鲁道夫

12
大部分FQA都是blx :)
rama-jka toti 2010年

1
@Konrad:“这里唯一真正的观察是C ++仅在编译时确保封装。” 是否有任何语言在运行时确保这一点?据我所知,在C#,Java,Python和许多其他语言中,都允许返回对私有成员(和函数的引用,对于允许将函数或函数的指针作为第一类对象的语言)。
安德烈·卡隆

@André:据我所知,JVM和CLR实际上可以确保这一点。我不知道是否总是这样做,但是据称您可以保护包装/装配体免遭此类入侵(尽管我自己也不要这样做)。
Konrad Rudolph

27

典型示例是重载operator <<。另一个常见用途是允许助手或管理员类访问您的内部组件。

这是我听说过的一些有关C ++朋友的准则。最后一个特别令人难忘。

  • 您的朋友不是您孩子的朋友。
  • 您孩子的朋友不是您的朋友。
  • 只有朋友可以触摸您的私密部位。

典型的例子是重载operator <<。friend我猜不使用典型。
curiousguy

16

编辑:再读一遍常见问题,我喜欢<< >>运算符重载并将其添加为这些类的朋友的想法,但是我不确定这不会破坏封装

如何破坏封装?

当您允许对数据成员的无限制访问时,将破坏封装。考虑以下类别:

class c1 {
public:
  int x;
};

class c2 {
public:
  int foo();
private:
  int x;
};

class c3 {
  friend int foo();
private:
  int x;
};

c1明显不封装。任何人都可以x在其中阅读和修改。我们无法执行任何类型的访问控制。

c2显然是封装的。没有公开访问权限x。您所能做的就是调用foo函数,该函数对类执行一些有意义的操作

c3?封装少了吗?是否允许无限制访问x?是否允许未知功能访问?

否。它仅允许一个函数访问该类的私有成员。就像一样c2。就像一样c2,可以访问的一个函数不是“一些随机的,未知的函数”,而是“类定义中列出的函数”。就像c2,我们可以通过查看类定义来查看谁有权访问的完整列表。

那么,这种封装的精确程度如何呢?相同数量的代码可以访问该类的私有成员。而且大家谁有权访问被列在类的定义。

friend不会破坏封装。这使某些Java程序员感到不舒服,因为当他们说“ OOP”时,实际上是指 “ Java”。当他们说“封装”时,并不是说“必须保护私有成员免受任意访问”,而是“一个Java类,其中唯一能够访问私有成员的功能是类成员”,即使对于有几个原因

首先,正如已经显示的那样,它过于严格。没有理由不应该允许朋友方法做同样的事情。

其次,它是不是限制性不够。考虑第四类:

class c4 {
public:
  int getx();
  void setx(int x);
private:
  int x;
};

根据上述Java思维,这是完美封装的。 但是,它绝对允许任何人读取和修改x。那怎么办呢?(提示:没有)

底线:封装是关于能够控制哪些函数可以访问私有成员。这不是关于恰恰是这些功能的定义的位置。


10

安德鲁例子的另一个常见版本,可怕的代码对

parent.addChild(child);
child.setParent(parent);

不必担心两行是否总是一起并且以一致的顺序完成,您可以将方法设置为私有,并具有用于增强一致性的友好函数:

class Parent;

class Object {
private:
    void setParent(Parent&);

    friend void addChild(Parent& parent, Object& child);
};

class Parent : public Object {
private:
     void addChild(Object& child);

     friend void addChild(Parent& parent, Object& child);
};

void addChild(Parent& parent, Object& child) {
    if( &parent == &child ){ 
        wetPants(); 
    }
    parent.addChild(child);
    child.setParent(parent);
}

换句话说,可以使公共接口更小,并强制执行跨朋友函数中的类和对象的不变式。


6
为什么有人需要一个朋友呢?为什么不让addChild成员函数也设置父级呢?
纳瓦兹

1
一个更好的例子是结交setParent朋友,因为您不想允许客户端更改父对象,因为您将在功能的addChild/ removeChild类别中对其进行管理。
Ylisar 2014年

8

您使用私有/受保护/公共权限控制成员和功能的访问权限?因此,假设这3个级别中的每个级别的概念都很明确,那么很显然我们缺少了一些东西...

例如,将成员/函数声明为受保护对象是非常通用的。您是说每个人都无法使用此功能(当然,除了继承的孩子)。但是异常呢?每个安全系统都会让您拥有某种“白名单”,对吗?

因此,friend使您可以灵活地进行坚如磐石的对象隔离,但允许为您认为合理的事物创建“漏洞”。

我猜人们说它是不需要的,因为总有一种设计会没有它。我认为这类似于对全局变量的讨论:永远不要使用它们,总有一种不用它们的方法...但是实际上,您会看到最终以(几乎)最优雅的方式出现的情况。 ..我认为与朋友的情况相同。

除了让您无需设置功能即可访问成员变量外,它实际上并没有任何好处

嗯,这并不是看待它的确切方式。想法是控制WHO可以访问具有或不具有设置功能的功能


2
friend漏洞如何?它允许类中列出的方法访问其私有成员。它仍然不允许任意代码访问它们。因此,它与公共成员职能没有什么不同。
jalf

与C ++的C#/ Java包级访问一样,您的朋友也非常接近。@jalf-朋友班(例如工厂班)怎么样?
Ogre Psalm33

1
@Ogre:那他们呢?您仍在专门为该班级授课,而没有其他人可以访问该班级的内部信息。您不仅为任何未知的代码打开了大门,使您的课程陷入困境。
jalf

8

我找到了方便使用好友访问的地方:私有功能的单元测试。


但是,公共功能也可以用于此吗?使用朋友访问的优势是什么?
Zheng Qu

@Maverobot您能否详细说明您的问题?
弗拉基米尔

5

当您构建容器并且想要为该类实现迭代器时,Friend会派上用场。


4

在我以前工作过的一家公司中,我们遇到了一个有趣的问题,当时我们用朋友来体面地对待。我在框架部门工作,我们在自定义OS上创建了基本的引擎级系统。在内部,我们有一个类结构:

         Game
        /    \
 TwoPlayer  SinglePlayer

所有这些类都是框架的一部分,并由我们的团队维护。该公司生产的游戏是在此框架的基础上构建的,该框架源自一个Games子公司。问题是Game具有与SinglePlayer和TwoPlayer需要访问的各种事物的接口,但是我们不想在框架类之外公开。解决方案是将这些接口设为私有,并允许TwoPlayer和SinglePlayer通过友谊访问它们。

确实,可以通过更好地实施我们的系统来解决整个问题,但是我们被束之高阁。


4

简短的答案是:在实际改善封装性时使用friend。提高可读性和可用性(运算符<<和>>是典范示例)也是一个很好的理由。

至于改进封装的示例,专门设计为与其他类的内部一起使用的类(想到的是测试类)是不错的选择。


运营商<<和>>是典型的例子 ”号相反规范计数器的例子
curiousguy 2011年

@curiousguy:运营商<<>>通常的朋友,而不是成员,因为使他们的成员将使其难以使用。当然,我说的是那些运营商需要访问私有数据的情况。否则,友谊是无用的。
Gorpik 2012年

因为使他们的成员将使其难以使用。 ”很显然,使得operator<<operator>>值类的成员,而不是非会员或成员i|ostream,将不会提供所需的语法,而我建议它。“ 我说的是那些操作员需要访问私有数据的情况 ”我不太清楚为什么输入/输出操作员需要访问私有成员。
curiousguy 2012年

4

C ++的创建者说这并没有打破任何封装原则,我将引用他的话:

“朋友”会违反封装吗? 不,不是的。就像成员身份一样,“朋友”是授予访问权限的明确机制。您不能(在符合标准的程序中)在不修改类源的情况下授予自己访问类的权限。

十分清楚...


@curiousguy:即使使用模板,也是如此。
纳瓦兹

@Nawaz模板友谊可以被授予,但是任何人都可以在不修改友谊授予类的情况下进行新的部分或显式专业化。但是当您这样做时,请注意违反ODR的行为。而且无论如何不要这样做。
curiousguy 2012年

3

另一种用途:朋友(+虚拟继承)可被用来避免从一个类派生(又名:“使类underivable”)=> 12

2开始

 class Fred;

 class FredBase {
 private:
   friend class Fred;
   FredBase() { }
 };

 class Fred : private virtual FredBase {
 public:
   ...
 }; 

3

为了进行TDD多次,我在C ++中使用了'friend'关键字。

朋友可以了解我的一切吗?


更新:我从Bjarne Stroustrup网站上找到了有关“ friend”关键字的有价值的答案。

就像成员身份一样,“朋友”是授予访问权限的明确机制。


3

您必须在何时何地使用该friend关键字时非常小心,并且像您一样,我很少使用它。以下是一些使用注意事项friend和替代方法。

假设您要比较两个对象以查看它们是否相等。您可以:

  • 使用访问器方法进行比较(检查每个ivar并确定相等性)。
  • 或者,您可以通过将其公开而直接访问所有成员。

第一种选择的问题在于,这可能是很多访问器,它比直接变量访问(略)慢,读起来又麻烦。第二种方法的问题是您完全破坏了封装。

最好的是,如果我们可以定义一个外部函数,该函数仍可以访问类的私有成员。我们可以使用friend关键字:

class Beer {
public:
    friend bool equal(Beer a, Beer b);
private:
    // ...
};

equal(Beer, Beer)现在,该方法可以直接访问ab的私有成员(可能是char *brandfloat percentAlcohol等)。这是一个非常人为的示例,您将尽快将其应用于friend重载== operator,但我们将继续进行介绍。

注意事项:

  • A friend不是该类的成员函数
  • 这是一个普通功能,可以特别访问该类的私有成员
  • 请勿将所有访问器和mutator替换为朋友(您最好做所有事情public!)
  • 友谊不是互惠的
  • 友谊不是传递的
  • 友谊不是继承的
  • 或者,正如C ++常见问题解答所解释的:“仅因为我授予您对我的友谊访问权限,并不会自动授予您的孩子对我的访问权限,不会自动授予您的朋友对我的访问权限,也不会自动授予我对您的访问权限”。

我只是friends在用另一种方法很难做到时才使用。再举一个例子,很多矢量数学函数通常创建为friends因的互操作性Mat2x2Mat3x3Mat4x4Vec2Vec3Vec4,等它只是这么容易成为朋友,而不是让到处使用存取。如前所述,friend通常在应用于<<(调试非常方便)>>以及==运算符时很有用,但也可以用于以下内容:

class Birds {
public:
    friend Birds operator +(Birds, Birds);
private:
    int numberInFlock;
};


Birds operator +(Birds b1, Birds b2) {
    Birds temp;
    temp.numberInFlock = b1.numberInFlock + b2.numberInFlock;
    return temp;
}

就像我说的那样,我一点也不friend经常使用,但是时不时地满足您的需求。希望这可以帮助!


2

关于运算符<<和operator >>,没有充分的理由结识这些运算符。的确,它们不应成为成员函数,但也不必成为朋友。

最好的办法是创建公共打印(ostream&)和读取(istream&)函数。然后,根据这些函数编写operator <<和operator >>。这提供了使您能够将这些功能虚拟化的附加好处,从而提供了虚拟序列化。


关于运算符<<和运算符>>,没有充分的理由结识这些运算符。 ”绝对正确。“ 这提供了使您可以将这些函数虚拟化的额外好处, ”如果相关类打算派生,则可以。否则,何必呢?
curiousguy 2011年

我真的不明白为什么这个答案被否决了两次-甚至没有解释!很不礼貌。
curiousguy 2011年

虚拟将增加性能的打击,在序列化中可能会很大
paulm 2015年

2

我仅使用friend-keyword对受保护的功能进行单元测试。有人会说您不应该测试受保护的功能。但是,我在添加新功能时发现此工具非常有用。

但是,我没有在类声明中直接使用关键字,而是使用了一个漂亮的模板技巧来实现这一点:

template<typename T>
class FriendIdentity {
public:
  typedef T me;
};

/**
 * A class to get access to protected stuff in unittests. Don't use
 * directly, use friendMe() instead.
 */
template<class ToFriend, typename ParentClass>
class Friender: public ParentClass
{
public:
  Friender() {}
  virtual ~Friender() {}
private:
// MSVC != GCC
#ifdef _MSC_VER
  friend ToFriend;
#else
  friend class FriendIdentity<ToFriend>::me;
#endif
};

/**
 * Gives access to protected variables/functions in unittests.
 * Usage: <code>friendMe(this, someprotectedobject).someProtectedMethod();</code>
 */
template<typename Tester, typename ParentClass>
Friender<Tester, ParentClass> & 
friendMe(Tester * me, ParentClass & instance)
{
    return (Friender<Tester, ParentClass> &)(instance);
}

这使我能够执行以下操作:

friendMe(this, someClassInstance).someProtectedFunction();

至少适用于GCC和MSVC。


2

在C ++中,“ friend”关键字在运算符重载和制作Bridge中很有用。

1.)运算符重载中的Friend关键字:运算符重载的
示例是:假设我们有一个“点”类,它具有两个浮点变量
“ x”(用于x坐标)和“ y”(用于y坐标)。现在我们必须重载"<<"(提取运算符),以便如果调用"cout << pointobj"它,它将打印x和y坐标(其中pointobj是Point类的对象)。为此,我们有两个选择:

   1.在“ ostream”类中重载“ operator <<()”函数。
   2.在“ Point”类中重载“ operator <<()”函数。
现在,第一个选项不好,因为如果我们需要为另一个不同的类再次重载该运算符,则必须再次对“ ostream”类进行更改。
这就是为什么第二个是最佳选择。现在编译器可以调用 "operator <<()"函数:

   1.使用ostream对象cout.As:cout.operator <<(Pointobj)(形成ostream类)。
2.不带对象的调用,例如:operator <<(cout,Pointobj)(来自Point类)。

因为我们已经在Point类中实现了重载。因此,要在没有对象的情况下调用此函数,我们必须添加"friend"关键字,因为我们可以在没有对象的情况下调用朋友功能。现在,函数声明将为:
"friend ostream &operator<<(ostream &cout, Point &pointobj);"

2.)建立网桥的Friend关键字:
假设我们必须创建一个函数,在该函数中,我们必须访问两个或多个类的私有成员(通常称为“ bridge”)。操作方法:
要访问某个类的私有成员,它应该是该类的成员。现在,要访问其他类的私有成员,每个类都应将该函数声明为朋友函数。例如:假设有两个类A和B。一个函数"funcBridge()"要访问两个类的私有成员。然后两个类都应声明"funcBridge()"为:
friend return_type funcBridge(A &a_obj, B & b_obj);

我认为这将有助于了解朋友关键字。


2

正如朋友声明的参考所言:

朋友声明出现在类主体中,并向出现朋友声明的类的私有和受保护成员授予函数或其他类访问权限。

谨在此提醒您,某些答案中存在技术错误,这些错误说friend只能访问受保护的成员。


1

树示例是一个很好的示例:在几个不同的类中实现一个对象而没有继承关系。

也许您还需要保护构造函数,并迫使人们使用您的“朋友”工厂。

...好吧,坦率地说,你可以没有它。


1

为了进行TDD多次,我在C ++中使用了'friend'关键字。
朋友可以了解我的一切吗?

不,这只是友谊的一种方式:`(


1

我使用的一个特定实例friend是创建Singleton类时。的friend关键字让我创建访问功能,这比总是具有对类“的GetInstance()”方法更加简洁。

/////////////////////////
// Header file
class MySingleton
{
private:
    // Private c-tor for Singleton pattern
    MySingleton() {}

    friend MySingleton& GetMySingleton();
}

// Accessor function - less verbose than having a "GetInstance()"
//   static function on the class
MySingleton& GetMySingleton();


/////////////////////////
// Implementation file
MySingleton& GetMySingleton()
{
    static MySingleton theInstance;
    return theInstance;
}

这可能是个问题,但我认为节省一些按键并不能证明在这里使用friend是合理的。GetMySingleton()应该是该类的静态方法。
Gorpik

私有c-tor将不允许使用非朋友功能来实例化MySingleton,因此此处需要使用friend关键字。
JBR威尔金森

@Gorpik“ 这可能是个问题,但我认为保存一些击键并不能证明在这里使用 Friend 是合理的。 ”确实如此。无论如何,friend不是需要一个特定的“正当理由”,增加一个成员函数时没有。
curiousguy

单身被认为是不好的做法,反正(谷歌“单身有害的”,你会得到很多结果像这样,我不使用功能来实现反模式可以被认为是一个很好地利用该功能的思考。
weberc2

1

Friend函数和类提供对类的私有成员和受保护成员的直接访问,以避免在一般情况下破坏封装。大多数用法是与ostream一起使用的:我们希望能够输入:

Point p;
cout << p;

但是,这可能需要访问Point的私有数据,因此我们定义了重载运算符

friend ostream& operator<<(ostream& output, const Point& p);

但是,有明显的封装含义。首先,现在,朋友类或函数可以完全访问该类的所有成员,即使是不符合其需要的成员。其次,该类和该好友的实现现在已陷入困境,以至于该类的内部更改可能破坏该好友。

从逻辑上讲,如果您将朋友视为课程的扩展,那么这不是问题。但是,在那种情况下,为什么首先要拔出朋友。

为了实现“朋友”声称的相同目的,但又不破坏封装,可以做到这一点:

class A
{
public:
    void need_your_data(B & myBuddy)
    {
        myBuddy.take_this_name(name_);
    }
private:
    string name_;
};

class B
{
public:
    void print_buddy_name(A & myBuddy)
    {
        myBuddy.need_your_data(*this);
    }
    void take_this_name(const string & name)
    {
        cout << name;
    }
}; 

封装没有中断,类B无法访问A中的内部实现,但是结果与我们将B声明为A的朋友一样。编译器将优化掉函数调用,因此结果相同指示为直接访问。

我认为,使用“朋友”只是带来好处的捷径,但是却有一定的成本。


1

这可能不是实际的用例情况,但可能有助于说明类之间使用friend的情况。

会所

class ClubHouse {
public:
    friend class VIPMember; // VIP Members Have Full Access To Class
private:
    unsigned nonMembers_;
    unsigned paidMembers_;
    unsigned vipMembers;

    std::vector<Member> members_;
public:
    ClubHouse() : nonMembers_(0), paidMembers_(0), vipMembers(0) {}

    addMember( const Member& member ) { // ...code }   
    void updateMembership( unsigned memberID, Member::MembershipType type ) { // ...code }
    Amenity getAmenity( unsigned memberID ) { // ...code }

protected:
    void joinVIPEvent( unsigned memberID ) { // ...code }

}; // ClubHouse

会员班级

class Member {
public:
    enum MemberShipType {
        NON_MEMBER_PAID_EVENT,   // Single Event Paid (At Door)
        PAID_MEMBERSHIP,         // Monthly - Yearly Subscription
        VIP_MEMBERSHIP,          // Highest Possible Membership
    }; // MemberShipType

protected:
    MemberShipType type_;
    unsigned id_;
    Amenity amenity_;
public:
    Member( unsigned id, MemberShipType type ) : id_(id), type_(type) {}
    virtual ~Member(){}
    unsigned getId() const { return id_; }
    MemberShipType getType() const { return type_; }
    virtual void getAmenityFromClubHouse() = 0       
};

class NonMember : public Member {
public:
   explicit NonMember( unsigned id ) : Member( id, MemberShipType::NON_MEMBER_PAID_EVENT ) {}   

   void getAmenityFromClubHouse() override {
       Amenity = ClubHouse::getAmenity( this->id_ );
    }
};

class PaidMember : public Member {
public:
    explicit PaidMember( unsigned id ) : Member( id, MemberShipType::PAID_MEMBERSHIP ) {}

    void getAmenityFromClubHouse() override {
       Amenity = ClubHouse::getAmenity( this->id_ );
    }
};

class VIPMember : public Member {
public:
    friend class ClubHouse;
public:
    explicit VIPMember( unsigned id ) : Member( id, MemberShipType::VIP_MEMBERSHIP ) {}

    void getAmenityFromClubHouse() override {
       Amenity = ClubHouse::getAmenity( this->id_ );
    }

    void attendVIPEvent() {
        ClubHouse::joinVIPEvent( this->id );
    }
};

便利设施

class Amenity{};

如果您在这里查看这些类的关系;ClubHouse拥有各种不同类型的会员资格和会员访问权限。成员都是从父类或基类派生的,因为它们都共享一个通用的ID和枚举类型,并且外部类可以通过在基类中找到的访问函数来访问其ID和类型。

但是,通过成员及其派生类的此类层次结构以及它们与ClubHouse类的关系,VIPMember类是具有“特殊特权”的派生类中唯一的一个。基类和其他2个派生类无法访问ClubHouse的joinVIPEvent()方法,但VIP成员类具有该特权,就好像它可以完全访问该事件一样。

因此,对于VIPMember和ClubHouse,这是一条双向通行的道路,其他会员级别受到限制。


0

在为类实现树算法时,教授给我们的框架代码将树类作为节点类的朋友。

除了让您无需设置功能即可访问成员变量外,它实际上并没有任何好处。


0

当不同的类(不从另一个类继承)使用另一个类的私有或受保护成员时,可以使用友谊。

朋友功能的典型用例是在两个不同类之间进行的操作,这些类访问二者的私有或受保护成员。

来自http://www.cplusplus.com/doc/tutorial/inheritance/

您可以看到此示例,其中非成员方法访问类的私有成员。该方法必须在该类中声明为该类的朋友。

// friend functions
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle() {}
    Rectangle (int x, int y) : width(x), height(y) {}
    int area() {return width * height;}
    friend Rectangle duplicate (const Rectangle&);
};

Rectangle duplicate (const Rectangle& param)
{
  Rectangle res;
  res.width = param.width*2;
  res.height = param.height*2;
  return res;
}

int main () {
  Rectangle foo;
  Rectangle bar (2,3);
  foo = duplicate (bar);
  cout << foo.area() << '\n';
  return 0;
}

0

可能我从上面的答案中遗漏了一些东西,但是封装中的另一个重要概念是隐藏实现。减少对私有数据成员(类的实现细节)的访问,可以在以后轻松得多地修改代码。如果朋友直接访问私有数据,则对实现数据字段(私有数据)的任何更改都会破坏访问该数据的代码。使用访问方法通常可以消除这种情况。我认为非常重要。


-1

可以遵守最严格和最纯正的OOP原则,并确保任何类的数据成员都没有访问器,因此,所有对象必须是唯一可以了解其数据的对象,唯一的处理方式是通过间接消息,即方法。

但是,即使C#都有内部可见性关键字,Java 在某些方面也具有其默认的程序包级别可访问性。实际上,C ++通过准确指定一个其他类以及其他类可以看到的类,从而最小化了对类的可见性的折衷,从而更接近于OOP的理想状态。

我并没有真正使用C ++,但是如果C#有好朋友,我会使用它,而不是我实际上使用很多的assembly-global 内部修饰符。它并没有真正破坏封装,因为.NET中的部署单元程序集。

但是还有一个InternalsVisibleTo Attribute(otherAssembly),它就像一个跨程序集机制。Microsoft将其用于可视设计器程序集。


-1

朋友对于回调也很有用。您可以将回调实现为静态方法

class MyFoo
{
private:
    static void callback(void * data, void * clientData);
    void localCallback();
    ...
};

在内部callback调用localCallback,并且其中clientData包含您的实例。在我看来,

要么...

class MyFoo
{
    friend void callback(void * data, void * callData);
    void localCallback();
}

这允许在纯cpp中将朋友定义为c样式的函数,而不会使类混乱。

同样,我经常看到的一种模式是将一个类的所有真正私有成员放入另一个类,该类在标头中声明,在cpp中定义并成为友好对象。这使编码人员可以对标头的用户隐藏该类的许多复杂性和内部工作。

在标题中:

class MyFooPrivate;
class MyFoo
{
    friend class MyFooPrivate;
public:
    MyFoo();
    // Public stuff
private:
    MyFooPrivate _private;
    // Other private members as needed
};

在cpp中

class MyFooPrivate
{
public:
   MyFoo *owner;
   // Your complexity here
};

MyFoo::MyFoo()
{
    this->_private->owner = this;
}

隐藏下游不需要的东西变得更加容易。


1
接口不是实现这一目标的更干净的方法吗?阻止某人查找MyFooPrivate.h的方法是什么?
JBRWilkinson

1
好吧,如果您使用私人和公共场合保守秘密,那么您将很容易被击败。我的意思是“隐藏”,MyFoo的用户实际上不需要查看私有成员。除此之外,保持ABI兼容性非常有用。如果使_private成为指针,则私有实现可以根据需要进行任意更改,而无需触摸公共接口,从而保持ABI兼容性。
shash

您指的是PIMPL习惯用语;目的不是像您在说的那样进行额外的封装,而是将实现细节移出了标头,因此更改实现细节不会强制重新编译客户端代码。此外,无需使用friend来实现此惯用语。
weberc2

嗯,是。其主要目的是移动实施细节。那里的朋友从私人或其他方式来处理公共班级中的私人成员很有用。
shash 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.