章节目录

聚合

本节阅读量:

在上一课中,我们注意到对象组合是从简单对象创建复杂对象的过程。我们还讨论了一种类型的对象组合,称为组合。在组合关系中,整个对象负责部分的存在。

在本课中,将研究对象组合的另一个子类型,称为聚合。


聚合

若要符合聚合的条件,整个对象及其部分必须具有以下关系:

  1. 部件(成员)是对象(类)的一部分
  2. 部件(成员)可以同时属于多个对象(类)
  3. 部件(成员)的存在不由对象(类)管理
  4. 部件(成员)不知道对象(类)的存在

与组合不同,在聚合中,部件(成员)可以同时属于多个对象(类),并且部件(成员)的存在不由整体对象(类)管理。创建聚合时,聚合不负责创建部件。当聚合被销毁时,聚合不负责销毁部件。

例如,考虑一个人与其家庭住址之间的关系。在这个例子中,为了简单起见,我们将说每个人都有一个地址。然而,这个地址一次可以属于多个人:例如,你和你的室友。然而,该地址不是由该人管理的——该地址可能在该人到达那里之前就存在,并将在该人离开之后存在。此外,一个人知道他们住在什么地址,但地址不知道人们住在其它的哪些地方。因此,这是一种聚合关系。

或者,考虑汽车和发动机。汽车发动机是汽车的一部分。尽管发动机属于汽车,但它也可以属于其他东西,如拥有汽车的人。汽车不对发动机的生产或损坏负责。虽然汽车知道它有一个发动机(为了到达任何地方,它必须这样做),但引擎并不知道它是汽车的一部分。

在对现实对象建模时,使用术语“已销毁”可能有点冒险。有人可能会说,“如果一颗流星从天而降,压碎了汽车,难道汽车零部件不也都被摧毁吗?”是的,当然。但那是流星的错。重要的一点是,汽车不对其部件的损坏负责。

我们可以说聚合模型是 “有一个” 关系(一个班级有教师,汽车有发动机)。

与合成类似,聚合的各个部分可以是单数的,也可以是复数的。


实现聚合

由于聚合类似于组合,因为它们都是部分-整体关系,因此它们的实现几乎相同,并且它们之间的区别主要是语义上的。在组合中,我们通常使用普通成员变量(或指针,但分配和释放过程由组合类处理)将部件添加到组合中。

在聚合中,我们还添加部件作为成员变量。然而,这些成员变量通常是引用或指针,用于在类作用域之外创建的对象。因此,聚合通常要么将其要指向的对象作为构造函数参数,要么初始为空,然后通过访问函数或操作符添加子对象。因为这些部分存在于类的作用域之外,

因此当类被销毁时,指针或引用成员变量将被销毁(但不会删除实际的对象)。因此,部件本身仍然存在。

让我们更详细地看一个教师和班级的例子。在这个例子中,我们将做几个简化:首先,该班级将只容纳一名教师。其次,老师不知道他们是哪个班级的一员。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <string>
#include <string_view>

class Teacher
{
private:
  std::string m_name{};

public:
  Teacher(std::string_view name)
      : m_name{ name }
  {
  }

  const std::string& getName() const { return m_name; }
};

class Department
{
private:
  const Teacher& m_teacher; // 为了简单示意,这里只有一个教师,可以有多个

public:
  Department(const Teacher& teacher)
      : m_teacher{ teacher }
  {
  }
};

int main()
{
  // 在班级外创建教师 Bob
  Teacher bob{ "Bob" }; // 创建一个教师

  {
    // 创建班级,并用构造函数传递 Bob进去
    Department department{ bob };

  } // department超出作用域,被销毁

  // Bob 仍然有效, 但 department 已被销毁

  std::cout << bob.getName() << " still exists!\n";

  return 0;
}

在这种情况下,bob是独立于department创建的,然后传递给department的构造函数。当department被销毁时,m_teacher引用被销毁,但bob本身没有被销毁,因此它仍然存在,直到稍后在main()中独立销毁为止。


为您正在建模的内容选择正确的关系

