23-3 聚合(todo)

在上一课23.2——组合中,我们提到对象组合是通过简单对象构建复杂对象的过程。我们还讨论了一种对象组合形式,即组合关系。在此关系中,整体对象对部分对象的存在负有责任。

本课我们将探讨对象组合的另一种形式,即聚合关系。


聚合

要构成聚合关系aggregation,整体对象及其部分必须满足以下条件:

  • 部分(成员)属于对象(类)
  • 部分(成员)可同时属于多个对象(类)
  • 部分(成员)的存在不受对象(类)管理
  • 部分(成员)未知晓整体(类)的存在

与组合类似,聚合仍属于部分-整体关系,即部分被整体所包含,且为单向关系。但不同于组合的是:部分可同时属于多个对象,且整体对象不负责管理部分的存在与生命周期。聚合创建时无需负责创建组成部分,销毁时亦无需负责销毁组成部分。

以人与住所的关系为例:为简化说明,假设每个人都有一个地址。但该地址可能同时属于多人——例如你和室友或伴侣。但该地址并非由个人管理——地址很可能在个人入住前就存在,并在个人离开后继续存在。此外,个人知道自己居住的地址,但地址并不知道哪些人居住于此。因此这属于聚合关系。

再以汽车与引擎为例。引擎是汽车的组成部分,虽归属汽车,却也可能属于其他主体(如车主)。汽车既不负责引擎的制造也不参与其报废,它仅知晓自身拥有引擎(否则无法行驶),而引擎并不知晓自己属于汽车。

在建模物理对象时,使用“被摧毁”一词可能存在争议。有人会质疑:“若陨石坠落砸毁汽车,难道汽车零件不会全被摧毁吗?”当然会。但这应归咎于陨石。关键在于汽车本身不负责其零件的毁灭(但外部力量可能负责)。

聚合模型体现了“拥有”关系(部门拥有教师,汽车拥有引擎)。

与组合类似,聚合的组成部分可以是单数或复数形式。


实现聚合

由于聚合与组合都属于部分-整体关系,其实现方式几乎完全相同,二者差异主要体现在语义层面。在组合中,我们通常通过普通成员变量(或由组合类处理分配与释放的指针)将部件添加到组合中。

聚合中同样采用成员变量添加部件,但这些成员变量通常是引用或指针,用于指向类作用域外创建的对象。因此聚合类要么通过构造函数参数接收目标对象,要么初始为空,随后通过访问函数或运算符添加子对象。

由于这些部件存在于类作用域之外,当类被销毁时,指针或引用成员变量将被销毁(但不会被删除)。因此,部件本身仍将存在。

让我们更详细地审视教师与系的示例。在此示例中,我们将进行两项简化:首先,每个系仅容纳一名教师;其次,教师不会知晓自己所属的系别。

#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; // This dept holds only one teacher for simplicity, but it could hold many teachers

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

int main()
{
  // Create a teacher outside the scope of the Department
  Teacher bob{ "Bob" }; // create a teacher

  {
    // Create a department and use the constructor parameter to pass
    // the teacher to it.
    Department department{ bob };

  } // department goes out of scope here and is destroyed

  // bob still exists here, but the department doesn't

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

  return 0;
}

image

在此情况下,bob独立于department创建,随后被传递至department的构造函数。当department被销毁时,m_teacher引用会被销毁,但teacher对象本身不会被销毁,因此它仍将存在直至在main()中被独立销毁。


为建模对象选择合适的关系

尽管在上例中教师不知自己所属部门看似有些荒谬,但在特定程序场景下这完全合理。确定关系类型时,应选择满足需求的最简关系,而非看似最符合现实情境的关系。

例如:开发汽车修理厂模拟器时,可将汽车与引擎设计为聚合关系,以便拆卸引擎存放备用;但若开发赛车模拟器,则应采用组合关系——引擎在该场景中永远无法脱离汽车独立存在。

最佳实践:
实现满足程序需求的最简单关系类型,而非现实中看似正确的关系类型。


组合与聚合的总结

组合:

  • 通常使用普通成员变量
  • 若类自行处理对象分配/释放,可使用指针成员
  • 负责部件的创建/销毁

聚合:

  • 通常使用指向或引用聚合类作用域外对象的指针或引用成员
  • 不负责创建/销毁组成部分

值得注意的是,组合与聚合的概念可在同一类中自由混合使用。完全可以编写一个对部分组件负责创建/销毁、对其他组件则不负责的类。例如,我们的Department类可包含名称和教师。名称通常通过组合添加至部门,其生命周期与部门同步。而教师则通过聚合添加至部门,其创建/销毁过程独立于部门。

聚合虽然极具实用性,但潜在风险更高,因为聚合机制不会处理组成部分的内存释放。释放操作需依赖外部实体完成。若外部实体不再持有被弃用部分的指针或引用,或单纯忘记执行清理(假设该类会自行处理),则会导致内存泄漏。

因此,应优先采用组合而非聚合模式。


若干警告/勘误

出于多种历史和语境原因,聚合的定义不像组合那样精确——因此您可能会发现其他参考资料对其定义与我们不同。这很正常,只需知晓即可。

