C++聚合和組合詳解

2020-07-16 10:04:42
當一個類的物件擁有另一個類的物件時,就會發生類聚合類組合是一種聚合形式,其中擁有者類控制被擁有者類物件的生命週期。

我們知道,一個類可以包含成員,而該成員本身又可以是其他類的物件。當一個類 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 關係的一種方式。