C++成員物件和封閉類詳解

2020-07-16 10:04:20
一個類的成員變數如果是另一個類的物件,就稱之為“成員物件”。包含成員物件的類叫封閉類(enclosed class)。

封閉類建構函式的初始化列表

當封閉類的物件生成並初始化時,它包含的成員物件也需要被初始化,這就會引發成員物件建構函式的呼叫。如何讓編譯器知道,成員物件到底是用哪個建構函式初始化的呢?這可以通過在定義封閉類別建構函式時,新增初始化列表的方式解決。

在建構函式中新增初始化列表的寫法如下:

類名::建構函式名(參數列): 成員變數1(參數列), 成員變數2(參數列), ...
{
    ...
}

:{之間的部分就是初始化列表。初始化列表中的成員變數既可以是成員物件,也可以是基本型別的成員變數。對於成員物件,初始化列表的“參數列”中存放的是建構函式的引數(它指明了該成員物件如何初始化)。對於基本型別成員變數,“參數列”中就是一個初始值。

“參數列”中的引數可以是任何有定義的表示式,該表示式中可以包括變數甚至函數呼叫等,只要表示式中的識別符號都是有定義的即可。例如:
#include <iostream>
using namespace std;
class CTyre  //輪胎類
{
private:
    int radius;  //半徑
    int width;  //寬度
public:
    CTyre(int r, int w) : radius(r), width(w) { }
};
class CEngine  //引擎類
{
};
class CCar {  //汽車類
private:
    int price;  //價格
    CTyre tyre;
    CEngine engine;
public:
    CCar(int p, int tr, int tw);
};
CCar::CCar(int p, int tr, int tw) : price(p), tyre(tr, tw)
{
};
int main()
{
    CCar car(20000, 17, 225);
    return 0;
}
第 9 行的建構函式新增了初始化列表,將 radius 初始化成 r,width 初始化成 w。這種寫法比在函數體內用 r 和 w 對 radius 和 width 進行賦值的風格更好。建議對成員變數的初始化都使用這種寫法。

CCar 是一個封閉類,有兩個成員物件:tyre 和 engine。在編譯第 27 行時,編譯器需要知道 car 物件中的 tyre 和 engine 成員物件該如何初始化。

編評器已經知道這裡的 car 物件是用上面的 CCar(int p, int tr, int tw) 建構函式初始化的,那麼 tyre 和 engine 該如何初始化,就要看第 22 行 CCar(int p,int tr,int tw) 後面的初始化列表了。該初始化列表表明,tyre 應以 tr 和 tw 作為引數呼叫 CTyre(intr, hit w) 建構函式初始化,但是並沒有說明 engine 該如何處理。在這種情況下,編譯器就認為 engine 應該用 CEngine 類的無參建構函式初始化。而 CEngine 類確實有一個編譯器自動生成的預設無參建構函式,因此,整個 car 物件的初始化問題就都解決了。

總之,生成封閉類物件的語句一定要讓編譯器能夠弄明白其成員物件是如何初始化的,否則就會編譯錯誤。

在上面的程式中,如果 CCar 類別建構函式沒有初始化列表,那麼第 27 行就會編譯出錯,因為編譯器不知道該如何初始化 car.tyre 物件,因為 CTyre 類沒有無參建構函式,而編譯器又找不到用來初始化 car.tyre 物件的引數。

封閉類物件生成時,先執行所有成員物件的建構函式,然後才執行封閉類自己的建構函式。成員物件建構函式的執行次序和成員物件在類定義中的次序一致,與它們在建構函式初始化列表中出現的次序無關。

當封閉類物件消亡時,先執行封閉類的解構函式,然後再執行成員物件的解構函式,成員物件解構函式的執行次序和建構函式的執行次序相反,即先構造的後解構,這是 C++ 處理此類次序問題的一般規律。

例如下面的程式:
#include<iostream>
using namespace std;
class CTyre {
public:
    CTyre() { cout << "CTyre constructor" << endl; }
    ~CTyre() { cout << "CTyre destructor" << endl; }
};
class CEngine {
public:
    CEngine() { cout << "CEngine constructor" << endl; }
    ~CEngine() { cout << "CEngine destructor" << endl; }
};
class CCar {
private:
    CEngine engine;
    CTyre tyre;
public:
    CCar() { cout << "CCar constructor" << endl; }
    ~CCar() { cout << "CCar destructor" << endl; }
};
int main() {
    CCar car;
    return 0;
}
執行結果:
CEngine constructor
CTyre constructor
CCar constructor
CCar destructor
CTyre destructor
CEngine destructor

封閉類的物件初始化時,要先執行成員物件的建構函式,是因為封閉類別建構函式中有可能用到成員物件。如果此時成員物件還沒有初始化,那就不合理了。

思考題:為什麼封閉類物件消亡時,要先執行封閉類的解構函式,然後才執行成員物件的解構函式?

封閉類的複製建構函式

封閉類的物件,如果是用預設複製建構函式初始化的,那麼它包含的成員物件也會用複製建構函式初始化。例如下而的程式:
#include <iostream>
using namespace std;
class A
{
public:
    A() { cout << "default" << endl; }
    A(A &a) { cout << "copy" << endl; }
};
class B
{
    A a;
};
int main()
{
    B b1, b2(b1);
    return 0;
}
程式的輸出結果是:
default
copy

說明 b2.a 是用類 A 的複製建構函式初始化的,而且呼叫複製建構函式時的實參就是 b1.a。