聚合
本节阅读量:在上一课中,我们注意到对象组合是从简单对象创建复杂对象的过程。我们还讨论了一种类型的对象组合,称为组合。在组合关系中,整个对象负责部分的存在。
在本课中,将研究对象组合的另一个子类型,称为聚合。
聚合
若要符合聚合的条件,整个对象及其部分必须具有以下关系:
- 部件(成员)是对象(类)的一部分
- 部件(成员)可以同时属于多个对象(类)
- 部件(成员)的存在不由对象(类)管理
- 部件(成员)不知道对象(类)的存在
与组合不同,在聚合中,部件(成员)可以同时属于多个对象(类),并且部件(成员)的存在不由整体对象(类)管理。创建聚合时,聚合不负责创建部件。当聚合被销毁时,聚合不负责销毁部件。
例如,考虑一个人与其家庭住址之间的关系。在这个例子中,为了简单起见,我们将说每个人都有一个地址。然而,这个地址一次可以属于多个人:例如,你和你的室友。然而,该地址不是由该人管理的——该地址可能在该人到达那里之前就存在,并将在该人离开之后存在。此外,一个人知道他们住在什么地址,但地址不知道人们住在其它的哪些地方。因此,这是一种聚合关系。
或者,考虑汽车和发动机。汽车发动机是汽车的一部分。尽管发动机属于汽车,但它也可以属于其他东西,如拥有汽车的人。汽车不对发动机的生产或损坏负责。虽然汽车知道它有一个发动机(为了到达任何地方,它必须这样做),但引擎并不知道它是汽车的一部分。
在对现实对象建模时,使用术语“已销毁”可能有点冒险。有人可能会说,“如果一颗流星从天而降,压碎了汽车,难道汽车零部件不也都被摧毁吗?”是的,当然。但那是流星的错。重要的一点是,汽车不对其部件的损坏负责。
我们可以说聚合模型是 “有一个” 关系(一个班级有教师,汽车有发动机)。
与合成类似,聚合的各个部分可以是单数的,也可以是复数的。
实现聚合
由于聚合类似于组合,因为它们都是部分-整体关系,因此它们的实现几乎相同,并且它们之间的区别主要是语义上的。在组合中,我们通常使用普通成员变量(或指针,但分配和释放过程由组合类处理)将部件添加到组合中。
在聚合中,我们还添加部件作为成员变量。然而,这些成员变量通常是引用或指针,用于在类作用域之外创建的对象。因此,聚合通常要么将其要指向的对象作为构造函数参数,要么初始为空,然后通过访问函数或操作符添加子对象。因为这些部分存在于类的作用域之外,
因此当类被销毁时,指针或引用成员变量将被销毁(但不会删除实际的对象)。因此,部件本身仍然存在。
让我们更详细地看一个教师和班级的例子。在这个例子中,我们将做几个简化:首先,该班级将只容纳一名教师。其次,老师不知道他们是哪个班级的一员。
|
|
在这种情况下,bob是独立于department创建的,然后传递给department的构造函数。当department被销毁时,m_teacher引用被销毁,但bob本身没有被销毁,因此它仍然存在,直到稍后在main()中独立销毁为止。
为您正在建模的内容选择正确的关系
尽管在上面的示例中,教师不知道他们为哪个班级工作似乎有点傻,但在给定的程序环境中,这可能是完全正确的。当您决定要实现哪种关系时,请实现满足需求的最简单的关系,而不是看起来最符合现实生活环境逻辑的关系。
例如,如果您正在编写汽车组装车间模拟器,您可能希望将汽车和发动机实现为聚合,以便可以卸下发动机,并将其放在某处的架子上,供以后使用。然而,如果您正在编写赛车模拟游戏,您可能希望将汽车和发动机实现为组合,因为在该上下文中,发动机永远不会存在于汽车外部。
最佳实践
实现最简单的关系类型,以满足程序的需要,而不是现实生活中看似正确的关系类型
组合与聚合总结
组合:
- 通常使用普通成员变量
- 如果使用指针成员变量,则类自己负责创建和销毁它们
- 负责部分的创建与销毁
聚合:
- 通常使用指针和引用成员变量,它们指向的对象在类外部创建和销毁
- 不负责部分的创建和销毁
值得注意的是,组合和聚合的概念可以在同一个类中自由混合。完全可以编写一个负责创建/销毁某些部分,而其它部分是外部管理的类。例如,班级可以有一个名字和一个老师。名字可能会通过组合添加到类中,并将与该类一起创建和销毁。另一方面,教师将通过聚合添加到类中,并独立创建/销毁。
虽然聚合可能非常有用,但它们也可能更危险,因为聚合不处理其部分的释放。资源销毁由外部执行。如果外部忘记执行清理,则内存将泄漏。
因此,应优先使用组合而不是聚合。
一些警告
由于各种原因,与组合不同,聚合的定义并不精确——因此您可能会看到其他参考资料对它的定义与我们的定义不同。
还有一个注意事项:在之前学习结构体时,我们将聚合数据类型(如结构体和类)定义为将多个变量组装在一起的数据类型。您还可能在C++旅程中遇到术语聚合类,它被定义为没有提供的构造函数、析构函数或重载赋值运算符的结构体或类,所有成员都是public成员,并且不使用继承——本质上是一个普通的旧数据结构。尽管命名相似,但聚合关系和聚合类是不同的,不应混淆。
std::reference_wrapper
在上面的班级/教师示例中,我们使用班级中的引用来存储教师。如果只有一个教师,这很好,但如果一个班级有多个教师怎么办?我们希望将这些教师存储在某种类型的列表中(例如,std::vector),但数组和各种标准库列表不能保存引用(因为列表元素必须是可赋值的,而且引用变量是无法赋值重新指向其它对象)。
|
|
可以使用指针而不是引用,但这可能会导致传递空指针。在班级/教师示例中,我们不希望遇到空指针。为了解决这个问题,可以使用std::reference_wrapper。
从本质上讲,std::reference_wrapper是一个类似于引用的类,但也允许赋值和复制,因此它与std::vector等列表兼容。
好消息是,使用它并不需要真正了解它是如何工作的。您只需要知道三件事:
- std::reference_wrapper 位于 <functional> 头文件
- 当创建 std::reference_wrapper 包装对象时,它不能时匿名对象(匿名对象的作用域是表达式内部,会导致悬空引用)
- 当想从 std::reference_wrapper 中取出对象时,可以使用 get() 成员函数
下面是一个在std::vector 使用 std::reference_wrapper 的示例:
|
|
要创建const引用的向量,指定模版参数是「const std::string」即可
|
|
