對象的構造(十四)

        我們在 C 語言中,每個變量都有其初始值。那麼問題來了,對象中成員變量的初始值是多少呢?從設計的角度來看,對象只是變量,因此:在棧上創建對象時,成員變量初始爲隨機值;在堆上創建對象時,成員變量初始爲隨機值;在靜態存儲區創建對象時,成員變量初識爲 0 值。

        下來我們以代碼爲例進行驗證,代碼如下

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI() { return i; }
    int getJ() { return j; }
};

Test gt;

int main()
{
    printf("gt.i = %d\n", gt.getI());
    printf("gt.j = %d\n", gt.getJ());
    
    Test at;
    
    printf("at.i = %d\n", at.getI());
    printf("at.j = %d\n", at.getJ());
    
    Test* pt = new Test;
    
    printf("pt->i = %d\n", pt->getI());
    printf("pt->j = %d\n", pt->getJ());
    
    return 0;
}

        gt 對象是在靜態存儲區創建的,所以 gt.i 和 gt.j 應該都爲 0,;at 對象是在棧上創建的,所以 at.i 和 at.j 應該都爲隨機值;pt 對象是在堆上創建的,所以 pt->i 和 pt->j 應該也爲隨機值。我們來編譯下,看看是否如我們所分析的那樣呢?

圖片.png

        我們看到前面兩個如我們所分析的那樣,最後一個不一樣。我們再來看看BCC編譯器呢

圖片.png

        我們看到BCC編譯器是如我們所分析的那樣。所以我們不能依賴於某種編譯器的特性。

        在生活中的對象都是在初始化後上市的,初識狀態(出廠設置)是對象普遍存在的一個狀態。那麼程序中如何對一個對象進行初始化呢?一般而言,對象都需要一個確定的初識狀態。解決方案便是在類中提供一個 public 的 initialize 函數,對象創建後立即調用 initialize 函數進行初始化。下來我們以代碼爲例進行分析,在上面代碼基礎上加上 initialize 函數

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI() { return i; }
    int getJ() { return j; }
    void initialize()
    {
        i = 1;
        j = 2;
    }
};

Test gt;

int main()
{
    gt.initialize();
    
    printf("gt.i = %d\n", gt.getI());
    printf("gt.j = %d\n", gt.getJ());
    
    Test at;
    at.initialize();
    
    printf("at.i = %d\n", at.getI());
    printf("at.j = %d\n", at.getJ());
    
    Test* pt = new Test;
    pt->initialize();
    
    printf("pt->i = %d\n", pt->getI());
    printf("pt->j = %d\n", pt->getJ());
    
    return 0;
}

        我們編譯,看看結果是否初始化好呢

圖片.png

        我們看到已經全部初始化爲按照我們所想要的狀態了。但是這個就存在一個問題了,initialize 只是一個普通的函數,必須顯示調用才行。如果爲調用 initialize 函數的話,結果是不確定的。如果我們忘記在 at 對象中調用 initialize 函數,編譯結果如下

圖片.png

        那麼這時問題來了,我們該如何解決這個問題呢?在 C++ 中介意定義與類名相同的特殊成員函數,這種特殊的成員函數叫做構造函數。注意:構造函數沒有返回類型的聲明;構造函數在對象定義時自動被調用。那麼這時我們就可以將上面的程序改爲這樣

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI() { return i; }
    int getJ() { return j; }
    Test()
    {
        printf("Test() Begin\n");
        
        i = 1;
        j = 2;
        
        printf("Test() End\n");
    }
};

Test gt;

int main()
{
    printf("gt.i = %d\n", gt.getI());
    printf("gt.j = %d\n", gt.getJ());
    
    Test at;
    
    printf("at.i = %d\n", at.getI());
    printf("at.j = %d\n", at.getJ());
    
    Test* pt = new Test;
    
    printf("pt->i = %d\n", pt->getI());
    printf("pt->j = %d\n", pt->getJ());
    
    return 0;
}

        我們編譯後結果如下

圖片.png

        我們這樣是不是就方便很多呢?那肯定了。我們可以明顯看到定義了三個對象後,調用了三次構造函數。那麼我們既然知道了有構造函數這一類的函數,它是否能像一般函數那樣進行帶參數呢?構造函數可以根據需要定義參數;一個類中可以存在多個重載的構造函數;構造函數的重載遵循 C++ 重載的規則。我們之前說過定義和聲明不同,在對象這塊也同樣適用。對象定義和對象聲明時不同的:對象定義 -- 申請對象的空間並調用構造函數;對象聲明 -- 告訴編譯器存在這樣一個對象。下來我們以代碼爲例進行分析

#include <stdio.h>

class Test
{
public:
    Test()
    {
        printf("Test()\n");
    }
    Test(int v)
    {
        printf("Test(int v), v = %d\n", v);
    }
};

