我一直在阅读C ++常见问题解答,并对friend
声明感到好奇。我个人从未使用过它,但是我对探索这种语言很感兴趣。
使用的一个好例子是friend
什么?
再读一遍FAQ,我喜欢<<
>>
运算符重载的想法,并添加为这些类的朋友。但是我不确定这不会破坏封装。这些例外什么时候可以保持在OOP的严格程度之内?
我一直在阅读C ++常见问题解答,并对friend
声明感到好奇。我个人从未使用过它,但是我对探索这种语言很感兴趣。
使用的一个好例子是friend
什么?
再读一遍FAQ,我喜欢<<
>>
运算符重载的想法,并添加为这些类的朋友。但是我不确定这不会破坏封装。这些例外什么时候可以保持在OOP的严格程度之内?
Answers:
首先(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 );
};
friend
增强封装的功能。friend
授予对成员的选择性访问权限,就像这样protected
做一样。任何细粒度的控制都比授予公共访问权要好。其他语言也定义了选择性访问机制,请考虑使用C#internal
。有关使用的大多数负面批评friend
都与紧密耦合有关,这通常被认为是一件坏事。但是,在某些情况下,更紧密的耦合正是您想要的,并friend
为您提供了强大的功能。
friend
是什么,而不是提供一个激励性的例子。Window / WindowManager示例比所示示例要好,但是太模糊了。此答案也没有解决问题的封装部分。
在工作中,我们广泛使用朋友来测试代码。这意味着我们可以为主要应用程序代码提供适当的封装和信息隐藏。但是我们也可以有单独的测试代码,该代码使用朋友检查内部状态和数据以进行测试。
可以说我不会将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基础强制转换了此指针,以便能够使用数据成员指针访问派生类的数据字段。
P<C>
在template<template<typename> class P> class C : P<C> {};
指出“使用类模板C需要模板参数” 时发生错误。您是否遇到过同样的问题,或者知道解决方案?
FlexibleClass
内FlexibleClass
应隐指的是其自己的类型。
@roo:封装在这里没有中断,因为类本身规定了谁可以访问其私有成员。如果封装可能是由班级以外的人引起的,例如,如果您operator <<
宣称“我是班级的朋友” ,则封装将被破坏foo
。
friend
代替使用public
,不使用private
!
实际上,C ++ FAQ 已经回答了这个问题。
friend
也不例外。唯一真正的观察结果是C ++仅在编译时确保封装。而且您无需再说任何话。剩下的就是胡扯。因此,总而言之:FQA的这一部分不值得一提。
典型示例是重载operator <<。另一个常见用途是允许助手或管理员类访问您的内部组件。
这是我听说过的一些有关C ++朋友的准则。最后一个特别令人难忘。
friend
我猜不使用典型。
编辑:再读一遍常见问题,我喜欢<< >>运算符重载并将其添加为这些类的朋友的想法,但是我不确定这不会破坏封装
如何破坏封装?
当您允许对数据成员的无限制访问时,将破坏封装。考虑以下类别:
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。那怎么办呢?(提示:没有)
底线:封装是关于能够控制哪些函数可以访问私有成员。这不是关于恰恰是这些功能的定义的位置。
安德鲁例子的另一个常见版本,可怕的代码对
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);
}
换句话说,可以使公共接口更小,并强制执行跨朋友函数中的类和对象的不变式。
addChild
成员函数也设置父级呢?
setParent
朋友,因为您不想允许客户端更改父对象,因为您将在功能的addChild
/ removeChild
类别中对其进行管理。
您使用私有/受保护/公共权限控制成员和功能的访问权限?因此,假设这3个级别中的每个级别的概念都很明确,那么很显然我们缺少了一些东西...
例如,将成员/函数声明为受保护对象是非常通用的。您是说每个人都无法使用此功能(当然,除了继承的孩子)。但是异常呢?每个安全系统都会让您拥有某种“白名单”,对吗?
因此,friend使您可以灵活地进行坚如磐石的对象隔离,但允许为您认为合理的事物创建“漏洞”。
我猜人们说它是不需要的,因为总有一种设计会没有它。我认为这类似于对全局变量的讨论:永远不要使用它们,总有一种不用它们的方法...但是实际上,您会看到最终以(几乎)最优雅的方式出现的情况。 ..我认为与朋友的情况相同。
除了让您无需设置功能即可访问成员变量外,它实际上并没有任何好处
嗯,这并不是看待它的确切方式。想法是控制WHO可以访问具有或不具有设置功能的功能。
friend
漏洞如何?它允许类中列出的方法访问其私有成员。它仍然不允许任意代码访问它们。因此,它与公共成员职能没有什么不同。
在我以前工作过的一家公司中,我们遇到了一个有趣的问题,当时我们用朋友来体面地对待。我在框架部门工作,我们在自定义OS上创建了基本的引擎级系统。在内部,我们有一个类结构:
Game
/ \
TwoPlayer SinglePlayer
所有这些类都是框架的一部分,并由我们的团队维护。该公司生产的游戏是在此框架的基础上构建的,该框架源自一个Games子公司。问题是Game具有与SinglePlayer和TwoPlayer需要访问的各种事物的接口,但是我们不想在框架类之外公开。解决方案是将这些接口设为私有,并允许TwoPlayer和SinglePlayer通过友谊访问它们。
确实,可以通过更好地实施我们的系统来解决整个问题,但是我们被束之高阁。
简短的答案是:在实际改善封装性时使用friend。提高可读性和可用性(运算符<<和>>是典范示例)也是一个很好的理由。
至于改进封装的示例,专门设计为与其他类的内部一起使用的类(想到的是测试类)是不错的选择。
<<
和>>
通常的朋友,而不是成员,因为使他们的成员将使其难以使用。当然,我说的是那些运营商需要访问私有数据的情况。否则,友谊是无用的。
operator<<
和operator>>
值类的成员,而不是非会员或成员i|ostream
,将不会提供所需的语法,而我不建议它。“ 我说的是那些操作员需要访问私有数据的情况 ”我不太清楚为什么输入/输出操作员需要访问私有成员。
C ++的创建者说这并没有打破任何封装原则,我将引用他的话:
“朋友”会违反封装吗? 不,不是的。就像成员身份一样,“朋友”是授予访问权限的明确机制。您不能(在符合标准的程序中)在不修改类源的情况下授予自己访问类的权限。
十分清楚...
为了进行TDD多次,我在C ++中使用了'friend'关键字。
朋友可以了解我的一切吗?
更新:我从Bjarne Stroustrup网站上找到了有关“ friend”关键字的有价值的答案。
就像成员身份一样,“朋友”是授予访问权限的明确机制。
您必须在何时何地使用该friend
关键字时非常小心,并且像您一样,我很少使用它。以下是一些使用注意事项friend
和替代方法。
假设您要比较两个对象以查看它们是否相等。您可以:
第一种选择的问题在于,这可能是很多访问器,它比直接变量访问(略)慢,读起来又麻烦。第二种方法的问题是您完全破坏了封装。
最好的是,如果我们可以定义一个外部函数,该函数仍可以访问类的私有成员。我们可以使用friend
关键字:
class Beer {
public:
friend bool equal(Beer a, Beer b);
private:
// ...
};
equal(Beer, Beer)
现在,该方法可以直接访问a
和b
的私有成员(可能是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 >>,没有充分的理由结识这些运算符。的确,它们不应成为成员函数,但也不必成为朋友。
最好的办法是创建公共打印(ostream&)和读取(istream&)函数。然后,根据这些函数编写operator <<和operator >>。这提供了使您能够将这些功能虚拟化的附加好处,从而提供了虚拟序列化。
我仅使用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。
在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);
我认为这将有助于了解朋友关键字。
我使用的一个特定实例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
它不是需要一个特定的“正当理由”,增加一个成员函数时没有。
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的朋友一样。编译器将优化掉函数调用,因此结果相同指示为直接访问。
我认为,使用“朋友”只是带来好处的捷径,但是却有一定的成本。
这可能不是实际的用例情况,但可能有助于说明类之间使用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,这是一条双向通行的道路,其他会员级别受到限制。
在为类实现树算法时,教授给我们的框架代码将树类作为节点类的朋友。
除了让您无需设置功能即可访问成员变量外,它实际上并没有任何好处。
当不同的类(不从另一个类继承)使用另一个类的私有或受保护成员时,可以使用友谊。
朋友功能的典型用例是在两个不同类之间进行的操作,这些类访问二者的私有或受保护成员。
来自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;
}
您可以遵守最严格和最纯正的OOP原则,并确保任何类的数据成员都没有访问器,因此,所有对象必须是唯一可以了解其数据的对象,唯一的处理方式是通过间接消息,即方法。
但是,即使C#都有内部可见性关键字,Java 在某些方面也具有其默认的程序包级别可访问性。实际上,C ++通过准确指定一个其他类以及仅其他类可以看到的类,从而最小化了对类的可见性的折衷,从而更接近于OOP的理想状态。
我并没有真正使用C ++,但是如果C#有好朋友,我会使用它,而不是我实际上使用很多的assembly-global 内部修饰符。它并没有真正破坏封装,因为.NET中的部署单元是程序集。
但是还有一个InternalsVisibleTo Attribute(otherAssembly),它就像一个跨程序集友机制。Microsoft将其用于可视设计器程序集。
朋友对于回调也很有用。您可以将回调实现为静态方法
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;
}
隐藏下游不需要的东西变得更加容易。