什么时候使用'friend'在c++中?

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

使用friend的好例子是什么?


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

183133 次浏览

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

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

首先(依我看)不要听那些说friend没用的人。它是有用的。在许多情况下,您将拥有具有不打算公开可用的数据或功能的对象。对于许多作者可能只是表面上熟悉不同领域的大型代码库尤其如此。

友元说明符也有替代方案,但通常都很麻烦(cppp级别的具体类/掩码类型定义),或者不是万无一错(注释或函数名约定)。

在答案上;

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

您可以通过考虑一个更复杂的类(如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 );
};
树的例子是一个很好的例子: 在一些不同的类中实现一个对象 具有继承关系。

也许你还需要它有一个构造函数protected和force 人们使用你的“朋友”工厂。

... 好吧,坦白地说,没有它你也能生活。

您使用私有/受保护/公共权限控制成员和函数的访问权限? 所以假设这3个层面中的每一个都很清楚,那么很明显我们遗漏了一些东西……< / p >

例如,将成员/函数声明为protected是非常通用的。你是说这个函数对于每一个人是不可触及的(当然,继承的子函数除外)。但是异常呢?每个安全系统都让你有某种类型的“白名单”,对吗?

所以朋友让你拥有磐石般的对象隔离的灵活性,但允许为你认为合理的事情创建一个“漏洞”。

我想人们之所以说不需要它,是因为总有一种设计可以不需要它。我认为这类似于讨论全局变量:你永远不应该使用它们,总有办法不用它们……但在现实中,你会看到在某些情况下,这是(几乎)最优雅的方式……我认为朋友之间也是如此。

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

好吧,这不完全是看待它的方式。 其思想是控制WHO可以访问什么,是否有设置函数与它关系不大。< / p >

典型的例子是重载操作符<<。另一个常见的用法是允许助手或管理类访问您的内部。

下面是我从c++朋友那里听到的一些指导原则。最后一个尤其令人难忘。

  • 你的朋友不是你孩子的朋友。
  • 你孩子的朋友并不是你的朋友。
  • 只有朋友才能碰你的隐私部位。

@roo:这里没有破坏封装,因为类本身规定了谁可以访问它的私有成员。只有当这可能是由类外部引起时,封装才会被破坏,例如,如果你的operator <<声明“我是类foo的朋友”。

friend替换了public,而不是private!

实际上,c++ FAQ 我已经回答了这个问题

可以坚持最严格和最纯粹的OOP原则,并确保任何类的数据成员都没有访问器,这样所有对象必须都是唯一可以知道它们的数据的对象,唯一的方法是通过间接的消息,即方法。

但即使c#也有内部可见性关键字,Java对某些事情有默认的级别的可访问性。c++实际上更接近于OOP的理想,通过指定完全< em > < / em >其他类和只有其他类可以看到类的可见性来最小化类的可见性妥协。

我实际上不使用c++,但如果c#有朋友s,我会用它来代替汇编全局内部修饰符,我实际上使用了很多。它并没有真正打破封装,因为。net 中的部署单元是一个程序集。

但是还有InternalsVisibleToAttribute(otherAssembly),它的作用类似于交叉组装的朋友机制。微软将此用于可视化的设计师程序集。

在做TDD的时候,我经常使用c++中的'friend'关键字。

朋友能知道我的一切吗?


更新:我从Bjarne Stroustrup网站找到了这个关于“朋友”关键字的有价值的答案。

“好友”是一种授予访问权限的显式机制,就像会员资格一样。

在做TDD的时候,我经常使用c++中的'friend'关键字。朋友能知道我的一切吗?

不,这只是一种单向的友谊。

我使用friend的一个特定实例是在创建单例类时。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;
}

关于operator<<运算符>>没有理由让这些运算符成为朋友。它们确实不应该是成员函数,但它们也不需要是朋友函数。

最好的方法是创建公共print(ostream&)和read(istream&)函数。然后,写操作符<<和用这些函数表示的算子>>。这提供了额外的好处,允许您将这些函数设置为虚拟函数,从而提供虚拟序列化。

