你在什么情况下会使用友元函数而不是静态成员函数?

83

当我们希望一个非成员函数访问类的私有成员时,我们将其声明为该类的友元函数。这使得它具有与静态成员函数相同的访问权限。无论哪种方式,你都可以获得一个与该类的任何实例都没有关联的函数。

什么情况下必须使用友元函数? 何时必须使用静态函数? 如果两者都可以解决问题,如何权衡它们的适用性?默认情况下是否应该优先考虑其中之一?

例如,当实现一个工厂来创建只有私有构造函数的类foo的实例时,该工厂函数应该是foo的静态成员(您将调用foo::create())还是友元函数(您将调用create_foo())?


8
“朋友”的属性和“静态”的属性是正交的。它们之间没有可互换性,这使得你的问题听起来像是“你在哪里用车代替显微镜?”请问,您的问题到底简化了什么?您为什么要问这个问题?否则,即使想开始解决这个问题也很难。 - AnT stands with Russia
4
如果您过载运算符,可以通过两种方式完成,那么哪种更好呢? - Swapna
9
它们非常相关,几乎没有什么区别。 - pm100
2
@g-makulik 我认为这个问题实际上是在问友元非成员函数与静态成员函数的区别,如果是这样的话,我会说它们并不真正正交。 - Joseph Mansfield
2
@g-makulik 公正的观点。我已经详细阐述了问题。 - Joseph Mansfield
显示剩余15条评论
15个回答

84

Bjarne Stroustrup的《C++程序设计语言》第11.5节指出,普通成员函数有三个特点:

  1. 可以访问类的内部
  2. 在类的作用域中
  3. 必须通过实例调用

友元函数只有一个特点。

静态函数具有1和2两个特点。


3
在我看来,尽管历史上有不同的用法,但“friend”比“static”(成员函数)更为通用,因为它们可以完成相同的工作,但其命名空间范围更加灵活。一个小问题是友元函数不能与类实例一起调用(例如A a; a.static_member(); ),但这种用法并不常见。 - alfC
9
在类的相同范围内有何利弊? - Joseph Mansfield
1
@sftrabbit,你应该把它发表为一个问题。答案可能会很长,并且超出了这个回答的范围。 - Drew Dormann
你确定引用正确吗?Bjarne Stroustrup的《C++程序设计语言》第11.5节是关于“显式类型转换”的,似乎没有提到静态函数和友元函数之间的区别。 - Pharap
你只是列出了可能的好处清单,而楼主询问的是你为什么和在哪里比其他人更喜欢这些好处。对于再次提及此事,我很抱歉。 - edmz

54

这个问题似乎涉及到程序员需要引入一个不适用于任何类的实例的函数(因此可以选择静态成员函数)。因此,我将限制本答案的范围为以下设计情况,即在静态函数f()和友元自由函数f()之间进行选择:

struct A
{
    static void f();     // Better this...
private:
    friend void f();  // ...or this?
    static int x;
};

int A::x = 0;

void A::f() // Defines static function
{
    cout << x;
}

void f() // Defines friend free function
{
    cout << A::x;
}

int main()
{
    A::f(); // Invokes static function
    f();    // Invokes friend free function
}

在不了解f()A的语义(稍后我会回来讨论这个问题)的情况下,这种局限性场景有一个简单的答案:static函数更好。我认为有两个原因。


通用算法:

主要原因是可以编写以下模板:

template<typename T> void g() { T::f(); }

如果我们有两个或更多的类,在它们的接口中都有一个静态函数f(),那么这将允许我们编写一个单一的函数,以通用方式在任何这样的类上调用f()
如果我们将f()作为自由的非成员函数,则无法编写等效的通用函数。虽然我们可以将f()放入命名空间中,以便使用N::f()语法来模仿A::f()语法,但仍然不可能编写如上所示的g<>()模板函数,因为命名空间名称不是有效的模板参数。
第二个原因是,如果我们将自由函数f()放入命名空间中,就不能直接在类定义中内联其定义,而不引入任何其他声明f()的声明。
struct A
{
    static void f() { cout << x; } // OK
private:
    friend void N::f() { cout << x; } // ERROR 
    static int x;
};

