C++聚合和组合详解

当一个类的对象拥有另一个类的对象时,就会发生类聚合类组合是一种聚合形式,其中拥有者类控制被拥有者类对象的生命周期。

我们知道,一个类可以包含成员,而该成员本身又可以是其他类的对象。当一个类 C 包含一个成员,而该成员又是另一个类 D 的对象时,C 中的每个对象都将有一个类 D 的对象。这在 C 和 D 之间创建了有一个 Has-a 的关系。在这种类型的关系中,每个 C 的实例都拥有类 D 的一个实例。

C++ 中,这样的所有权通常是由于 C 具有 D 类型的成员而产生的,但也可能是由于 C 具有指向 D 的对象的指针而产生的。术语聚合通常广泛用于描述一个类的对象拥有其他类的对象的情况。

成员初始化列表

来看以下的 Person 和 Date 类代码:
class Date
{
    string month;
    int day, year;
    public:
        Date(string m, int d, int y)
        {
            month = m;
            day = d;
            year = y;
        }
};

class Person
{
    string name;
    Date dateOfBirth;
    public:
        Person(string name, string month, int day, int year)
        {
            //将month、day和year传递给 dateOfBirth构造函数
            this->name = name;
        }
};
Person 构造函数接收形参 month、day 和 year,将它们传递给其 dateOfBirth 成员的 Date 构造函数。C++ 提供了一个特殊的表示法,称为成员初始化列表,它允许类的构造函数将实参传递给成员对象的构造函数。

成员初始化列表是一个使用逗号分隔的列表,可以通过它调用成员对象构造函数。它有一个冒号前缀,位于构造函数的函数头之后,函数体之前:
class Person
{
    string name;
    Date dateOfBirth;
    public:
        Person(string name, string month, int day, int year):dateOfBirth (month, day, year) //成员初始化类别
        {
            this->name = name;
        }
};
请注意,冒号在构造函数头的末尾。另外,在调用被包含的 Date 对象的构造函数时,它使用的是对象的名称即 dateOfBirth,而不是使用对象的类,即 Date。这允许在相同的初始化列表中调用同一个类的不同对象的构造函数。

虽然成员初始化列表常用于调用成员对象的构造函数,但它也可以用来初始化任何类型的成员变量。因此,Person 和 Date 类可以写成如下形式:
class Date
{
    string month;
    int day, year;
    public:
        Date(string m, int d, int y):month(m),day(d),year (y) // 成员初始化类别
        {
        }
};

class Person
{
    string name;
    Date dateOfBirth;
    public:
        Person(string name, string month, int day, int year): name(name),dateOfBirth(month, day, year)
        {
        }
};
可以看到,Date 和 Person 构造函数的函数体现在是空的,这是因为,以前一般在函数体中执行的给成员变量赋值任务现在改由初始化列表完成。

许多程序员更喜欢使用成员初始化列表而不愿意在构造函数的内部赋值,因为它允许编译器在某些情况下生成更有效的代码。在使用成员初始化列表时,按照在类中声明的顺序列出初始化列表中的成员是一种很好的编程习惯。

最后,请注意在 Person 构造函数初始化列表中出现的 name(name)。编译器能够确定第一次出现的 name 是指成员变量,而第二次出现的 name 则是指形参。

通过指针聚合

现在再来做一个假设,每个人除了有一个出生日期之外,还应该有一个居住的国家。一个国家应该有一个名字,可能还有许多其他的属性:
class Country
{
    string name;
    //其他字段
};
因为很多人都会“拥有”同一个国家,所以 Person 和 Country 之间的 Has-a 关系不应该通过在每个 Person 对象中嵌入一个 Country 类的实例来实现。

由于许多人将共享同一个国家,所以釆用嵌入包含的方式实现 Has-a 关系将会造成不必要的数据重复和浪费。另外,当一个国家的任何数据发生变化时,都需要更新许多 Person 对象。使用指针来实现该 Has-a 关系就可以避免这些问题。

以下是 Person 类的一个修改版本,它已经包含了一个指向居住的国家的指针:
class Person
{
    string name;
    Date dateOfBirth;
    shared_ptr<Country> pCountry; //指向居住的国家
    public:
        Person(string name, string month, int day, int year, shared_ptr<Country>& pC)
    {
    }
};

聚合、组合和对象生命周期

组合是一个用于描述特殊聚合情形的术语,其被拥有的对象的生命周期与其拥有者的生命周期是一致的。

