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;
}

在此情况下,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!

显示提示
提示:将教师信息存储在 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;
}

浙公网安备 33010602011771号