int main()
{
    Test t1;         // 調用 Test()
    Test t2(1);      // 調用 Test(int v)
    Test t3 = 2;     // 調用 Test(int v)
    
    int i(10);
    
    printf("i = %d\n", i);
    
    return 0;
}

        我們看到第 18 行的 t1 對象的構造函數肯定調用了 Test(),第 19 和 20 行則是調用了 Test(int v);在 C 語言中還有 int i(10) 這種寫法,我們看看編譯是否會通過?

圖片.png

        我們看到編譯通過,並且如我們所分析的那樣。那麼構造函數的調用是否有什麼規則呢?在一般情況下,構造函數在對象定義時被自動調用,一些特殊情況下,需要手工調用構造函數。我們如何利用構造函數來創建一個數組呢?

#include <stdio.h>

class Test
{
private:
    int m_value;
public:
    Test()
    {
        printf("Test()\n");
        
        m_value = 0;
    }
    Test(int v)
    {
        printf("Test(int v), v = %d\n", v);
        
        m_value = v;
    }
    
    int getValue()
    {
        return m_value;
    }
};

int main()
{
    Test ta[3] = {Test(), Test(1), Test(2)};
    
    for(int i=0; i<3; i++)
    {
        printf("ta[%d].getValue() = %d\n", i, ta[i].getValue());
    }
    
    Test t = Test(10);
    
    printf("t.getValue() = %d\n", t.getValue());
    
    return 0;
}

        我們首先來分析下,數組第一個成員調用的構造函數應該是 Test(),後面兩個成員調用的是 Test(int v) 函數,並打印出相應的值。最後定義的對象 t,它會打印出構造函數和得到的值都爲 10,我們來看看編譯結果

圖片.png

        下來我們來開發一個數組類解決原生數組的安全性問題:提供函數獲取數組長度;提供函數獲取數組元素;提供函數設置數組元素。我們來看看它是怎麼實現的


IntArray.h 源碼

#ifndef _INTARRAY_H_
#define _INTARRAY_H_

class IntArray
{
private:
    int m_length;
    int* m_pointer;
public:
    IntArray(int len);
    int length();
    bool get(int index, int& value);
    bool set(int index, int value);
    void free();
};

#endif


IntArray.cpp 源碼

#include "IntArray.h"

IntArray::IntArray(int len)
{
    m_pointer = new int[len];
    
    for(int i=0; i<len; i++)
    {
        m_pointer[i] = 0;
    }
    
    m_length = len;
}

int IntArray::length()
{
    return m_length;
}

bool IntArray::get(int index, int& value)
{
    bool ret = (0 <= index) && (index <= length());
    
    if( ret )
    {
        value = m_pointer[index];
    }
    
    return ret;
}

bool IntArray::set(int index, int value)
{
    bool ret = (0 <= index) && (index <= length());
    
    if( ret )
    {
        m_pointer[index] = value;
    }
    
    return ret;
}

void IntArray::free()
{
    delete[] m_pointer;
}


test.cpp 源碼

#include <stdio.h>
#include "IntArray.h"

int main()
{
    IntArray a(5);
    
    for(int i=0; i<a.length(); i++)
    {
        a.set(i, i+1);
    }
    
    for(int i=0; i<a.length(); i++)
    {
        int value = 0;
        
        if( a.get(i, value) )
        {
            printf("a[%d] = %d\n", i, value);
        }
    }
    
    a.free();
    
    return 0;
}

        我們編譯後得到如下結果

圖片.png

        下來我們來看看特殊的構造函數:無參構造函數和拷貝構造函數。無參構造函數顧名思義就是沒有參數的構造函數,而拷貝構造函數則是參數爲 const class_name& 的構造函數。那麼這兩類構造函數有什麼區別呢?無參構造函函數是當類中沒有定義構造函數時,編譯器默認提供一個無參構造函數,並且其函數體爲空;拷貝構造函數是當類中沒有定義拷貝構造函數時,編譯器默認提供一個拷貝構造函數,簡單的進行成員變量的值複製。下來我們以代碼爲例進行分析

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI()
    {
        return i;
    }
    
    int getJ()
    {
        return j;
    }
/*    
    Test()
    {
        printf("Test()\n");
    }
    
    Test(const Test& t)
    {
        printf("Test(const Test& t)\n");
        i = t.i;
        j = t.j;
    }
*/
};

int main()
{
    Test t1;
    Test t2 = t1;
    
    printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ());
    printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ());
    
    return 0;
}

        我們先將自己提供的無參構造函數和拷貝構造函數註釋掉,編譯下,看編譯器是否提供默認的構造函數,是否可以通過

圖片.png

        我們看到編譯是通過的,也就是說,編譯器通過了默認的構造函數。我們再來自己提供呢,看看是否會發生衝突

圖片.png

        我們看到打印出了自己定義的語句,證明它是調用了我們自己寫的構造函數。那麼這個拷貝構造函數的意義在哪呢?一是兼容 C 語言的初始化方式,二是初始化行爲能夠符合預期的邏輯。那麼這塊就牽扯到是淺拷貝還是深拷貝。淺拷貝是拷貝後對象的物理狀態相同,深拷貝是拷貝後對象的邏輯狀態相同。注意:編譯器提供的拷貝構造函數只進行淺拷貝!

        下來我們以實例代碼看看對象的初始化是怎樣進行的

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
    int* p;