组合有一个很好的示例是,一个类 C 包含的成员是另一个类 D 的对象,被包含的 D 对象在创建 C 对象的同时创建,并且当 C 对象被销毁或超出作用域时,D 对象也将被销毁或超出作用域。

组合的另一个例子是,类 C 包含一个指向 D 对象的指针,D 对象由 C 构造函数创建并由 C 析构函数销毁。

以下程序修改了上述类,以说明聚合、组合和对象生命周期这些概念。每个类都有一个构造函数来宣告其对象的创建,也有一个析构函数来宣告它们的消亡。Person 类有一个静态成员如下:

int Person :: uniquePersonID;

该静态成员用于生成一个数字,在创建 Person 对象分配给它。这些数字可以作为一种通用的个人身份识别号码,就像现实生活中的个人身份证号码一样。这些数字存储在 Person 和 Date 类的 personID 字段中,用于标识正在创建或销毁的对象。每个 dateOfBirth 对象携带与包含它的 Person 对象相同的 personID 编号。
// This program illustrates aggregation, composition and object lifetimes.
#include <iostream>
#include <string>
using namespace std;

class Date
{
    string month;
    int day, year;
    int personID; // ID of person whose birthday this is
    public:
        Date(string m, int d, int y, int id):month(m), day(d), year(y), personID(id)
        {
            cout << "Date-Of-Birth object for person " << personID << " has been created.\n";
        }
        ~Date()
        {
            cout << "Date-Of-Birth object for person " << personID << " has been destroyed. \n";
        }
};
class Country
{
    string name;
    public:
        Country(string name) : name(name)
        {
            cout << "A Country object has been created.\n";
        }
        ~Country()
        {
            cout << "A Country obj ect has been destroyed.\n";
        }
};

class Person
{
    string name;
    Date dateOfBirth;
    int personID; // Person identification number (PID)
    shared_ptr <Country> pCountry;
    public:
        Person(string name, string month, int day,int year, shared_ptr<Country>& pC): name(name),dateOfBirth(month,day,year,Person::uniquePersonID), personID(Person::uniquePersonID), pCountry(pC)
        {
            cout << "Person object "<< personID << " has been created.\n";
            Person::uniquePersonID ++;
        }
        ~Person()
        {
            cout << "Person object " << personID << "  has been destroyed. \n";
        }
        static int uniquePersonID; // Used to generate PIDs
};

//Define the static class variable
int Person::uniquePersonID = 1;

int main()
{
    // Create a Country object
    shared_ptr<Country> p_usa = make_shared<Country>("USA");
    // Create a Person object
    shared_ptr<Person> p = make_shared<Person>("Peter Lee", "January", 1, 1985, p_usa);
    // Create another Person object
    shared_ptr<Person> p1 = make_shared<Person>("Eva Gustafson", "May", 15, 1992, p_usa);
    cout << "Now there are two people.\n";
    // Both person will go out of scope when main returns
    return 0;
}
程序输出结果:

A Country object has been created.
Date-Of-Birth object for person 1 has been created.
Person object 1 has been created.
Date-Of-Birth object for person 2 has been created. Person object 2 has been created.
Now there are two people.
Person object 1 has been destroyed.
Date-Of-Birth object for person 1 has been destroyed. Now there is only one.
Person object 2 has been destroyed.
Date-Of-Birth object for person 2 has been destroyed.
A Country object has been destroyed.

dateOfBirth 对象和包含它们的 Person 对象之间的关系是组合的一个例子。从程序输出结果中可以看到,这些 Date 对象是同时创建的,与拥有它们的 Person 对象同时销毁。相对来说,Person 与 Country 之间的关系则可以说是更普通的聚合形式。

通过查看 print 成员函数,可以看到封闭类的成员函数如何访问所包含类的成员函数的示例。

Has-a 关系

当一个类包含第二个类的实例时,第一个类被认为形成了 Has-a 第二个类的关系。

例如,Acquaintance 类有一个 Date 类组成了它的 dob 成员,而 Date 类则有一个 string 对象组成了它的 month 成员。在面向对象系统的设计过程中,Has-a 关系对建立类和对象之间的关系模型非常重要。

程序中的类之间还有一个重要关系是 Is-a 关系。需要记住的是,对象的组合实现了Has-a 关系,而继承则是实现 Is-a 关系的一种方式。