1. 程式人生 > >十七、物件的構造

十七、物件的構造

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\n", gt.getI()); // 0
    printf("gt.j = %d\n", gt.getJ()); // 0
    
    Test t1;    // 區域性物件 棧區
    
    printf("t1.i = %d\n", t1.getI()); // 隨機值
    printf("t1.j = %d\n", t1.getJ()); // 隨機值
    
    Test* pt = new Test;    // 類也是一個數據型別,堆區
    
    printf("pt->i = %d\n", pt->getI()); // 堆區應該也是隨機值
    printf("pt->j = %d\n", 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\n", gt.getI());
    printf("gt.j = %d\n", gt.getJ());
    
    Test t1; 
    t1.initialize();  
    printf("t1.i = %d\n", t1.getI());
    printf("t1.j = %d\n", t1.getJ());
    
    Test* pt = new Test; 
    pt->initialize();
    printf("pt->i = %d\n", pt->getI());
    printf("pt->j = %d\n", 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\n", gt.getI(), gt.getJ());

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

    Test * pt = new Test;
    //pt->initialize();
    printf("pt->i = %d, pt->j = %d\n", 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()\n");
    }
    Test(int v) 
    { 
        printf("Test(int v), v = %d\n", 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\n", i);
    
    return 0;
}

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

建構函式的呼叫:

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

5、建立一個數組

#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;
    }
    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\n", 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\n", i, value);
        }
    }

    a.free();
    
    return 0;
}

6、小結

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

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

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

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

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