在构建容器并希望为该类实现迭代器时,Friend可以派上用场。

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

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

在我之前工作过的一家公司里,我们遇到了一个有趣的问题,我们用朋友来产生良好的影响。我在我们的框架部门工作,我们在自定义操作系统上创建了一个基本的引擎级系统。在内部我们有一个类结构:

         Game
/    \
TwoPlayer  SinglePlayer

所有这些类都是框架的一部分,由我们的团队维护。该公司所制作的游戏便是基于这一源自games子游戏的框架。问题是Game有各种单人和双人玩家需要访问的东西的接口,但我们不想在框架类之外公开。解决方案是将这些接口设为私有,并允许双人和单人玩家通过友谊访问它们。

事实上,这整个问题本可以通过更好地实施我们的系统来解决,但我们被锁定在我们所拥有的东西上。

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

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

与其担心这两行是否总是一起执行,并且顺序一致,你可以将方法设为私有,并有一个friend函数来强制一致性:

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

换句话说,您可以保持公共接口更小,并强制在友元函数中跨越类和对象的不变量。

好友对于回调也很有用。可以将回调函数作为静态方法来实现

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

这样就更容易隐藏下游不需要看到的东西。

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();
};

你会在 answer中找到一个不做作的例子。另一个使用它的代码在 answer中。CRTP基类强制转换其this指针,以便能够使用数据成员指针访问派生类的数据字段。

简单的回答是:当它实际上是提高了封装时,使用朋友。提高可读性和可用性(操作符<<>>是典型的例子)也是一个很好的理由。

至于改进封装的例子,专门设计用于处理其他类的内部结构的类(想到了测试类)是很好的候选。

我只使用friend关键字来单元测试受保护的函数。有些人会说不应该测试受保护的功能。然而,在添加新功能时,我发现这个工具非常有用。

然而,我没有直接在类声明中使用关键字in,而是使用一个漂亮的模板-hack来实现这一点:

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。

编辑:多读一些常见问题,我喜欢<<>>操作符重载并添加为这些类的朋友,但我不确定这如何不破坏封装

它将如何破坏封装?

当允许不受限制的访问数据成员时,就打破了封装。考虑以下类:

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。这怎么说得通?(提示:事实并非如此)

< p >底线: 封装是关于能够控制哪些函数可以访问私有成员。是关于这些函数定义的精确位置

另一种用法:朋友(+虚拟继承)可用于避免从类派生(即:"make a class underivable") => 12

2:

 class Fred;


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


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

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

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

很明显…

友函数和类提供对类的私有和受保护成员的直接访问,以避免在一般情况下破坏封装。大多数使用是与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的朋友是一样的。 编译器将优化掉函数调用,因此这将导致与直接访问相同的指令

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

你必须非常小心何时/何处使用friend关键字,而且,像你一样,我很少使用它。下面是一些使用friend和替代方法的注意事项。

假设你想比较两个对象,看它们是否相等。你可以:

  • 使用访问器方法进行比较(检查每个ivar并确定是否相等)。
  • 或者,您可以通过将所有成员设为public直接访问它们。

第一个选项的问题是,这可能会有很多访问器,这比直接变量访问(稍微)慢,更难读取,而且很麻烦。第二种方法的问题是完全破坏了封装。

如果我们可以定义一个外部函数,它仍然可以访问类的私有成员,那就更好了。我们可以使用friend关键字来做到这一点:

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

方法equal(Beer, Beer)现在可以直接访问ab的私有成员(可能是char *brandfloat percentAlcohol等)。这是一个相当做作的例子,你会更早地将friend应用到重载的== operator上,但我们会讲到这一点。

有几件事需要注意:

  • friend不是类的成员函数
  • 它是一个普通函数,具有对类的私有成员的特殊访问
  • 不要将所有的访问器和突变器都替换为友元(你也可以将所有的访问器和突变器都设置为public!)
  • 友谊不是互惠的
  • 友谊是不可传递的
  • 友谊不是遗传的
  • 或者,如c++常见问题解答:“仅仅因为我授予你与我的友谊访问权限,并不会自动授予你的孩子与我的访问权限,并不会自动授予你的朋友与我的访问权限,也不会自动授予我与你的访问权限。”