为了解决上述问题,我们需要在类A的定义之前加上以下声明:
namespace N
{
    void f(); // Declaration of f() inside namespace N
}

struct A
{
    ...
private:
    friend void N::f() { cout << x; } // OK
    ...
};

然而,这样做违背了我们只在一个地方声明和定义f()的意图。
此外,如果我们想要分别声明和定义f(),同时保持f()在命名空间中,我们仍然需要在A类定义之前引入f()的声明:否则编译器会抱怨f()必须在命名空间N内声明才能合法使用N::f名称。
因此,现在我们将有f()在三个不同的地方提到而不是两个(声明和定义):
- 在A定义之前,在命名空间N内声明; - 在A定义内部的friend声明; - 在命名空间N内定义f()
一般来说,f()的声明和定义不能合并的原因是f()应该访问A的内部,因此,在定义f()时必须看到A的定义。然而,正如先前所述,在进行相应的friend声明之前,必须看到N内部的f()声明。这实际上强制我们分离f()的声明和定义。
语义上考虑:
虽然上述两点是普遍有效的,但有些原因会使人们更喜欢将f()声明为static而不是将其作为Afriend或反之,这是由话题的范围驱动的。
需要澄清的是,一个类的成员函数,无论它是静态的还是非静态的,逻辑上都是该类的一部分。它对其定义做出了贡献,从而提供了概念上的特征。
另一方面,尽管被授予访问其友元类内部成员的权限,但friend函数仍然是逻辑上外部于类定义的算法。
一个函数可以是多个类的友元,但只能是一个类的成员。
因此,在特定的应用领域中,设计师在决定将函数作为友元或成为后者的成员时,可能希望考虑到两者的语义(这不仅适用于静态函数,还适用于非静态函数,其中可能会涉及其他语言约束)。
函数在逻辑上是否有助于表征类及/或其行为,还是它更像是一个外部算法?这个问题需要了解特定的应用领域才能回答。
品味: 我认为除了刚才给出的理由之外,任何其他争论都纯粹源自于品味:实际上,自由的友元和静态成员方法都允许清楚地声明类的接口位于单一位置(类的定义),因此从设计角度来看,它们是等效的(当然,要考虑上述观察结果)。
剩下的区别是风格上的:我们是否想在声明函数时写入static关键字或friend关键字,以及我们是否想在定义类时写入A::类作用域限定符而非N::命名空间作用域限定符。因此,我不会再进一步评论这个问题。

5
好答案。这正是我在寻找的那种东西。 - Joseph Mansfield
@AndyProwl 一个命名空间中结合类和友元函数怎么样?友元函数是非成员函数,因此它们比静态成员函数提供更好的封装。Scott Meyers在第23项建议使用非成员函数。并且在这里:http://cpptips.com/nmemfunc_encap - Gusev Slava

13

区别在于明确表达类和函数之间关系的意图。

当您想要有意地指示两个不相关的类或类和函数之间存在强耦合和特殊关系时,使用friend

当函数在逻辑上是其所属类的一部分时,请使用static成员函数。


