编程C++ 软件架构师

C++中的聚合(aggregation)和组合(composition)是什么?它们之间有什么区别,以及何时使用哪种方法?

用 Hintsage AI 助手通过面试

答案。

在C++编程中,常常使用两种方式来组合对象:聚合和组合。这些概念反映了类之间的不同关系,并影响相关对象的生命周期和销毁责任。

问题的背景:

在面向对象设计中,始终重要的是区分对象之间的依赖关系。随着面向对象语言(如Smalltalk、C++、Java)的出现,如何更好地建模“部分—整体”关系的问题变得更加突出。在C++中,由于需要手动管理内存和对象生命周期,这一问题变得尤为重要。

问题:

在聚合和组合之间错误选择,可能导致内存泄漏、资源重复或对象销毁错误。此外,这些概念经常被混淆。

解决方案:

  • 组合是对象拥有部分对象并对其创建/销毁负责的关系。在C++中,这通常表示为值成员类或通过unique_ptr。
  • 聚合是较弱的关系,部分对象独立于“整体”存在,生命周期的责任不在于拥有者。通常通过非拥有引用(指针/引用)实现。

代码示例:

// 组合: class Engine {}; class Car { Engine engine; // Engine随Car的创建和销毁而创建和销毁 }; // 聚合: class Person {}; class Team { std::vector<Person*> members; // 指向Person对象,但不拥有它们 };

关键特性:

  • 组合 — 强关系(部分),拥有
  • 聚合 — 弱关系(使用),不拥有
  • 组合自动管理内存,而聚合需要谨慎并就拥有者达成协议

反向问题。

如果在类成员中有一个指向对象的指针,这总是聚合吗?

不一定!如果类拥有此指针(例如,通过std::unique_ptr),这仍然是组合。关系类型不是由字段类型决定,而是由生命周期的责任决定。

class House { std::unique_ptr<Room> room; // 组合,House拥有Room };

组合可以通过引用或原始指针实现吗?

可以 — 但仅在对象由拥有者创建和销毁,且引用或指针用于优化的情况下。然而,更好的做法是使用值对象或智能指针以明确表示所有权。

如果在组合中部分对象在拥有者之外创建并传递给它,会发生什么?

在这种情况下,这可能导致组合的不变性被破坏:如果外部创建的对象被传递给拥有者并被销毁,而某处仍有指向它的引用 — 将会出现悬空指针。必须严格定义项目中的所有权和销毁责任。

常见错误和反模式

  • 混淆聚合和组合的概念(例如,存储不必要的原始指针,但在拥有者的析构函数中试图销毁它们)
  • 在需要严格生命周期的地方使用聚合(例如,复杂对象的细节)
  • 不释放非拥有的指针

实际案例

负面案例

一个团队决定通过原始指针在容器中存储所有嵌套对象,并在析构函数中手动销毁它们。一切正常,直到他们更改了所有权模型。结果指针被释放了两次,导致崩溃。

优点:

  • 对于某些变体的架构灵活性(例如,对象之间的漂浮关系)

缺点:

  • 内存管理错误的高风险
  • 难以维护

正面案例

另一个团队改为对所有真正的所有权关系使用std::unique_ptr,而非拥有的仅在作为临时引用时使用。这明确表达了架构。

优点:

  • 透明且清晰的所有权关系
  • 无内存泄漏或双重释放

缺点:

  • 并不总是可能实现循环组合
  • 有时需要改进对象之间的沟通协议