尽管在上面的示例中,教师不知道他们为哪个班级工作似乎有点傻,但在给定的程序环境中,这可能是完全正确的。当您决定要实现哪种关系时,请实现满足需求的最简单的关系,而不是看起来最符合现实生活环境逻辑的关系。

例如,如果您正在编写汽车组装车间模拟器,您可能希望将汽车和发动机实现为聚合,以便可以卸下发动机,并将其放在某处的架子上,供以后使用。然而,如果您正在编写赛车模拟游戏,您可能希望将汽车和发动机实现为组合,因为在该上下文中,发动机永远不会存在于汽车外部。


组合与聚合总结

组合:

  1. 通常使用普通成员变量
  2. 如果使用指针成员变量,则类自己负责创建和销毁它们
  3. 负责部分的创建与销毁

聚合:

  1. 通常使用指针和引用成员变量,它们指向的对象在类外部创建和销毁
  2. 不负责部分的创建和销毁

值得注意的是,组合和聚合的概念可以在同一个类中自由混合。完全可以编写一个负责创建/销毁某些部分,而其它部分是外部管理的类。例如,班级可以有一个名字和一个老师。名字可能会通过组合添加到类中,并将与该类一起创建和销毁。另一方面,教师将通过聚合添加到类中,并独立创建/销毁。

虽然聚合可能非常有用,但它们也可能更危险,因为聚合不处理其部分的释放。资源销毁由外部执行。如果外部忘记执行清理,则内存将泄漏。

因此,应优先使用组合而不是聚合。


一些警告

由于各种原因,与组合不同,聚合的定义并不精确——因此您可能会看到其他参考资料对它的定义与我们的定义不同。

还有一个注意事项:在之前学习结构体时,我们将聚合数据类型(如结构体和类)定义为将多个变量组装在一起的数据类型。您还可能在C++旅程中遇到术语聚合类,它被定义为没有提供的构造函数、析构函数或重载赋值运算符的结构体或类,所有成员都是public成员,并且不使用继承——本质上是一个普通的旧数据结构。尽管命名相似,但聚合关系和聚合类是不同的,不应混淆。


std::reference_wrapper

在上面的班级/教师示例中,我们使用班级中的引用来存储教师。如果只有一个教师,这很好,但如果一个班级有多个教师怎么办?我们希望将这些教师存储在某种类型的列表中(例如,std::vector),但数组和各种标准库列表不能保存引用(因为列表元素必须是可赋值的,而且引用变量是无法赋值重新指向其它对象)。

1
std::vector<const Teacher&> m_teachers{}; // 非法

可以使用指针而不是引用,但这可能会导致传递空指针。在班级/教师示例中,我们不希望遇到空指针。为了解决这个问题,可以使用std::reference_wrapper。

从本质上讲,std::reference_wrapper是一个类似于引用的类,但也允许赋值和复制,因此它与std::vector等列表兼容。

好消息是,使用它并不需要真正了解它是如何工作的。您只需要知道三件事:

  1. std::reference_wrapper 位于 <functional> 头文件
  2. 当创建 std::reference_wrapper 包装对象时,它不能时匿名对象(匿名对象的作用域是表达式内部,会导致悬空引用)
  3. 当想从 std::reference_wrapper 中取出对象时,可以使用 get() 成员函数

下面是一个在std::vector 使用 std::reference_wrapper 的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <functional> // std::reference_wrapper
#include <iostream>
#include <vector>
#include <string>

int main()
{
  std::string tom{ "Tom" };
  std::string berta{ "Berta" };

  std::vector<std::reference_wrapper<std::string>> names{ tom, berta }; // vector中存储的是 reference, 而不是值

  std::string jim{ "Jim" };

  names.emplace_back(jim);

  for (auto name : names)
  {
    // 使用 get() 成员函数获得存储的引用
    name.get() += " Beam";
  }

  std::cout << jim << '\n'; // 打印 Jim Beam

  return 0;
}

要创建const引用的向量,指定模版参数是「const std::string」即可

1
2
// Vector 中装的是指向 const std::string 的引用
std::vector<std::reference_wrapper<const std::string>> names{ tom, berta };

23.1 对象组合

上一节

23.3 关联

下一节