我一直在阅读c++常见问题,对朋友声明很好奇。我个人从未使用过它,但我对探索这门语言很感兴趣。

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


读了一些常见问题,我喜欢<< >>操作符重载和添加作为这些类的朋友的想法。然而,我不确定这如何不会破坏封装。什么时候这些异常可以保持在OOP的严格范围内?


当前回答

这可能不是一个实际的用例情况,但可能有助于说明类间朋友关系的使用。

会所

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{};

如果你看看这些类之间的关系;会所拥有各种不同类型的会员资格和会员资格。成员都派生自超类或基类,因为它们都共享公共的ID和枚举类型,外部类可以通过基类中的访问函数访问它们的ID和类型。

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

所以对于vip会员和ClubHouse,这是一个双向通道,而其他会员职业是有限的。

其他回答

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

可以说,我不会将friend关键字作为设计的重要组成部分。

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

“朋友”是否违反封装? 不。但事实并非如此。“好友”是一种授予访问权限的显式机制,就像会员资格一样。你不能(在符合标准的程序中)在不修改类源的情况下授予自己对类的访问权。

很明显…

看来我迟到了14年。但情况是这样的。

TLDR TLDR

有了友类,您就可以将封装扩展到组成数据结构的类组。

TLDR

Your data structure in general consists of multiple classes. Similarly to a traditional class (supported by your programming language), your data structure is a generalized class which also has data and invariants on that data which spans across objects of multiple classes. Encapsulation protects those invariants against accidental modification of the data from the outside, so that the data-structure's operations ("member functions") work correctly. Friend classes extend encapsulation from classes to your generalized class.

太长

类是一种数据类型,它带有指定数据类型值子集(称为有效状态)的不变量。对象是类的有效状态。类的成员函数将给定对象从有效状态移动到另一个有效状态。

对象数据不能从类成员函数外部修改,这一点很重要,因为这可能会破坏类不变量(即将对象移动到无效状态)。封装禁止从类外部访问对象数据。这是编程语言的一个重要安全特性,因为它使无意中破坏类不变量变得很困难。

类通常是实现数据结构的自然选择,因为数据结构的属性(例如性能)依赖于其数据上的不变量(例如红黑树不变量)。然而,有时单个类不足以描述一个数据结构。

数据结构是将数据从有效状态移动到另一有效状态的数据、不变量和函数的任何集合。这是一个类的泛化。细微的区别在于,数据可能分散在不同的数据类型上,而不是集中在单一的数据类型上。

数据结构示例

A prototypical example of a data structure is a graph which is stored using separate objects for vertices (class Vertex), edges (class Edge), and the graph (class Graph). These classes do not make sense independently. The Graph class creates Vertexs and Edges by its member functions (e.g. graph.addVertex() and graph.addEdge(aVertex, bVertex)) and returns pointers (or similar) to them. Vertexs and Edges are similarly destroyed by their owning Graph (e.g. graph.removeVertex(vertex) and graph.removeEdge(edge)). The collection of Vertex objects, Edge objects and the Graph object together encode a mathematical graph. In this example the intention is that Vertex/Edge objects are not shared between Graph objects (other design choices are also possible).

一个Graph对象可以存储所有顶点和边的列表,而每个顶点可以存储一个指向其所属Graph的指针。因此,Graph对象表示整个数学图,只要需要数学图,就可以传递它。

不变的例子

图数据结构的一个不变量是顶点被列在它的所有者图的列表中。这个不变量跨越了Vertex对象和Graph对象。多个类型的多个对象可以参与给定的不变量。

封装的例子

与类类似,数据结构得益于封装,它可以防止数据被意外修改。这是因为数据结构需要保留不变量,以便能够以承诺的方式运行,就像类一样。

In the graph data structure example, you would state that Vertex is a friend of Graph, and also make the constructors and data-members of Vertex private so that a Vertex can only be created and modified by Graph. In particular, Vertex would have a private constructor which accepts a pointer to its owning graph. This constructor is called in graph.addVertex(), which is possible because Vertex is a friend of Graph. (But note that Graph is not a friend of Vertex: there is no need for Vertex to be able to access Graph's vertex-list, say.)

术语

数据结构的定义本身就像一个类。我建议我们开始使用术语“广义类”来描述将数据从有效状态移动到另一有效状态的任何数据集、不变量和函数。c++类是广义类的一种特殊类型。不言而喻,友类是将封装从c++类扩展到广义类的精确机制。

(事实上,我希望用“广义类”的概念取代“类”,并使用“本机类”来表示编程语言支持的类的特殊情况。然后,在教授类时,您将学习本机类和这些广义类。但这可能会令人困惑。)

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

它实际上没有任何好处,除了让你在不使用设置函数的情况下访问成员变量。

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

Point p;
cout << p;

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

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

然而,这里有明显的封装含义。首先,现在友类或函数可以完全访问类的所有成员,甚至不属于它的需要。其次,类和友元的实现现在交织在一起,以至于类中的内部更改可以破坏友元。

如果您将好友视为类的扩展,那么从逻辑上讲,这就不是问题。但是,在这种情况下,为什么有必要首先把朋友挖出来呢?

在不破坏封装的情况下,要实现“friends”所宣称的相同目标,可以这样做:

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的朋友是一样的。 编译器将优化掉函数调用,因此这将导致与直接访问相同的指令。

我认为使用“朋友”是一个简单的捷径,有争议的好处,但一定的成本。