C++ friend友元函数和友元类用法详解(附带实例)
在 C++ 中,封装不仅关乎如何隐藏数据和实现细节,还包括如何在保持封装性的同时,允许某些外部函数或类访问类的私有或受保护成员。这就引入了友元函数(friend functions)和友元类(friend classes)的概念,它们提供了一种特殊的访问权限,是 C++ 中封装机制的一个重要补充。
友元函数是定义在类外部,但有权访问类的所有私有和受保护成员的函数。它不是类的成员函数,但需要在类的定义中明确声明其为友元(通过 friend 关键字)。
友元函数的主要用途是允许两个或多个类共享对彼此私有数据的访问,或者实现一些需要访问对象内部数据但又不适合作为类成员的函数,如某些运算符重载函数。
友元类的所有成员函数都能够访问另一个类的私有和受保护成员,类似于友元函数,通过在类定义中声明另一个类为友元类来实现。
友元类的使用场景包括实现高度协作的类之间的紧密交互(如设计模式中的访问者模式),或者在类库的内部实现中,需要深入访问另一类的内部数据和功能。
了解友元函数与友元类的基本概念后,让我们深入探讨为什么需要友元,以及如何在保持类封装性的同时使用友元提供必要的访问权限。
这时,我们就需要一个机制,可以精确地控制哪些外部函数或类可以访问当前类的私有成员,而不是完全公开这些成员。友元关系正是为了满足这种需求而设计的。
在日常交往中,虽然我们可能不会轻易地与所有人分享我们的秘密,但对于某些特定的朋友,我们可能会毫无保留地分享。这与 C++ 中的友元关系非常相似,我们不会轻易地公开类的私有成员,但对于某些特定的函数或类,我们可能会选择性地公开。
友元关系具有如下特点:
【实例】在一个小社区中,有一个被大家信任的邮递员,他被允许使用每家的私人信箱来投递邮件。在这里,每户人家的信箱就是类的私有成员,而邮递员则类似于全局友元函数——他不属于任何一个家庭(类),但却有权访问他们的信箱(私有成员)。
通过这种方式,全局友元函数提供了一种灵活的方法来访问类的私有数据,同时也保持了类的封装性。这种机制在设计需要广泛访问但又不适合直接成为类成员的功能时非常有用。
【实例】有一个园艺师,他被一个花园的主人信任,允许他特别照顾某些稀有的植物。在这里,花园代表一个类,稀有植物代表私有成员,而园艺师就是友元成员函数——虽然他不属于花园的“家庭”(类),但由于他有特殊的权限,因此能够照料这些特别的植物。
在 main() 函数中,我们创建了 Garden 和 Gardener 的对象,然后通过 Gardener 对象调用 careForPlant() 方法,间接地照顾了 Garden 的稀有植物。这个过程展示了友元成员函数如何突破类的封装界限,以实现特定的功能需求。
【实例】在一个私人图书馆中存放着珍贵的研究资料,图书管理员因其职责和信任而被允许完全访问所有藏书。在这里,图书馆类似于一个类,藏书代表私有成员,而图书管理员则相当于一个友元类——他们不是图书馆的“部分”,但由于特殊的许可,可以访问所有的藏书。
在 main() 函数中,我们创建了 Library 和 Librarian 的对象,并通过 Librarian 对象调用 revealSecret() 方法,成功访问了 Library 的私有藏书。
尽管友元关系在某些情况下非常有用,例如实现紧密协作的类或操作符重载,但它们的使用应谨慎,以避免破坏封装性和增加代码耦合度。
虽然可以通过减少不必要的公共接口来简化类的设计,但过度依赖可能导致破坏封装性和增加类间耦合度。
友元成员函数提供了比全局友元函数更细粒度的控制,能够精确定义哪些成员函数可以访问其他类的内部状态。然而,它可能导致类之间的高耦合,特别是当涉及多个类时。
虽然友元类提供了广泛的数据访问权限,便于执行复杂的操作,但同时也带来了最大程度的封装性破坏和耦合性增加。
下表直观地展示了不同类型的友元关系(全局友元函数、友元成员函数和友元类)的使用场景、封装性破坏程度以及耦合度等。这样的比较可以帮助读者更加清晰地理解每种友元关系的适用条件和潜在影响。
通过以上表格,读者可以根据自己的具体需求和设计目标,在全局友元函数、友元成员函数和友元类之间做出更加明智的选择。每种友元关系都有其独特的优点和局限性,适当的使用可以显著提高代码的灵活性和表现力,但必须谨慎处理,以保持代码的封装性和降低不必要的耦合。
友元函数是定义在类外部,但有权访问类的所有私有和受保护成员的函数。它不是类的成员函数,但需要在类的定义中明确声明其为友元(通过 friend 关键字)。
友元函数的主要用途是允许两个或多个类共享对彼此私有数据的访问,或者实现一些需要访问对象内部数据但又不适合作为类成员的函数,如某些运算符重载函数。
友元类的所有成员函数都能够访问另一个类的私有和受保护成员,类似于友元函数,通过在类定义中声明另一个类为友元类来实现。
友元类的使用场景包括实现高度协作的类之间的紧密交互(如设计模式中的访问者模式),或者在类库的内部实现中,需要深入访问另一类的内部数据和功能。
了解友元函数与友元类的基本概念后,让我们深入探讨为什么需要友元,以及如何在保持类封装性的同时使用友元提供必要的访问权限。
为什么需要友元
当我们编写代码时,经常会遇到需要让某些特定的外部函数或类访问当前类的私有成员的情况。这种需求可能是由于设计决策、性能优化或其他特殊原因引起的。但是,我们并不希望这些私有成员被任意的外部函数或类访问,因为这会破坏封装性并可能导致数据不一致或其他潜在问题。这时,我们就需要一个机制,可以精确地控制哪些外部函数或类可以访问当前类的私有成员,而不是完全公开这些成员。友元关系正是为了满足这种需求而设计的。
在日常交往中,虽然我们可能不会轻易地与所有人分享我们的秘密,但对于某些特定的朋友,我们可能会毫无保留地分享。这与 C++ 中的友元关系非常相似,我们不会轻易地公开类的私有成员,但对于某些特定的函数或类,我们可能会选择性地公开。
友元关系具有如下特点:
- 非对称性:如果类 A 声明类 B 为其友元,则类 B 可以访问类 A 的私有成员,但类 A 不能访问类 B 的私有成员,除非类 B 也声明类 A 为其友元。
- 非传递性:如果类 A 是类 B 的友元,类 B 是类 C 的友元,则类 A 不自动成为类 C 的友元。
- 限定作用域:友元声明只在给定的类中有效,必须在每个类中显式声明。
C++全局友元函数
在 C++ 中,全局友元函数允许在不是类成员的情况下访问类的私有和受保护成员。这一特性非常适合用于实现那些需要访问类内部数据但又不适合作为类成员的功能。【实例】在一个小社区中,有一个被大家信任的邮递员,他被允许使用每家的私人信箱来投递邮件。在这里,每户人家的信箱就是类的私有成员,而邮递员则类似于全局友元函数——他不属于任何一个家庭(类),但却有权访问他们的信箱(私有成员)。
#include <iostream> #include <string> using namespace std; // 定义一个类,代表一个家庭,它有一个私有信箱 class Family { private: string mailbox = "You have a new letter!"; // 声明邮递员为友元,赋予其访问信箱的权限 friend void Postman(Family&); public: Family() {} }; // 全局友元函数,邮递员可以访问家庭的私有信箱 void Postman(Family &f) { cout << "Postman checks the mailbox: " << f.mailbox << endl; } int main() { Family family; Postman(family); // 邮递员访问家庭的信箱 return 0; }运行结果为:
Postman checks the mailbox: You have a new letter!
在这个示例中,Family 类有一个私有成员 mailbox,这是家庭的私人信箱。我们声明了一个全局函数 Postman() 为 Family 的友元,这意味着 Postman() 函数可以访问 Family 的所有成员,包括私有成员。在 main 函数中,我们创建了 Family 类的一个对象 family,并通过 Postman() 函数查看了信箱的内容。通过这种方式,全局友元函数提供了一种灵活的方法来访问类的私有数据,同时也保持了类的封装性。这种机制在设计需要广泛访问但又不适合直接成为类成员的功能时非常有用。
C++友元成员函数
友元成员函数允许一个类的成员函数访问另一个类的私有或受保护成员。这种设计可以用于特定情况——两个类需要紧密合作,但又希望保持封装性。【实例】有一个园艺师,他被一个花园的主人信任,允许他特别照顾某些稀有的植物。在这里,花园代表一个类,稀有植物代表私有成员,而园艺师就是友元成员函数——虽然他不属于花园的“家庭”(类),但由于他有特殊的权限,因此能够照料这些特别的植物。
#include <iostream> // 前向声明 class Garden; // 园艺师类 class Gardener { public: void careForPlant(const Garden& g); }; // 花园类,拥有一些稀有植物 class Garden { private: void rarePlant() const { std::cout << "Watering a rare plant." << std::endl; } // 声明Gardener的特定成员函数为友元 friend void Gardener::careForPlant(const Garden&); }; // Gardener 类成员函数定义 void Gardener::careForPlant(const Garden& g) { std::cout << "Gardener is caring for a plant." << std::endl; g.rarePlant(); // 访问并照顾花园的稀有植物 } int main() { Garden garden; Gardener gardener; gardener.careForPlant(garden); // 园艺师照顾花园的稀有植物 return 0; }运行结果为:
Gardener is caring for a plant.
Watering a rare plant.
在 main() 函数中,我们创建了 Garden 和 Gardener 的对象,然后通过 Gardener 对象调用 careForPlant() 方法,间接地照顾了 Garden 的稀有植物。这个过程展示了友元成员函数如何突破类的封装界限,以实现特定的功能需求。
C++友元类
在 C++ 中,友元类允许一个类完全访问另一个类的私有或受保护成员。这种关系特别适用于两个类需要共享数据或功能但又要保持其他封装特性的场景。【实例】在一个私人图书馆中存放着珍贵的研究资料,图书管理员因其职责和信任而被允许完全访问所有藏书。在这里,图书馆类似于一个类,藏书代表私有成员,而图书管理员则相当于一个友元类——他们不是图书馆的“部分”,但由于特殊的许可,可以访问所有的藏书。
#include <iostream> #include <string> using namespace std; // 声明图书管理员类,以便图书馆类能提前知道其存在 class Librarian; // 图书馆类,拥有珍贵的藏书 class Library { private: string secretDocument = "Original copy of rare manuscript"; // 声明图书管理员为友元类,允许它访问所有私有成员 friend class Librarian; public: Library() {} }; // 图书管理员类,能够访问图书馆的藏书 class Librarian { public: void revealSecret(const Library& lib) { cout << "Librarian accesses the library's secret: " << lib.secretDocument << endl; } }; int main() { Library library; Librarian librarian; librarian.revealSecret(library); // 图书管理员访问了图书馆的珍贵藏书 return 0; }运行结果为:
Librarian accesses the library's secret: Original copy of rare manuscript
在这个示例中,Library 类有一个私有成员 secretDocument,代表图书馆的珍贵资料。我们允许 Librarian 类访问这些资料,但不希望这种权限被其他人获取。因此,在 Library 类中将 Librarian 声明为友元类。这样,Librarian 类的任何成员函数都可以访问 Library 的所有成员,包括私有成员。在 main() 函数中,我们创建了 Library 和 Librarian 的对象,并通过 Librarian 对象调用 revealSecret() 方法,成功访问了 Library 的私有藏书。
C++友元关系的使用场景
在 C++ 中,友元关系提供了一种特殊的访问权限,使得在类外部定义的函数或其他类可以访问该类的私有和受保护成员。尽管友元关系在某些情况下非常有用,例如实现紧密协作的类或操作符重载,但它们的使用应谨慎,以避免破坏封装性和增加代码耦合度。
1) 全局友元函数
适用于需要跨多个类访问私有成员的函数,特别是在逻辑上不属于任何一个类的场合,如操作符(或运算符)重载。虽然可以通过减少不必要的公共接口来简化类的设计,但过度依赖可能导致破坏封装性和增加类间耦合度。
2) 友元成员函数
当两个或更多的类需要共享数据或功能,并且这些功能严格限制在几个特定的成员函数中时,使用友元成员函数最为合适。友元成员函数提供了比全局友元函数更细粒度的控制,能够精确定义哪些成员函数可以访问其他类的内部状态。然而,它可能导致类之间的高耦合,特别是当涉及多个类时。
3) 友元类
当一个类需要完全访问另一个类的所有私有和受保护成员,并且双方的合作对于执行某些任务至关重要时,使用友元类是合理的。虽然友元类提供了广泛的数据访问权限,便于执行复杂的操作,但同时也带来了最大程度的封装性破坏和耦合性增加。
4) 使用建议
- 理解友元的真正意图:确保使用友元关系是为了提高代码的功能性和效率,而非简单地绕过封装性;
- 限制友元的范围:仅在绝对必要时,为那些真正需要访问类内部数据的函数或类声明友元关系,避免不必要的耦合;
- 避免链式友元关系:尽量不创建复杂的友元关系网,每个友元声明都应有明确的目的和合理的边界;
- 优先使用友元函数而不是友元类:如果仅有个别函数需要访问某些私有数据,考虑仅将这些函数声明为友元,而不是整个类。
下表直观地展示了不同类型的友元关系(全局友元函数、友元成员函数和友元类)的使用场景、封装性破坏程度以及耦合度等。这样的比较可以帮助读者更加清晰地理解每种友元关系的适用条件和潜在影响。
特性 | 全局友元函数 | 友元成员函数 | 友元类 |
---|---|---|---|
访问粒度 | 函数级别 | 成员函数级别 | 类级别 |
使用场景 | 需要访问多个类的私有成员,且逻辑上不属于任何一个类 | 两个类之间存在密切的协作关系,需要访问对方的私有成员 | 一个类需要完全访问另一个类的私有和受保护成员 |
封装性破坏程度 | 中等,只破坏了特定数据的封装性 | 中等,只有特定的成员函数可以访问另一个类的内部状态 | 高,允许另一个类的所有成员访问私有和受保护成员 |
耦合度 | 中到高,增加了不同类之间的耦合 | 高,增加了成员函数之间的耦合 | 高,两个类之间的耦合度最高 |
推荐使用条件 | 适用于操作符重载或不自然归属于任何类的逻辑 | 当两个类密切相关,并且仅需要单向访问时推荐使用 | 当两个类需要紧密合作,并且双向访问私有成员时推荐使用 |
权衡考虑 | 是否有可能通过成员函数或者类设计来避免使用全局友元函数 | 是否能通过公开接口或设计模式减少对友元成员函数的需求 | 是否能通过重新设计类的职责和接口来避免使用友元类,或者是否可以接受高耦合度 |
通过以上表格,读者可以根据自己的具体需求和设计目标,在全局友元函数、友元成员函数和友元类之间做出更加明智的选择。每种友元关系都有其独特的优点和局限性,适当的使用可以显著提高代码的灵活性和表现力,但必须谨慎处理,以保持代码的封装性和降低不必要的耦合。