最后补充说明:在第13.7节 结构体、成员及成员选择介绍 中,我们将聚合数据类型(如结构体和类)定义为将多个变量组合在一起的数据类型。在C++学习过程中,您可能还会遇到“聚合类”(aggregate class)这一术语,其定义为:不提供构造函数、析构函数或重载赋值运算符,所有成员均为公有的结构体或类,且不使用继承——本质上即普通数据结构(POD)。尽管名称相似,聚合体(aggregate)与聚合(aggregation)存在本质区别,切勿混淆。


std::reference_wrapper

在上文的Department/Teacher示例中,我们使用Department中的引用来存储Teacher。当仅存在一位教师时这种方式可行,但若一个Department拥有多位教师呢?我们希望将这些教师存储在某种列表中(例如std::vector),但固定大小的数组和标准库中的各类列表都无法容纳引用(因为列表元素必须可赋值,而引用无法被重新赋值)。

std::vector<const Teacher&> m_teachers{}; // Illegal

与其使用引用,我们也可以使用指针,但这会导致存储或传递空指针的可能性。在Department/Teacher示例中,我们不希望允许空指针。为了解决这个问题,可以使用std::reference_wrapper。

本质上,std::reference_wrapper 是一个类,它像引用一样工作,但同时允许赋值和复制,因此与 std::vector 等列表兼容。

好消息是,使用它时你其实不必理解其工作原理。你只需知道三件事:

  • std::reference_wrapper 位于 头文件中。
  • 创建 std::reference_wrapper 封装对象时,该对象不能是匿名对象(因匿名对象仅在表达式作用域内有效,会导致引用悬空)。
  • 要从 std::reference_wrapper 中获取原始对象,需调用 get() 成员函数。

以下是在 std::vector 中使用 std::reference_wrapper 的示例:

#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 }; // these strings are stored by reference, not value

  std::string jim{ "Jim" };

  names.emplace_back(jim);

  for (auto name : names)
  {
    // Use the get() member function to get the referenced string.
    name.get() += " Beam";
  }

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

  return 0;
}

要创建一个常量引用的向量,我们需要在 std::string 前添加 const 限定符,如下所示:

// Vector of const references to std::string
std::vector<std::reference_wrapper<const std::string>> names{ tom, berta };

测验时间

问题 #1

以下情况更适合采用组合还是聚合实现?

a) 具有颜色的球体
b) 雇佣多名员工的雇主
c) 大学中的院系
d) 你的年龄
e) 一袋弹珠

显示答案

a) 组合论:颜色是球体固有的属性。
b) 聚合论:雇主最初没有雇员,且希望破产时不会摧毁所有雇员。
c) 组合论:没有大学就不可能存在系部。
d) 组合论:年龄是你固有的属性。
e) 聚合论:袋子与袋中弹珠具有独立存在性。

问题 #2

更新部门/教师示例,使部门能够管理多位教师。以下代码应能执行:

#include <iostream>

// ...

int main()
{
  // Create a teacher outside the scope of the Department
  Teacher t1{ "Bob" };
  Teacher t2{ "Frank" };
  Teacher t3{ "Beth" };

  {
    // Create a department and add some Teachers to it
    Department department{}; // create an empty Department

    department.add(t1);
    department.add(t2);
    department.add(t3);

    std::cout << department;

  } // department goes out of scope here and is destroyed

  std::cout << t1.getName() << " still exists!\n";
  std::cout << t2.getName() << " still exists!\n";
  std::cout << t3.getName() << " still exists!\n";

  return 0;
}

这应该输出:

Department: Bob Frank Beth
Bob still exists!
Frank still exists!
Beth still exists!

image

显示提示

提示:将教师信息存储在 std::vector 中

显示解答

#include <functional> // std::reference_wrapper
#include <iostream>
#include <string>
#include <string_view>
#include <vector>

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:
  std::vector<std::reference_wrapper<const Teacher>> m_teachers{};

public:
  Department() = default;

  // Pass by regular reference. The user of the Department class shouldn't care
  // about how it's implemented.
  void add(const Teacher& teacher)
  {
    m_teachers.emplace_back(teacher);
  }

  friend std::ostream& operator<<(std::ostream& out, const Department& department)
  {
    out << "Department: ";

    for (const auto& teacher : department.m_teachers)
    {
      out << teacher.get().getName() << ' ';
    }

    out << '\n';

    return out;
  }
};

int main()
{
  // Create a teacher outside the scope of the Department
  Teacher t1{ "Bob" };
  Teacher t2{ "Frank" };
  Teacher t3{ "Beth" };

  {
    // Create a department and add some Teachers to it
    Department department{}; // create an empty Department

    department.add(t1);
    department.add(t2);
    department.add(t3);

    std::cout << department;

  } // department goes out of scope here and is destroyed

  std::cout << t1.getName() << " still exists!\n";
  std::cout << t2.getName() << " still exists!\n";
  std::cout << t3.getName() << " still exists!\n";

  return 0;
}
posted @ 2026-01-30 22:48  游翔  阅读(0)  评论(0)    收藏  举报