聚合
本节阅读量:在上一课中,我们了解到对象组合是从简单对象创建复杂对象的过程。我们还讨论了一种对象组合类型,称为组合。在组合关系中,整体对象负责部分的存在。
在本课中,我们将研究对象组合的另一个子类型:聚合。
聚合
若要符合聚合的条件,整个对象及其部分必须具有以下关系:
- 部件(成员)是对象(类)的一部分
- 部件(成员)可以同时属于多个对象(类)
- 部件(成员)的存在不由对象(类)管理
- 部件(成员)不知道对象(类)的存在
与组合不同,在聚合中,部件(成员)可以同时属于多个对象(类),并且部件(成员)的存在不由整体对象(类)管理。创建聚合时,聚合不负责创建部件;聚合被销毁时,也不负责销毁部件。
例如,考虑一个人与其家庭住址之间的关系。在这个例子中,为了简单起见,我们假设每个人都有一个地址。然而,一个地址可以同时属于多个人:例如,你和你的室友。同时,该地址并不是由某个人管理的——它可能在某个人搬来之前就已经存在,并且会在这个人离开之后继续存在。此外,一个人知道自己住在什么地址,但地址不知道有哪些人住在那里。因此,这是一种聚合关系。
或者,考虑汽车和发动机。汽车发动机是汽车的一部分。尽管发动机属于汽车,但它也可以属于其他对象,例如拥有汽车的人。汽车不负责发动机的生产或销毁。虽然汽车知道自己有一个发动机(否则它无法行驶),但发动机并不知道自己是汽车的一部分。
在对现实对象建模时,使用术语“已销毁”可能有点冒险。有人可能会说,“如果一颗流星从天而降,压碎了汽车,难道汽车零部件不也都被摧毁吗?”是的,当然。但那是流星的错。重要的一点是,汽车不对其部件的损坏负责。
我们可以说,聚合模型表达的是 “有一个” 关系(一个班级有教师,汽车有发动机)。
与合成类似,聚合的各个部分可以是单数的,也可以是复数的。
实现聚合
聚合与组合类似,因为它们都是部分-整体关系,所以二者的实现方式几乎相同,区别主要体现在语义上。在组合中,我们通常使用普通成员变量(或指针,但分配和释放过程由组合类处理)将部件添加到组合中。
在聚合中,我们同样会将部件添加为成员变量。然而,这些成员变量通常是引用或指针,指向在类作用域之外创建的对象。因此,聚合通常要么将要指向的对象作为构造函数参数传入,要么先初始化为空,然后通过访问函数或操作符添加子对象。因为这些部分存在于类的作用域之外,
所以当类被销毁时,指针或引用成员变量会被销毁(但不会删除实际对象)。因此,部件本身仍然存在。
让我们更详细地看一个教师和班级的例子。在这个例子中,我们会做几个简化:首先,该班级只包含一名教师。其次,老师不知道自己属于哪个班级。
|
|
在这种情况下,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」即可。
|
|