面向對象編程(C++篇2)——構造

1. 引述

在C++中,學習類的第一課往往就是構造函數。根據構造函數的定義,構造函數式是用於初始化類對象的數據成員的。無論何時,只要類被創建,就會執行構造函數:

class ImageEx
{
public:
    ImageEx()
    {
        cout << "Execute the constructor!" << endl;
    }
};

int main()
{
    ImageEx imageEx;    
    return 0;
}

那麼問題來了,爲什麼要有構造函數?

2. 詳述

2.1. 數據類型初始化

正如上一篇文章《面向對象編程(C++篇1)——引言》中提到的那樣:類是抽象的自定義數據類型。對於C++的內置數據類型,我們可以採用如下方式進行初始化:

double price = 109.99;

這種初始化行爲很像賦值操作,但是初始化與賦值是兩種概念:初始化的含義是創建變量的時候賦予其一個初始值,而賦值的含義則是把對象的當前值擦除,以一個新的值來代替。實際上,我們同樣可以使用類似構造函數一樣的方式初始化內置數據類型:

double price(109.99);

那麼,我們在定義變量的時候不進行初始化會怎麼樣呢?答案是會進行默認初始化(其實不太準確,在某些情況下,會不被初始化,進而產生未定義的行爲,是非常危險的):

double price;
price = 109.99;

在C++中,一個合理的原則是:變量類型定義時初始化。這個原則不僅可以避免未初始化可能產生的未定義行爲,還節省了性能:避免定義(默認初始化)後再進行賦值操作。

2.2. 類初始化

可能你會認爲,先定義(默認初始化)之後再進行賦值,對性能影響不大。這句話對於C#、Java、JavaScript這樣的語言來說是成立的,它們的應用場景很多時候可以不用關心這個(性能場景則不一定)。而對於C++這樣的面向底層的語言來說,追求的是"零成本抽象(zero overhead abstraction)"的設計原則,只是簡單的數據結構影響當然不太,但是對於一個非常複雜的數據類型,則可能存在不可忽視的性能開銷。

可以爲一個類的數據成員提供一個類內初始值:

class ImageEx
{
    int imgWidth = 0;
    int imgHeight = 0;
    int bandCount = 0;
};

類的數據成員如果不進行初始化,那麼就會如前所述,進行默認初始化:

class ImageEx
{
public:

    void Print()
    {
        cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
        for (int i = 0; i < 10; i++)
        {
            printf("%d\t", data[i]);
        }
    }

private:
    int imgWidth;
    int imgHeight;
    int bandCount;

    unsigned char data[10];
};

int main()
{
    ImageEx imageEx;
    imageEx.Print();

    return 0;
}

運行結果:
figure1

默認初始化的未定義行爲當然不是我們想要的,於是我們給他加一個初始化函數:

class ImageEx
{
public:

    void Init()
    {
        imgWidth = 200;
        imgHeight = 100;
        bandCount = 3;
        memset(data, 0, 10 * sizeof(unsigned char));
    }

    void Print()
    {
        cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
        for (int i = 0; i < 10; i++)
        {
            printf("%d\t", data[i]);
        }
        cout << endl;
    }

private:
    int imgWidth;
    int imgHeight;
    int bandCount;

    unsigned char data[10];
};

int main()
{
    ImageEx imageEx;
    imageEx.Print();
    imageEx.Init();
    imageEx.Print();

    return 0;
}

運行結果:
figure2

從上例可以發現,如果我們自己給類的數據成員進行初始化函數,其實類的數據成員早就進行了一次默認初始化操作,這個初始化函數其實是一次額外的賦值。以這個類對象中的數組數據成員data爲例,假使這個數組的容量很大,其額外的一次賦值操作對於底層來說,是不可忽略的性能開銷。

那麼使用構造函數的原因就很容易理解了,構造函數就是實現當類定義時初始化數據成員的,這樣可以避免額外的初始化性能開銷:

class ImageEx
{
public:
    ImageEx()
    {
        cout << "Default initialization!" << endl;
        Print();
        cout << "Execute the constructor!" << endl;
        Init();
    }


    void Print()
    {
        cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
        for (int i = 0; i < 10; i++)
        {
            printf("%d\t", data[i]);
        }
        cout << endl;
    }

private:
    void Init()
    {
        imgWidth = 200;
        imgHeight = 100;
        bandCount = 3;
        memset(data, 0, 10 * sizeof(unsigned char));
    }

    int imgWidth;
    int imgHeight;
    int bandCount;

    unsigned char data[10];
};

int main()
{
    ImageEx imageEx;
    imageEx.Print();

    return 0;
}

進一步探究,構造函數本質是個函數,函數是由語句組成,已經定義的數據類型只能賦值初始化,而無法再進行構造。也就是說,在調用構造函數之前,數據成員還是已經默認初始化了:

figure3

因此,初始化最好的實現是使用構造函數的初始值列表:

class ImageEx
{
public:
    ImageEx() :
        imgWidth(200),
        imgHeight(100),
        bandCount(3),
        data{ 0, 1, 2 }
    {
        cout << "Execute the constructor!" << endl;
    }


    void Print()
    {
        cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
        for (int i = 0; i < 10; i++)
        {
            printf("%d\t", data[i]);
        }
        cout << endl;
    }

private:
    int imgWidth;
    int imgHeight;
    int bandCount;

    unsigned char data[10];
};

int main()
{
    ImageEx imageEx;
    imageEx.Print();

    return 0;
}

運行結果:
figure4

通過這種實現,類中所有的數據成員都在定義時初始化,從而使類對象也實現了定義時初始化;避免了先定義後賦值的性能開銷,體現了C++"零成本抽象(zero overhead abstraction)"的設計哲學。

上一篇
目錄
下一篇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章