我只在用另一种方法更难的时候才真正使用friends。另一个例子是,由于Mat2x2Mat3x3Mat4x4Vec2Vec3Vec4等的互操作性,许多向量数学函数通常被创建为friends。而且做朋友比到处使用访问器要容易得多。正如所指出的,friend在应用于<<(非常方便调试)、friends0和friends1操作符时通常是有用的,但也可以用于这样的事情:

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,但偶尔它正是你所需要的。希望这能有所帮助!

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

友元函数的典型用例是 在访问私有或受保护的两个不同的类之间进行

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

我发现了一个使用好友访问的方便地方:私有函数的Unittest。

在c++中,"friend"关键字在操作符重载和桥接中很有用 1)。操作符重载中的朋友关键字:
操作符重载的例子是:假设我们有一个类“Point”,它有两个浮动变量
“x”(用于x坐标)和“y”(用于y坐标)。现在我们必须重载"<<"(提取操作符),这样如果我们调用"cout << pointobj",它将打印x和y坐标(其中pointobj是Point类的对象)。为此,我们有两个选项:
1.重载“ostream”类中的“operator <<()”函数。
2.重载"Point"类中的"operator<<()"函数
第一个选项并不好,因为如果我们需要为一些不同的类再次重载这个操作符,那么我们必须再次在“ostream”类中进行更改。
这就是为什么第二名是最好的选择。现在编译器可以调用
"operator <<()"功能:
< / p >
   1.Using ostream object cout.As: cout.operator<<(Pointobj) (form ostream class).
2.Call without an object.As: operator<<(cout, Pointobj) (from Point class).

Beacause we have implemented overloading in Point class. So to call this function without an object we have to add"friend" keyword because we can call a friend function without an object. Now function declaration will be As:
"friend ostream &operator<<(ostream &cout, Point &pointobj);"

2.) Friend keyword in making bridge :
Suppose we have to make a function in which we have to access private member of two or more classes ( generally termed as "bridge" ) . How to do this:
To access private member of a class it should be member of that class. Now to access private member of other class every class should declare that function as a friend function. For example : Suppose there are two class A and B. A function "funcBridge()" want to access private member of both classes. Then both class should declare "funcBridge()" as:
friend return_type funcBridge(A &a_obj, B & b_obj);

I think this would help to understand friend keyword.

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

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

会所

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只能访问受保护的成员。

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

TLDR TLDR

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

TLDR

数据结构通常由多个类组成。与传统类类似(由编程语言支持),数据结构是一个广义类,它也具有数据和数据上的不变量,这些数据跨越多个类的对象。封装保护这些不变量不受外部数据的意外修改,从而使数据结构的操作(“成员函数”)正确工作。友类将封装从类扩展到广义类。

太长

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

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

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

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

数据结构示例

数据结构的一个典型例子是Edge8,它使用顶点(类Vertex)、边(类Edge)和图(类Graph)的单独对象进行存储。这些类独立起来没有意义。Graph类通过其成员函数(例如graph.addVertex()graph.addEdge(aVertex, bVertex))创建__abc0和__abc1,并返回指向它们的指针(或类似指针)。__abc0和__abc1同样会被它们所属的Graph破坏(例如Edge0和Edge1)。Vertex对象、Edge对象和Graph对象的集合一起编码了一个数学图。在本例中,意图是Vertex/Edge对象不被Graph对象共享(其他设计选择也是可能的)。

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

不变的例子

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

封装的例子

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

在图数据结构的例子中,你可以声明VertexGraph的朋友,并将Vertex的构造函数和数据成员设为私有,这样Vertex只能由Graph创建和修改。特别是,Vertex将有一个私有构造函数,该构造函数接受指向其所属图的指针。这个构造函数在graph.addVertex()中被调用,这是可能的,因为VertexGraph的友元。(但请注意,Graph不是Vertex的朋友:比如,Vertex不需要能够访问Graph的顶点列表。)

术语

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

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