程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++對象模型之默認構造函數的構造操作

C++對象模型之默認構造函數的構造操作

編輯:關於C++
一個類,如果沒有任何的用戶聲明的的構造函數,那麼會有一個默認的構造函數被隱式地聲明出來。這個被隱式聲明的構造函數,究竟什麼時候被合成、被編譯器合成的默認構造函數究竟執行怎麼樣的操作,編譯器如何處理用戶定義的構造函數,就是本文要探討的問題。

1、默認構造函數何時被合成
如果一個類沒有任何的用戶聲明的構造函數,那麼在當編譯器需要的時候,編譯器會為類合成一個默認的構造函數,它只用於執行編譯器所需要的操作。注意,默認的構造函數是在編譯器需要的時候被合成出來,而不是程序需要的時候,如果程序需要,則默認的構造函數應該由程序員實現。

那麼編譯器需要的時候是什麼時候呢?正確來說,編譯器需要的時候是遇到如下四種情況的時候,它需要為以下四種類型的類合成一個默認的構造函數:
1)類的成員變量帶有默認構造函數
2)類的基類帶有默認構造函數
3)類帶有virtual函數
4)類帶有一個virtual基類
且合成操作只有在構造函數真正需要被調用時才會被合成。

現在還有一個問題就是,在C++中各個不同的編譯模塊中,編譯器如何避免合成多個默認呢?解決方法是把合成的默認構造函數、復制構造函數、析構函數、賦值操作運算符等都以inline的方式完成。如果函數太復雜,不適合做成inline,就會成成一個顯式非inline的static函數。無論是inline還是非inline的static函數,其作用都是為了不被文件以外者訪問。

對於下面的這段程序:
class X
{
    public:
        int mData;
        int *mPtr;
}; 

類X沒有聲明任何的構造函數,但是它並不屬於上述所說的4種類型的類,所以編譯器並不會為類X合成一個默認的構造函數。

下面詳細分析編譯器為上述4種類型的類合成的默認構造函數的行為。
2、 類的成員變量帶有默認構造函數 (即類含有成員類對象(member class objects ))
這種情況是指:一個類沒有任何構造函數,但它的成員變量是一個有默認構造函數的類的變量。

例如,如下代碼所示:
class X
{
    public:
        X(){mData = 0; cout << "X::X()" << endl;}
        int mData;
};

class Xs
{
    public:
        X mX;
        int mN;
};

int main()
{
    Xs xs;
    cout << xs.mX.mData << endl;
    cout << xs.mN << endl;
    return 0;
} 

類Xs沒有定義任何構造函數,但是其成員變量mX是類型是X,且類X擁有一個默認的構造函數,其運行結果如下:
\

從運行的結果可以看出,在編譯器為類Xs合成的默認構造函數中,調用了X的構造函數為其成員變量mX進行初始化,但是該合成的默認構造函數卻沒有對類Xs的成員變量mN進行初始化。可見,編譯器為類Xs合成的構造函數如下偽代碼所示:
inline Xs::Xs()
{
    mX.X::X();
}

有時候我們會為類定義一個默認構造函數,但是在該構造函數中,只初始化部分的成員變量,那麼會發生什麼樣的行為和效果呢?保持類X和main函數的測試代碼不變,修改類Xs的代碼為如下:
class Xs
{
    public:
        Xs()
        {
            cout << "Xs::Xs()" << endl;
            mN = 1;
        }
        X mX;
        int mN;
}; 

可以看到,我們為類Xs添加了一個默認的構造函數,但在該構造函數中,我們只為成員變量mN進行初始化,其運行結果如下:
\

由運行結果可以看出,雖然在類的Xs的默認構造函數中,我們沒有顯示地對mX進行初始化,但是類X的默認構造函數還是被調用了,且其調用順序還在類Xs的默認構造函數函數體代碼的前面。

由此可見,編譯器的行為是:如果類內含有一個或多個類成員對象(member class object),那麼該類的每一個構造函數必須調用每一個類成員的默認構造函數(按照成員聲明順序)。編譯器會擴張已存在的構造函數,在其中安插一些代碼,使得用戶的代碼被執行之前,先調用必要類成員的默認構造函數。

對於此時類Xs的構造函數可用以下偽代碼表示:
inline Xs::Xs()
{
    mX.X::X();
    cout << "Xs::Xs()" << endl;
    mN = 1;
} 

對於此類情況,編譯器合成默認的構造函數或向已有的默認構造函數中插入代碼的意義在於:使每個類成員變量都得到初始化。
3、類的基類帶有默認構造函數
這種情況是指:一個沒有任何構造函數的類派生自一個帶有默認構造函數的類。

例如,如下代碼所示:
class X
{
    public:
        X(){mData = 0; cout << "X::X()" << endl;}
        int mData;
};
class XX : public X
{
    public:
        int mN;
};
class XXX : public XX
{
};

int main()
{
    XX xx;
    cout << xx.mData << endl;
    cout << xx.mN << endl;

    XXX xxx;
    return 0;
}

類XX沒有任何構造函數,但是其基類X存在一個默認的構造函數,其運行結果如下:
\

從運行結果可以看出,編譯器合成的構造函數調用上一層基類的默認構造函數。對於一個後繼派生的class而言,這個合成的默認構造函數與一個被顯式提供的默認構造函數無異。

如果類的設計者提供了一個或多個構造函數(包括默認構造函數),但是在其提供的構造函數中並不顯式地調用其基類的構造函數,編譯器會如何處理呢?

