十7、對象的構造

一、成員變量的初始值

#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;
}

二、對象的初始化

從程序設計的角度,對象只是變量,所以:c++

  • 在棧上建立對象時,成員變量初始爲隨機值
  • 在堆上建立對象時,成員變量初始爲隨機值
  • 在靜態存儲區建立對象時,成員變量初始爲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++出現了構造函數來解決這個問題設計

三、構造函數

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;
}

四、帶參數的構造函數

構造函數和普通函數的差異:構造函數沒有返回值,名字和類型同樣code

此時就只剩下參數能夠討論:構造函數也能夠帶參數對象

帶有參數的構造函數:

  • 構造函數能夠根據須要定義參數
  • 一個類中能夠存在多個重載的構造函數
  • 構造函數的重載遵循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++中差異很大,由於對象的初始化要調用構造函數

構造函數的調用:

  • 通常狀況下,構造函數在對象定義時被自動調用
  • 一些特殊狀況下,須要手工調用構造函數

五、建立一個數組

#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;
}

六、特殊的構造函數

兩個特殊的構造函數

  • 無參構造函數:無參數的構造函數

    當類中沒有定義構造函數時,編譯器默認提供一個無參構造函數,而且其函數體爲空

  • 拷貝構造函數:參數爲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\n", t1.getI(), t1.getJ());    // 隨機數
    printf("t2.i = %d, t2.j = %d\n", 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\n", t1.getI(), t1.getJ(), t1.getP());
    printf("t2.i = %d, t2.j = %d, *t2.p = %p\n", 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\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.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指針,所指向的空間中的值是同樣的
*/

何時使用深拷貝:對象中有成員指向了系統資源

  • 成員指向了動態內存空間
  • 成員打開了外存中的文件
  • 成員使用了系統中的網絡端口
  • ……
通常性原則:自定義拷貝構造函數,必然須要實現深拷貝!!!

七、數組類的改進

// 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];
    }
}

八、小結

  • 每一個對象在使用以前都應該初始化

    類的構造函數用於對象的初始化

    構造函數與類同名而且沒有返回值

    構造函數在定義時自動被調用

  • 構造函數能夠根據須要定義參數

    構造函數之間能夠存在重載關係

    構造函數遵循C++中重載函數的規則

    對象定義時會觸發構造函數的調用

    在一些狀況下能夠手動調用構造函數

  • C++編譯器會默認提供構造函數

    無參構造函數用於定義對象的默認初始狀態

    拷貝構造函數在建立對象時拷貝對象的狀態

    對象的拷貝有淺拷貝和深拷貝兩種方式

    • 淺拷貝使得對象的物理狀態相同
    • 深拷貝使得對象的邏輯狀態相同
相關文章
相關標籤/搜索