7
友元函数(以及类)可以访问您的类的私有和受保护成员。但一般情况下,很少有使用友元函数或类的好处,建议尽量避免使用。
静态函数只能访问静态数据(也就是类作用域的数据)。它们可以在不创建类实例的情况下调用。静态函数适用于以下场景:
  • 作为回调函数
  • 操作类作用域成员
  • 检索常量数据,这些数据不希望枚举在头文件中

  • 这个,我很高兴有人发帖说它们通常应该避免使用! - Craig
    3
    这个回答有点误导性。静态函数可以访问类的私有数据;但是它们必须有某种方式来访问实例(即将一个实例传递给静态函数)。一个常见的例子是工厂方法创建一个对象,然后修改其内部内容,然后将其返回给调用者。 - pm100
    1
    这并不会引起误解。静态函数无法访问实例数据。它可以通过实例访问实例数据,但不能直接访问。 - user1401452

    5
    静态函数是指当您想要一个在类的每个实例中都相同的函数时使用的函数。这样的函数没有访问“this”指针的权限,因此无法访问任何非静态字段。当您想要一个可以在不实例化类的情况下使用的函数时,它们经常被使用。
    友元函数是指不在类中但您想要让它们访问您类的私有成员的函数。
    而这(静态 vs. 友元)并不是使用其中一个与另一个相反的问题,因为它们不是相互对立的。

    如果在实例化之前使用静态函数,为什么要将其与类关联起来?你能举个例子吗?我正在努力理解这个问题...谢谢! - Swapna
    假设你有一个名为Converter的类,它只是用于转换单位。你可以拥有静态函数,例如Converter.ConvertFromMetersToMiles(),这些函数很合乎逻辑,但你不需要实例化该类。 - synepis
    你的其他函数可以访问你的静态数据,所以你可以使用静态函数来设置一个控制类所有实例行为的静态变量。但是在这种情况下,需要考虑明显的线程安全问题。 - thebretness
    1
    那么,您已经描述了静态函数是一个已从任何特定对象中分离的成员函数,而友元函数是一个非成员函数,其被赋予了对类的访问权限。那它们就是一样的吗?在什么情况下应该使用其中之一? - Joseph Mansfield

    2

    静态函数只能访问一个类的成员。友元函数可以访问多个类,如下面的代码所示:

    class B;
    class A { int a; friend void f(A &a, B &b); };
    class B { int b; friend void f(A &a, B &b); };
    void f(A &a, B &b) { std::cout << a.a << b.b; }
    

    f()可以访问A类和B类的数据。


    2
    • One reason to prefer a friend over static member is when the function needs to be written in assembly (or some other language).

      For instance, we can always have an extern "C" friend function declared in our .cpp file

      class Thread;
      extern "C" int ContextSwitch(Thread & a, Thread & b);
      
      class Thread
      {
      public:
          friend int ContextSwitch(Thread & a, Thread & b);
          static int StContextSwitch(Thread & a, Thread & b);
      };
      

      And later defined in assembly:

                      .global ContextSwitch
      
      ContextSwitch:  // ...
                      retq
      

      Technically speaking, we could use a static member function to do this, but defining it in assembly won't be easy due to name mangling (http://en.wikipedia.org/wiki/Name_mangling)

    • Another situation is when you need to overload operators. Overloading operators can be done only through friends or non-static members. If the first argument of the operator is not an instance of the same class, then non-static member would also not work; friend would be the only option:

      class Matrix
      {
          friend Matrix operator * (double scaleFactor, Matrix & m);
          // We can't use static member or non-static member to do this
      };
      

    2

    标准要求操作符=() []和->必须是成员函数,而类特定的new、new[]、delete和delete[]运算符必须是静态成员。如果出现不需要类对象调用函数的情况,则将函数定义为静态函数。对于所有其他函数:
    如果一个函数需要流I/O的操作符=() []和->,或者它需要左侧参数的类型转换, 或者它可以仅使用类的公共接口实现,那么将其定义为非成员函数(在前两种情况下需要友元函数)
    如果它需要虚拟行为, 则添加一个虚拟成员函数来提供虚拟行为,并基于该函数进行实现
    否则, 将其定义为成员函数。


    1

    静态函数是一种没有访问 this 的函数。

    友元函数是一种可以访问类的私有成员的函数。


    10
    一个静态函数也可以访问类的内部。 - pm100

    1

    如果函数不需要读取或修改类的特定实例的状态(意味着您不需要修改内存中的对象),或者如果您需要使用指向类成员函数的函数指针,则可以使用静态函数。在第二种情况下,如果您需要修改驻留对象的状态,则需要传递this并使用本地副本。在第一种情况下,可能会出现这样的情况,即执行某个任务的逻辑不依赖于对象的状态,但是您的逻辑分组和封装将其作为特定类的成员。

    当您创建的代码不是类的成员且不应该成为类的成员,但具有绕过私有/受保护封装机制的合法目的时,您可以使用友元函数或类。其中一个目的可能是您有两个需要一些共同数据的类,但编写两次逻辑会很糟糕。实际上,我只在我编写的类中的1%左右使用了此功能。它很少需要。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接