public:
    int getI()
    {
        return i;
    }
    
    int getJ()
    {
        return j;
    }
    
    int* getP()
    {
        return p;
    }
    
    Test(int v)
    {
        i = 1;
        j = 2;
        p = new int;
        
        *p = v;
    }
    
    Test(const Test& t)
    {
        i = t.i;
        j = t.j;
        p = new int;
        
        *p = *t.p;
    }
    
    void free()
    {
        delete p;
    }
};

int main()
{
    Test t1(3);
    Test t2(t1);
    
    printf("t1.i = %d, t1.j = %d, *t1.p = %d\n", t1.getI(), t1.getJ(), *t1.getP());
    printf("t2.i = %d, t2.j = %d, *t2.p = %d\n", t2.getI(), t2.getJ(), *t2.getP());
    
    t1.free();
    t2.free();
    
    return 0;
}

        我們看看 t1 應該進行的是淺拷貝,t2 應該進行的是深拷貝。我們看看編譯結果

圖片.png

        我們如果只有淺拷貝,沒有深拷貝的話,看看結果會是怎樣的,將第 34 - 41 行的代碼註釋掉,將第 54 和 55 行的打印 *p 的值改爲打印 p 的地址。圖片.png

        我們看到它運行的時候報段錯誤了,t1.p 和 t2.p 指向了同一個地址。我們看看它是怎樣進行的

圖片.png

        我們看到將同一個地址釋放兩次肯定是會出問題的,這時我們就需要進行深拷貝了。那麼我們就要考慮到底什麼時候需要進行深拷貝?當對象中有成員指代了系統中的資源時,如:成員指向了動態內存空間,成員打開了外存中的文件,成員使用了系統中的網絡端口...

        我們在實現拷貝構造函數這塊有個一般性的原則,自定義拷貝構造函數時,必須要實現深拷貝。那麼我們再來優化下之前的數組類


IntArray.h 源碼

#ifndef _INTARRAY_H_
#define _INTARRAY_H_

class IntArray
{
private:
    int m_length;
    int* m_pointer;
public:
    IntArray(int len);
    IntArray(const IntArray& obj);
    int length();
    bool get(int index, int& value);
    bool set(int index, int value);
    void free();
};

#endif

IntArray.cpp 源碼

#include "IntArray.h"

IntArray::IntArray(int len)
{
    m_pointer = new int[len];
    
    for(int i=0; i<len; i++)
    {
        m_pointer[i] = 0;
    }
    
    m_length = len;
}

IntArray::IntArray(const IntArray& obj)
{
    m_length = obj.m_length;
    
    m_pointer = new int[obj.m_length];
    
    for(int i=0; i<obj.m_length; i++)
    {
        m_pointer[i] = obj.m_pointer[i];
    }
}

int IntArray::length()
{
    return m_length;
}

bool IntArray::get(int index, int& value)
{
    bool ret = (0 <= index) && (index <= length());
    
    if( ret )
    {
        value = m_pointer[index];
    }
    
    return ret;
}

bool IntArray::set(int index, int value)
{
    bool ret = (0 <= index) && (index <= length());
    
    if( ret )
    {
        m_pointer[index] = value;
    }
    
    return ret;
}

void IntArray::free()
{
    delete[] m_pointer;
}


test.cpp 源碼

#include <stdio.h>
#include "IntArray.h"

int main()
{
    IntArray a(5);
    
    for(int i=0; i<5; i++)
    {
        a.set(i, i+1);
    }
    
    for(int i=0; i<a.length(); i++)
    {
        int value = 0;
        
        if( a.get(i, value) )
        {
            printf("a[%d] = %d\n", i, value);
        }
    }
    
    printf("\n");
    
    IntArray b = a;
    
    for(int i=0; i<b.length(); i++)
    {
        int value = 0;
        
        if( b.get(i, value) )
        {
            printf("b[%d] = %d\n", i, value);
        }
    }
    
    a.free();
    b.free();
    
    return 0;
}

        我們看看編譯結果是否如我們代碼所寫的那樣,創建數組並初始化。用數組 a 初始化數組 b。

圖片.png

        通過對對象的構造的學習,總結如下:1、每個對象在使用之前都應該初始化;2、類的構造函數用於對象的初始化,構造函數與類同名並且沒有返回值;3、構造函數在對象定義時被自動調用,構造函數可以根據需要定義參數;4、構造函數之間可以存在重載關係,並且構造函數遵循 C++ 中重載函數的規則;5、對象定義時會觸發構造函數的調用,在一些情況下可以手動調用構造函數;6、C++ 編譯器會默認提供構造函數;7、無參構造函數用於定義對象的默認初識狀態,拷貝構造函數在創建對象時拷貝對象的狀態;8、對象的拷貝有淺拷貝和深拷貝兩種方式:淺拷貝使得對象的物理狀態相同,而深拷貝則使得對象的邏輯狀態相同。


        歡迎大家一起來學習 C++ 語言,可以加我QQ:243343083

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