我一直在阅读C++ FAQ并对声明感到好奇friend
。我个人从未使用过它,但是我对探索这种语言很感兴趣。
什么是使用的好例子friend
?
再阅读一下FAQ,我喜欢<<
>>
运算符重载并添加为这些类的朋友的想法。但是我不确定这如何不破坏封装。这些异常何时可以保持在 OOP 的严格性范围内?
首先(IMO)不要听那些说没用的人friend
。它是有益的。在许多情况下,您将拥有具有不打算公开的数据或功能的对象。对于具有许多作者的大型代码库尤其如此,这些作者可能只是对不同领域的表面熟悉。
朋友说明符有替代品,但它们通常很麻烦(cpp 级别的具体类/掩码类型定义)或不是万无一失的(注释或函数名称约定)。
回答问题;
说明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 );
};
在工作中,我们广泛使用朋友来测试代码。这意味着我们可以为主要应用程序代码提供适当的封装和信息隐藏。但我们也可以有单独的测试代码,使用朋友来检查内部状态和数据以进行测试。
我只想说我不会使用friend关键字作为您设计的重要组成部分。
该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
}
有时,您会发现策略需要访问派生类:
// 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 基转换其 this 指针,以便能够使用数据成员指针访问派生类的数据字段。
典型的例子是重载运算符<<。另一个常见用途是允许助手或管理员类访问您的内部。
以下是我听到的关于 C++ 朋友的一些指导原则。最后一个特别令人难忘。
编辑:阅读 faq 有点长我喜欢 << >> 运算符重载并添加为这些类的朋友的想法,但是我不确定这如何不破坏封装
它将如何破坏封装?
当您允许不受限制地访问数据成员时,您会破坏封装。考虑以下类:
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。这有什么意义呢?(提示:没有)
底线:封装是关于能够控制哪些函数可以访问私有成员。这与这些函数的定义的确切位置无关。
Andrew 示例的另一个常见版本,可怕的代码对联
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);
}
换句话说,您可以使公共接口更小,并强制执行跨越友元函数中的类和对象的不变量。
您使用 Private/Protected/Public 权限控制成员和功能的访问权限?因此,假设这 3 个级别中的每一个级别的概念都很清楚,那么我们应该清楚地知道我们遗漏了一些东西......
例如,受保护的成员/函数的声明非常通用。您是说每个人都无法使用此功能(当然,继承的孩子除外)。但是例外呢?每个安全系统都允许您拥有某种类型的“白名单”,对吗?
所以朋友让您拥有坚如磐石的对象隔离的灵活性,但允许为您认为合理的事情创建“漏洞”。
我猜人们说它是不需要的,因为总有一种设计不需要它。我认为这类似于对全局变量的讨论:你永远不应该使用它们,总有办法不用它们......但实际上,你会看到最终成为(几乎)最优雅的方式的情况。 ..我认为朋友的情况也是如此。
除了让您在不使用设置函数的情况下访问成员变量之外,它并没有任何好处
好吧,这不完全是看待它的方式。这个想法是控制 WHO 可以访问什么,有或没有设置功能与它无关。
我找到了方便使用好友访问的地方:私有函数的单元测试。
当你正在构建一个容器并且你想为那个类实现一个迭代器时,Friend 会派上用场。
C++ 的创建者说这并没有违反任何封装原则,我将引用他的话:
“朋友”是否违反封装? 不,不是的。“朋友”是一种授予访问权限的显式机制,就像成员资格一样。您不能(在符合标准的程序中)在不修改其源代码的情况下授予自己对类的访问权限。
再清楚不过了...
在我以前工作过的一家公司里,我们遇到了一个有趣的问题,在那里我们使用朋友来产生体面的影响。我在我们的框架部门工作,我们在自定义操作系统上创建了一个基本的引擎级系统。在内部,我们有一个类结构:
Game
/ \
TwoPlayer SinglePlayer
所有这些类都是框架的一部分,由我们的团队维护。该公司制作的游戏是建立在该框架之上的,该框架源自一个 Games 孩子。问题是 Game 具有 SinglePlayer 和 TwoPlayer 需要访问但我们不想暴露在框架类之外的各种事物的接口。解决方案是将这些接口设为私有,并允许 TwoPlayer 和 SinglePlayer 通过友谊访问它们。
说实话,这整个问题可以通过更好地实施我们的系统来解决,但我们被锁定在我们所拥有的东西上。
简短的回答是:在真正改进封装时使用friend 。提高可读性和可用性(运算符 << 和 >> 是典型示例)也是一个很好的理由。
至于改进封装的示例,专门设计用于与其他类的内部一起工作的类(想到测试类)是很好的候选者。
为了多次执行 TDD,我在 C++ 中使用了“朋友”关键字。
朋友能知道我的一切吗?
更新:我从Bjarne Stroustrup 网站找到了这个关于“朋友”关键字的有价值的答案。
“朋友”是一种授予访问权限的显式机制,就像成员资格一样。
您必须非常小心何时/何地使用friend
关键字,并且像您一样,我很少使用它。以下是有关使用friend
和替代方法的一些说明。
假设您想比较两个对象以查看它们是否相等。您可以:
第一个选项的问题在于,这可能是很多访问器,这比直接变量访问(稍微)慢,更难阅读,而且很麻烦。第二种方法的问题是你完全打破了封装。
如果我们可以定义一个仍然可以访问类的私有成员的外部函数,那就太好了。我们可以使用friend
关键字来做到这一点:
class Beer {
public:
friend bool equal(Beer a, Beer b);
private:
// ...
};
该方法equal(Beer, Beer)
现在可以直接访问a
andb
的私有成员(可能是char *brand
,float percentAlcohol
等。这是一个相当人为的示例,您会尽快应用friend
到重载的== operator
,但我们会做到这一点。
需要注意的几点:
friend
不是类的成员函数public
!)friends
我只在以另一种方式更难做到的时候才真正使用。再举一个例子,许多向量数学函数通常是friends
由于Mat2x2
, Mat3x3
, Mat4x4
, Vec2
, Vec3
,Vec4
等的互操作性而创建的。而且成为朋友要容易得多,而不必到处使用访问器。正如所指出的,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
经常使用,但时不时地它正是你所需要的。希望这可以帮助!
关于 operator<< 和 operator>> 没有充分的理由让这些运算符成为朋友。的确,它们不应该是成员函数,但它们也不需要成为朋友。
最好的办法是创建公共 print(ostream&) 和 read(istream&) 函数。然后,根据这些函数编写 operator<< 和 operator>>。这带来了额外的好处,即允许您将这些函数设为虚拟,从而提供虚拟序列化。
我只使用friend-keyword 来对受保护的函数进行单元测试。有些人会说你不应该测试受保护的功能。但是,在添加新功能时,我发现这个非常有用的工具。
但是,我不直接在类声明中使用关键字 in ,而是使用漂亮的模板破解来实现这一点:
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。
在 C++ 中,“friend”关键字在运算符重载和桥接中很有用。
1.) 运算符重载中的 Friend 关键字:运算符重载
的示例是:假设我们有一个类“Point”,它有两个浮点变量
“x”(用于 x 坐标)和“y”(用于 y 坐标)。现在我们必须重载"<<"
(提取运算符),这样如果我们调用"cout << pointobj"
then 它将打印 x 和 y 坐标(其中 pointobj 是 Point 类的对象)。为此,我们有两个选择:
1.在“ostream”类中重载“operator <<()”函数。 2.在“Point”类中重载“operator<<()”函数。现在第一个选项不好,因为如果我们需要为某些不同的类再次重载此运算符,那么我们必须再次在“ostream”类中进行更改。
"operator <<()"
函数:1.使用ostream对象cout.As:cout.operator<<(Pointobj)(形成ostream类)。
2.Call without an object.As: operator<<(cout, Pointobj) (来自 Point 类)。
因为我们已经在 Point 类中实现了重载。所以要在没有对象的情况下调用这个函数,我们必须添加"friend"
关键字,因为我们可以在没有对象的情况下调用友元函数。现在函数声明将是:
"friend ostream &operator<<(ostream &cout, Point &pointobj);"
2.) 制作桥接器中的 Friend 关键字:
假设我们必须创建一个函数,其中我们必须访问两个或多个类的私有成员(通常称为“桥接器”)。如何做到这一点:
要访问一个类的私有成员,它应该是该类的成员。现在要访问其他类的私有成员,每个类都应该将该函数声明为友元函数。例如:假设有两个类 A 和 B。一个函数"funcBridge()"
想要访问这两个类的私有成员。然后两个类都应该声明"funcBridge()"
为:
friend return_type funcBridge(A &a_obj, B & b_obj);
我认为这将有助于理解朋友关键字。
树示例是一个很好的示例:在几个不同的类中实现一个对象,而没有继承关系。
也许您还需要它来保护构造函数并强制人们使用您的“朋友”工厂。
...好吧,坦率地说,你可以没有它。
为了多次执行 TDD,我在 C++ 中使用了“朋友”关键字。
朋友能知道我的一切吗?
不,它只是一种单向的友谊:`(
我使用的一个特定实例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;
}
友元函数和类提供对类的私有和受保护成员的直接访问,以避免在一般情况下破坏封装。大多数用法与 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 的朋友一样。编译器将优化掉函数调用,所以这将导致相同的结果指令作为直接访问。
我认为使用“朋友”只是一条捷径,它的好处有争议,但成本是肯定的。
这可能不是一个实际的用例情况,但可能有助于说明类之间朋友的使用。
会所
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 和类型。
然而,通过 Member 及其派生类的这种层次结构以及它们与 ClubHouse 类的关系,唯一具有“特权”的派生类是 VIPMember 类。基类和其他 2 个派生类无法访问 ClubHouse 的 joinVIPEvent() 方法,但 VIP Member 类拥有该特权,就好像它可以完全访问该事件一样。
因此,VIPMember 和 ClubHouse 是一条双向通道,其他会员等级受到限制。
在为类实现树算法时,教授给我们的框架代码将树类作为节点类的朋友。
除了让您在不使用设置函数的情况下访问成员变量之外,它实际上并没有任何好处。
您可以遵守最严格和最纯粹的 OOP 原则,并确保任何类的数据成员都没有访问器,因此所有对象都必须是唯一可以了解其数据的对象,而对其采取行动的唯一方法是通过间接消息,即方法。
但即使是 C# 也有一个内部可见性关键字,而 Java对于某些事情有其默认的包级别可访问性。C++ 实际上更接近于 OOP 理想,通过精确指定哪个其他类和只有其他类可以看到它,从而最大限度地减少对类的可见性的妥协。
我并没有真正使用 C++,但如果 C# 有friend s,我会使用它而不是程序集全局内部修饰符,我实际上使用了很多。它并没有真正破坏封装,因为 .NET 中的部署单元是一个程序集。
但是还有InternalsVisibleTo Attribute(otherAssembly) ,它就像一个跨程序集友元机制。Microsoft 将此用于可视化设计器程序集。
当不同的类(不是从另一个继承一个)使用另一个类的私有或受保护成员时,您可以使用友谊。
友元函数的典型用例是在访问两者的私有或受保护成员的两个不同类之间执行的操作。
来自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;
}
可能我从上面的答案中遗漏了一些东西,但封装中的另一个重要概念是隐藏实现。减少对私有数据成员(类的实现细节)的访问允许以后更容易修改代码。如果朋友直接访问私有数据,对实现数据字段(私有数据)的任何更改都会破坏访问该数据的代码。使用访问方法主要消除了这一点。我认为相当重要。
朋友对于回调也很有用。您可以将回调实现为静态方法
class MyFoo
{
private:
static void callback(void * data, void * clientData);
void localCallback();
...
};
where 在内部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;
}
隐藏下游不需要以这种方式看到的东西变得更容易。