為類XX加上兩個構造函數,修改main函數的測試代碼,如下所示:
class XX : public X
{
    public:
        XX()
        {
            mN = 1;
        }
        XX(int n)
        {
            mN = n;
        }
        int mN;
};

int main()
{
    XX xx1;
    XX xx2(2);
    return 0;
} 

類XX的構造函數都沒有顯式地調用其基類的構造函數。其運行結果如下:
\

從運行結果可以證明:編譯器會擴張派生類的每一個現在的構造函數,將要調用的所有必要的默認構造的程序代碼加插進去。注意:由於已經存在用戶自定義的構造函數,所以編譯器不再合成新的構造函數。

對於此類情況,編譯器合成默認的構造函數或向已有的默認構造函數中插入代碼的意義在於:確保類的基類子對象得到初始化。

4、類帶有virtual函數
這種情況是指:類聲明或繼承了一個或多個virtual函數。
例如,對於以下的代碼:
class X
{
    public:
        virtual ~X()
        {
            cout << "X::~X()" << endl;
        }
        virtual void print()
        {
            cout << "X::print()" << endl;
        }
        int mData;
};
class XX : public X
{
    public:
        virtual ~XX()
        {
            cout << "XX::~XX()" << endl;
        }
        virtual void print()
        {
            cout << "XX::print()" << endl;
        }
        int mN;
};
int main()
{
    X *x = new XX;
    x->print();
    delete x;
    return 0;
} 

類X有一個virtual函數print和一個virtual析構函數,其運行結果如下:
\
從運行結果可以看出,利用X的指針調用print函數,調用的是類XX的print函數,且在delete析構時也是調用了類XX的析構函數,先析構派生類再析構基類。

所以,由於虛函數的加入,編譯器會在編譯期間發生如下操作,以使虛函數機制發揮作用:
1)虛函數表(vtbl)會被編譯器產生而來,用於存放為類的virtual函數地址。
2)在每一個類的對象內,一個額外的指針成員會被編譯器合成出來,這個指針就是指向虛函數表的指針(vptr)。

此外,編譯器還會改寫虛函數的調用。例如,上述的main函數可能會被改寫成如下的偽代碼
int main()
{
     X *x = malloc(sizeof(XX));
    x->vptr = XX::vtbl;
    (x->vptr[1])(x); // 1為print函數在vtbl中的索引
    (x->vptr[0](x); // 0為析構函數在vtbl中的索引
    free(x);
    return 0;
} 

所以,編譯器合成的默認構造函數會為類安插一個vptr,並指定其初值,使其指向該類的虛函數表。若該類已經定義了構造函數,編譯器會為每個構造函數安插代碼來做同樣的事情(即安插vptr,並設定初值)。

對於此類情況,編譯器合成默認的構造函數或向已有的默認構造函數中插入代碼的意義在於:使virtual函數機制(多態)可以正確地生效。

5、類帶有一個virtual基類 這種情況是指類派生自一個繼承串鏈,其中有一個或更多的virtual基類。

virtual基類的實現方法 在不同的編譯器之間有極大的差異,但是每一種實現的共同點是必須使virtual基類在其每一個派生類對象中的位置,能夠在執行期間准備妥當。

例如,對於以下的代碼(並不是設計良好的代碼):
class X
{
    public:
        X() {mData = 100;}
        int mData;
};

class X1 : virtual public X
{
    public:
        X1() {mX1 = 101;}
        int mX1;
};

class X2 : virtual public X
{
    public:
        X2() {mX2 = 102;}
        int mX2;
};

class XX : public X1, public X2
{
    public:
        XX() {mXX = 103;}
        int mXX;
};

int main()
{
    cout << "sizeof(XX): " << sizeof(XX) << endl;
    XX xx;
    int *p = (int*) &xx;
    for (int i = 0; i < sizeof(XX) / sizeof(int); ++i, ++p)
    {
        cout << *p << endl;
    }
    return 0;
}

在main函數中遍歷並輸出對象的內容,其運行結果如下:

\


從結果可以看到,即像在所有的類中都沒有定義virtual函數,編譯器還是會為子類XX插入了兩個vptr,其中第一個vptr屬於X1,第二個vptr屬於X2。至於它有什麼作用,我還不清楚,但是可以肯定的是編譯器會為我們定義的構造函數安插必要的代碼來實現virtual基類的機制。若類沒有聲明任何的構造函數,編譯器必須合成一個默認構造函數來完成相同的操作。

注:若基類X中有virtual函數,則還會安插一個X::vptr,它的位置在100之前,即在基類X的成員變量之前。

6、總結
滿足上述4種情況之一或以上的類,若沒有聲明任何的構造函數,編譯器會為其合成一個默認的構造函數;若已聲明一個或多個構造函數,則編譯器會為每個構造函數安插一定的代碼來完成編譯必要的工作。這些被合成的構造函數稱為implicit nontrivial default constructors(隱式有效的默認構造函數)。

不滿足上述4種情況的類,而又沒有聲明任何構造函數,則該類擁有的是implicit trivial default constructors(隱式無效的默認構造函數),實際上不會被合成出來。

合成出來的默認構造函數,只會初化化基類子對象和成員類對象,其他的非static成員變量並不會被初始化。

此外,C++有兩個常見的誤解:
1)任何class如果沒有定義默認構造函數,就會被合成出來。
2)編譯器合成的默認構造函數,會顯式設定class內每一個成員數據的默認值。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved