十七、物件的構造

小胖鼠發表於2019-05-12

1、成員變數的初始值

#include <stdio.h>

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

Test gt;        // 全域性物件 全域性區,統一初始值為0

int main()
{
    printf("gt.i = %d
", gt.getI()); // 0
    printf("gt.j = %d
", gt.getJ()); // 0
    
    Test t1;    // 區域性物件 棧區
    
    printf("t1.i = %d
", t1.getI()); // 隨機值
    printf("t1.j = %d
", t1.getJ()); // 隨機值
    
    Test* pt = new Test;    // 類也是一個資料型別,堆區
    
    printf("pt->i = %d
", pt->getI()); // 堆區應該也是隨機值
    printf("pt->j = %d
", pt->getJ());
    
    delete pt;
    
    return 0;
}

2、物件的初始化

從程式設計的角度,物件只是變數,因此:

  • 在棧上建立物件時,成員變數初始為隨機值
  • 在堆上建立物件時,成員變數初始為隨機值
  • 在靜態儲存區建立物件時,成員變數初始為0

靜態儲存區包括了全域性變數和static修飾的區域性變數

需要解決的問題:使類的成員變數不管在哪個儲存區進行定義,它的初始值都是固定的。

物件的初始化:

  • 一般而言,物件都需要一個確定的初始狀態
  • 解決方案:

    • 在類中提供一個publicinitialize函式
    • 在物件建立後立即呼叫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
", gt.getI());
    printf("gt.j = %d
", gt.getJ());
    
    Test t1; 
    t1.initialize();  
    printf("t1.i = %d
", t1.getI());
    printf("t1.j = %d
", t1.getJ());
    
    Test* pt = new Test; 
    pt->initialize();
    printf("pt->i = %d
", pt->getI());
    printf("pt->j = %d
", pt->getJ());
    delete pt;

    return 0;
}

這種方式存在的問題:

  • initialize只是一個普通函式,必須顯示呼叫
  • 如果未呼叫initialize函式,執行結果是不確定的

這個初始化函式在物件建立之手就必須馬上呼叫,新建物件之手,需要人工手動新增initialize()函式,如果可以有一個函式在建立物件後自動呼叫,初始化成員變數就是極好的。

於是C++出現了建構函式來解決這個問題

3、建構函式

C++中可以定義與類名相同的特殊成員函式:建構函式

  • 建構函式沒有任何返回型別的宣告
  • 建構函式在物件定義時自動被呼叫
#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() {
        i = 1;
        j = 2;
    }
};

Test gt;

int main()
{
    //gt.initialize();
    printf("gt.i = %d, gt.j = %d
", gt.getI(), gt.getJ());

    Test t1;
    //t1.initialize();
    printf("t1.i = %d, t1.j = %d
", t1.getI(), t1.getJ());

    Test * pt = new Test;
    //pt->initialize();
    printf("pt->i = %d, pt->j = %d
", pt->getI(), pt->getJ());

    return 0;
}

4、帶引數的建構函式

建構函式和普通函式的差別:建構函式沒有返回值,名字和型別一樣

此時就只剩下引數可以討論:建構函式也可以帶引數

帶有引數的建構函式:

  • 建構函式可以根據需要定義引數
  • 一個類中可以存在多個過載的建構函式
  • 建構函式的過載遵循C++過載的規則
class Test
{
public:
    Test(int v)
    {
        // use v to initialize member
    }
};

注意:

物件定義和物件宣告不同:

  • 物件定義——申請物件的空間並呼叫建構函式
  • 物件宣告——告訴編譯器存在這樣一個物件
Test t;    // 定義物件並呼叫建構函式

int main()
{
    // 告訴編譯器存在名為t的Test物件
    extern Test t;
    
    return 0;
}

建構函式的自動呼叫

class Test {
public:
    Test(){}
    Test(int v) { }
    Test(const int& cv){}    // 拷貝建構函式
};    

Test t;            // 呼叫建構函式Test()
Test t1(1);        // 定義了一個物件t1,並呼叫帶有引數的建構函式,傳入引數為1,根據過載規則,建構函式為Test(int v)
Test t2 = 1;    // 用 1 來初始化物件t2,初始化需要藉助建構函式,根據過載規則,選擇Test(int v)
/*這裡的過程其實是:
首先呼叫建構函式Test(int v)建立一個臨時物件,引數為1;
然後就變成了用一個物件初始化另一個物件,此時應該是要呼叫拷貝建構函式進行成員變數值的複製,將這個臨時物件作為引數用來構造物件t2。
但是編譯器發現,可以通過過載的建構函式Test(int v)來直接初始化物件,而達到相同效果,所以將這條語句優化為Test t1(1)
    
*/

初始化和賦值:

#include <stdio.h>

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

int main()
{
    Test t;      // 呼叫 Test()
    Test t1(1);  // 呼叫 Test(int v)
    Test t2 = 2; // 呼叫 Test(int v)
    
    
    int i = 1;    // 用1來初始化變數i
    i = 2;        // 用2對變數i進行賦值
    
    t = t2;        // 用物件t2對物件t進行賦值
    
    int i(100);    // 用100來初始化i
    
    printf("i = %d
", i);
    
    return 0;
}

初始化和賦值是不一樣的,C語言中差別不大,C++中差別很大,因為物件的初始化要呼叫建構函式

建構函式的呼叫:

  • 一般情況下,建構函式在物件定義時被自動呼叫
  • 一些特殊情況下,需要手工呼叫建構函式

5、建立一個陣列

#include <stdio.h>

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

int main()
{
    Test ta[3];    // 呼叫3次Test() ,每個陣列元素中的m_value都按Test()來處理,不一定需要這樣的結果
    Test ta2[3] = {Test(), Test(1), Test(2)};    // 手工呼叫建構函式,3個陣列元素呼叫不同的建構函式
    
    for (int i = 0; i < 3; i++)
    {
        printf("ta[%d].getValue() = %d
", i, ta[i].getValue());    
        // 手工呼叫建構函式後,m_value初始化成不同值
    }
    
    Test t = Test(100);    // 建立物件之後,呼叫建構函式來初始化物件
    
    return 0;
}

需求:開發一個陣列類解決原生陣列的安全性問題

  • 提供函式獲取陣列長度
  • 提供函式獲取函式元素
  • 提供函式設定陣列元素
// 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.c
#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;
}

// main.c
#include <stdio.h>
#include "IntArray.h"
int main()
{
    IntArray a(5);    // 定義了一個物件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
", i, value);
        }
    }

    a.free();
    
    return 0;
}

6、特殊的建構函式

兩個特殊的建構函式

  • 無參建構函式:無引數的建構函式

    當類中沒有定義建構函式時,編譯器預設提供一個無參建構函式,並且其函式體為空

  • 拷貝建構函式:引數為const class_name&的建構函式

    當類中沒有定義拷貝建構函式時,編譯器預設提供一個拷貝建構函式,簡單的進行成員變數的值複製

如果類中已經有建構函式,編譯器就不會提供預設的建構函式

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
public:
    int getI()
    {
        return i;
    }
    int getJ()
    {
        return j;
    }
    
    /*
        Test(){};    // 編譯器會提供一個預設的無參建構函式
    */
    
     // 拷貝建構函式,這也是一個建構函式,寫了這個之後,編譯器就不會提供預設的無參建構函式,建立物件就會失敗,需要手工再建立一個無參建構函式
    Test(const Test& t)
    {
        i = t.i;
        j = t.j;
    }
    
    Test(){};
    
};

class T
{
/*
    空類:裡面至少有一個無參建構函式
*/
}


int main()
{
    Test t;    // 未定義建構函式時,依然能建立物件,因為編譯器提供了一個無參的建構函式

    int i = 2;
    int j = i;    // 用一個變數初始化另一個變數
    
    // 同理到 類 物件中
    Test t1;
    Test t2 = t1;    // 用物件初始化另一個物件時,編譯器提供預設的拷貝建構函式
        
    printf("t1.i = %d, t1.j = %d
", t1.getI(), t1.getJ());    // 隨機數
    printf("t2.i = %d, t2.j = %d
", t2.getI(), t2.getJ());    // 隨機數
    
    return 0;
}

拷貝建構函式的意義:

  • 相容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(const Test& t)
    {
        i = t.i;
        j = t.j;
        // p指標的值不直接複製
        p = new int;    // 指向一個新的堆空間的地址
        
        *p = *t.p;        // 將新的堆空間的值進行重新指定
                       // 將t.p指向的堆空間的值,複製給新的p指向記憶體
    }
    
    // 定義一個帶引數的建構函式
    Test(int v)
    {
        i = 1;
        j = 2;
        p = new int;    
        *p = v;
    }
    
    void free()
    {
        delete p;
    }
};

int main()
{
    Test t;        // 沒引數,沒有預設的無參建構函式,所以會建立失敗
    Test t1(3);    // 提供一個引數,建立物件
    /* 
    t1在建立的時候,p指標指向堆空間裡面的某個記憶體地址 
    用t1去初始化t2的時候,t2.p也應該指向堆空間的某個記憶體地址,並且和t1.p不是指向同一個記憶體地址
    這個程式只是將引數3傳給v,然後在堆空間裡存放這個值
    拷貝構造的時候也是做這件事,而不是直接將指標地址複製
    */
    Test t2 = t1;
    // 或者
    Test t2(t1);    // 將t1作為引數傳入拷貝建構函式
    Test t3 = 2;    // 以2為引數呼叫建構函式Test(2), 生成臨時物件去初始化物件t3,但是被編譯器優化為Test t3(2),不呼叫拷貝建構函式

    
    printf("t1.i = %d, t1.j = %d, *t1.p = %p
", t1.getI(), t1.getJ(), t1.getP());
    printf("t2.i = %d, t2.j = %d, *t2.p = %p
", t2.getI(), t2.getJ(), t2.getP());
    /* 
    採用預設的拷貝建構函式:
        t1.p = 0x8e6a008     指向堆空間的一個地址
           t2.p = 0x8e6a008    
        t1和t2的p指向了堆空間的同一個地址
    採用手工構造的拷貝建構函式:
        t1.p = 0x8528008
        t2.p = 0x8528018
        兩個物件的指標成員指向的堆空間的地址不一致了,狀態還是一致嗎?但是這個地址裡面儲存的int型別的資料確是相同的
    */
       printf("t1.i = %d, t1.j = %d, *t1.p = %d
", t1.getI(), t1.getJ(), *t1.getP());
    printf("t2.i = %d, t2.j = %d, *t2.p = %d
", t2.getI(), t2.getJ(), *t2.getP());
    // 列印值:*t1.p = 3, *t2.p = 3
    
    
    t1.free();
    t2.free();
    /* 
    這裡會報記憶體錯誤 
    0x8e6a008這個記憶體地址會被釋放兩次,t1.free()之後,t2.free()就不能再釋放了
    */
    
    return 0;
}

/*
    t1.i = 1, t1.j = 2, t1.p = 0x8528008
    t2.i = 1, t2.j = 2, t2.p = 0x8528018
       這就是物理狀態,就是物件佔據的記憶體中,他們的每個位元組是否相等
       t1和t2這兩個物件在記憶體中佔據的空間中的值是不一樣的
       
       從另一個角度看
       t1.i = 1, t1.j = 2, *t1.p = 3
    t2.i = 1, t2.j = 2, *t2.p = 3
    這就是邏輯狀態,t1和t2是一樣的,我們需要的僅僅是t1和t2的p指標,所指向的空間中的值是一樣的
*/

什麼時候使用深拷貝:物件中有成員指向了系統資源

  • 成員指向了動態記憶體空間
  • 成員開啟了外存中的檔案
  • 成員使用了系統中的網路埠
  • ……

一般性原則:自定義拷貝建構函式,必然需要實現深拷貝!!!

7、陣列類的改進

// IntArray.C
// 建構函式
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];
    }
}

8、小結

  • 每個物件在使用之前都應該初始化

    類的建構函式用於物件的初始化

    建構函式與類同名並且沒有返回值

    建構函式在定義時自動被呼叫

  • 建構函式可以根據需要定義引數

    建構函式之間可以存在過載關係

    建構函式遵循C++中過載函式的規則

    物件定義時會觸發建構函式的呼叫

    在一些情況下可以手動呼叫建構函式

  • C++編譯器會預設提供建構函式

    無參建構函式用於定義物件的預設初始狀態

    拷貝建構函式在建立物件時拷貝物件的狀態

    物件的拷貝有淺拷貝和深拷貝兩種方式

    • 淺拷貝使得物件的物理狀態相同
    • 深拷貝使得物件的邏輯狀態相同

相關文章