1. 程式人生 > >C++語言(03)——對象的構造

C++語言(03)——對象的構造

image char* tex 作用 goto語句 能夠 ros 部分 printf

對象的構造(上)

成員變量的初始值

(1)從程序設計的角度來看,對象只是變量,定義對象就是定義變量,所以:
在棧上創建對象時,成員變量初始值為隨機值
在堆上創建對象時,成員變量初始值為隨機值
在靜態數據區上創建對象時,成員變量初始值為0
(2)全局變量和static修飾的局部變量存儲在靜態數據區,沒有顯式初始化其值為0(bss/ZI段)


/**
從程序設計的角度來看,對象只是變量,定義對象就是定義變量,所以:
    在棧上創建對象時,成員變量初始值為隨機值
    在堆上創建對象時,成員變量初始值為隨機值
    在靜態數據區上創建對象時,成員變量初始值為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 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;
}

對象的初始化

(1)生活中的對象都是初始化之後上市的,如手機,電腦等,我們希望程序中的對象也可以初始化為固定值

initialize函數

(1)在類中提供一個public的initialize函數
(2)函數中手工對類的成員進行顯式初始化
(3)對象創建後需要立即調用initialize函數進行初始化
(4)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 t1;

    //t1.initialize();

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

    t1.initialize();

    Test* pt = new Test;

    pt->initialize();

    printf("pt->i = %d\n", pt->getI());
    printf("pt->j = %d\n", pt->getJ());

    delete pt;

    return 0;
}

構造函數

(1)C++中可以定義與類同名的構造函數
(2)構造函數:與類同名、沒有任何的返回值類型、在對象定義時會被自動調用

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

對象的構造(中)

構造函數重載

(1)構造函數可以根據需要定義參數
(2)一個類中可以存在多個重載的構造函數構造函數的重載遵循C++重載的規則

/**
(1)構造函數可以根據需要定義參數
(2)一個類中可以存在多個重載的構造函數構造函數的重載遵循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 t;      // 調用 Test()
    Test t1(1);  // 調用 Test(int v)
    //註意此處的Test t1(1),因為1是int類型數據,所以是要告訴編譯器這個對象初始化時調用的構造函數
    //參數為int而且只有一個參數

    Test t2 = 2; // 調用 Test(int v)

    //C++中支持這樣初始化
    int i(100);

    printf("i = %d\n", i);

    return 0;
}

(3)構造函數在對象定義時會被自動調用,此外我們可以手工調用構造函數

#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 ta[3] = {Test(), Test(1), Test(2)};  //我們可以手工調用構造函數

    for(int i=0; i<3; i++)
    {
        printf("ta[%d].getValue() = %d\n", i , ta[i].getValue());
    }

/*  int i(100);     //C++中是可以這樣初始化的,等價於int i = 100;
    printf("i = %d.\n", i);

    Test t = Test(100);

    printf("t.getValue() = %d\n", t.getValue());
 */   
    return 0;
}

註意:
對象的聲明和定義不同,對象定義:申請對象的空間並調用構造函數
對象聲明:告訴編譯器有這樣一個對象

對象的構造(下)

特殊的構造函數

無參構造函數:

(1)就是沒有參數的構造函數
(2)當類中沒有定義構造函數時(拷貝構造函數也是構造函數),編譯器會默認提供一個無參構造函數,其函數體為空

拷貝構造函數

(1)參數為const class_name&的構造函數
(2)當類中沒有定義拷貝構造函數時,編譯器會默認提供一個拷貝構造函數,簡單的進行成員變量的復制(淺拷貝)
(3)拷貝構造函數的意義:兼容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;
    }
    int getPP()
    {
        return *p;
    }
    /*Test(const Test& t)   //編譯器提供的默認拷貝構造函數
    {
        i = t.i;
        j = t.j;
    }
    Test()                  //編譯器提供的默認無參構造函數
    {                       //函數體為空
    }*/
     Test(int v)
    {
        i = 1;
        j = 2;
        p = new int;

        *p = v;
    }
    void free()
    {
        delete p;
    }
};

int main()
{
    Test t1(1);
    Test t2 = t1;       //默認進行淺拷貝

    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(), t1.getP());

    //test
    printf("t1.*p = %d.\n", t1.getPP());
    printf("t2.*p = %d.\n", t2.getPP());
    //結果表明t1(1),把1作為參數,傳給了構造函數

    t1.free();
    //t2.free();    // double free or corruption (fasttop): 0x09441008

    return 0;
}

淺拷貝與深拷貝

(1)淺拷貝:拷貝後的物理狀態相同
(2)深拷貝:拷貝後的邏輯狀態相同
(3)編譯器默認提供的拷貝構造函數只進行淺拷貝
什麽時候需要深拷貝?
對象中有成員使用了系統資源(成員指向了動態內存空間、成員打開了外存中的文件、成員使用了系統中的網絡端口)
註意:
(1)調用淺拷貝構造函數進行初始化,初始化的兩個變量不但參數相同,而且共用同一塊內存,在兩次釋放內存時就會出錯
(2)工程中自定義拷貝構造函數時,必然要實現深拷貝(為新的對象重新分配資源)

#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 = new int;        //必須有這一步,重新分配資源

        *p = *t.p;          //註意這裏是*t.p
    }
    Test(int v)
    {
        i = 1;
        j = 2;
        p = new int;

        *p = v;
    }
    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;
}

初始化列表的使用

類中的const成員

(1)在類中可以定義const成員,const成員會被分配空間,存儲位置取決於其對象定義在哪裏
(2)類中的const是只讀變量
(3)在類中不能直接對const成員進行初始化,只能在初始化列表中指定初始值


//在類中不能直接對const成員進行初始化,只能在初始化列表中指定初始值
#include <stdio.h>

class Test
{
private:
    const int ci;       //只讀變量
public:
    Test() : ci(1)
    {
        // ci = 10;
    }
    int getCI() 
    { 
        return ci; 
    }
};

int main()
{
    Test t;

    printf("t.ci = %d\n", t.getCI());

    return 0;
}

初始化列表

(1)C++中提供了初始化列表對成員進行初始化
(2)語法規則
ClassName::ClassName() //構造函數
: m1(v1),m2(v2, v3),m3(v1) //初始化列表
{
//構造函數函數體
}
(3)註意事項:
成員的初始化順序與成員的聲明順序相同,與初始化列表中的順序無關
初始化列表先於構造函數的函數體執行

/*
C++中提供了初始化列表對成員進行初始化
成員的初始化順序與成員的聲明順序相同,與初始化列表中的順序無關
初始化列表先於構造函數的函數體執行
*/

#include <stdio.h>

class Value
{
private:
    int mi;
public:
    Value(int i)
    {
        printf("i = %d\n", i);
        mi = i;
    }
    int getI()
    {
        return mi;
    }
};

class Test
{
private:
    Value m2;
    Value m3;
    Value m1;
public:
    Test() : m1(1), m2(2), m3(3)    //成員的初始化順序與成員的聲明順序相同,與初始化列表中的順序無關
    {
        printf("Test::Test()\n");
    }
};

int main()
{
    Test t;     //初始化列表先於構造函數的函數體執行

    return 0;
}

對象的構造順序

(1)局部變量的構造順序依賴於程序的執行流,所以開發中要避免使用goto語句)(破壞程序的執行流)
(2)堆對象的構造順序依賴於new的使用順序
(3)全局對象的構造順序是不確定的,不同的編譯器使用不同的規則確定構造順序,所以要盡量避免全局對象


//局部變量的構造順序依賴於程序的執行流,所以開發中要避免使用goto語句(破壞程序的執行流)
#include <stdio.h>

class Test
{
private:
    int mi;
public:
    Test(int i)
    {
        mi = i;
        printf("Test(int i): %d\n", mi);
    }
    Test(const Test& obj)
    {
        mi = obj.mi;
        printf("Test(const Test& obj): %d\n", mi);
    }
    int getMi()
    {
        return mi;
    }
};

int main()
{
    int i = 0;
    Test a1 = i; // Test(int i): 0

    while( i < 3 )
    {
        Test a2 = ++i; // Test(int i): 1, 2, 3
    }
goto End;       
        Test a(100);    // crosses initialization of ‘Test a’編譯報錯,但是在vc10中編譯時ok的
End:
    printf("a.mi = %d\n", a.getMi());   //此處的訪問必然導致bug
    return 0;
}

對象的銷毀

(1)生活中對象都是初始化後才上市的,對象被銷毀前會做一些清理工作

程序中如何銷毀一個對象

方案1:
提供一個public的free函數,
(1)當對象不再需要時立即調用free函數進行清理
(2)free只是一個普通的函數,必須顯示的調用
(3)對象銷毀之前沒有做清理,很可能造成資源泄漏
方案2:
析構函數
(1)C++中可以定義一個特殊的清理函數,析構函數,功能和構造函數相反
(2)析構函數在對象銷毀時被自動調用
(3)析構函數沒有返回值也沒有參數(表明析構函數在一個類中是唯一的,不可能重載)
(4)語法:
~ClassName()
(5)一般當類中自定義了構造函數,並且函數中使用了系統資源,則需要定義析構函數,釋放系統資源,防止內存泄漏

#include <stdio.h>

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

int main()
{
    Test t(1);

    Test* pt = new Test(2);

    delete pt;

    return 0;
}

神秘的臨時對象

(1)直接調用構造函數將產生一個臨時對象,臨時對象的生命周期只有一條語句的時間,臨時對象的作用域只在一條語句中
(2)臨時對象是C++中值得警惕的灰色地帶,是性能的瓶頸,也是bug的來源之一

/**直接調用構造函數將產生一個臨時對象,臨時對象的生命周期只有一條語句的時間,臨時對象的作用域只在一條語句中**/
#include <stdio.h>

class Test 
{
    int mi;
public:
    Test(int i) 
    {
        mi = i;
    }
    Test() 
    {
        Test(0);    //直接調用構造函數將產生一個臨時對象,臨時對象的作用域只有一行代碼。
                    // 所以此處就相當於空
    }
    void print() 
    {
        printf("mi = %d\n", mi);
    }
};

int main()
{
    Test t;

    t.print();

    return 0;
}

(3)實際工程開發中需要人為的避開臨時對象
(4)現代C++編譯器會盡力避開臨時對象
思考:如何解決構造函數的代碼復用問題?
方案是提供一個private的init函數,然後在構造函數中去調用它.


/**
思考如何解決構造函數的代碼復用問題?
    方案是提供一個private的init函數,然後在構造函數中去調用它
**/

#include <stdio.h>

class Test {
    int mi;

    void init(int i)    //提供一個private的init函數,然後在構造函數中去調用它
    {
        mi = i;
    }
public:
    Test(int i) {
        init(i);
    }
    Test() {
        init(0);
    }
    void print() {
        printf("mi = %d\n", mi);
    }
};

int main()
{
    Test t;

    t.print();

    return 0;
}

二階構造模式

回顧構造函數的特點:
與類同名,沒有返回值,在對象創建時被動調用,用於對象的初始化

關於構造的幾個問題:

1、如何判斷構造函數的執行結果?
一般來說無法判斷。但是我們可以人為的類中定義一個用於表明構造函數執行結果的變量,並在構造函數結束的地方給該變量賦值,最後通過讀取該變量的值來得知構造函數的執行結果
2、在構造函數中執行return語句會發生什麽?
首先在構造函數中指向他return是合法的,執行return語句後構造函數立即結束
3、構造函數執行結束是否意味著對象構造成功?
構造函數只提供自動初始化成員變量的機會,不能保證初始化邏輯一定成功。構造函數決定的是對象的初始化狀態,而不是對象的誕生。
也就是說構造函數初始化操作的失敗不影響對象的誕生

半成品對象

初始化操作不能按照預期完成而得到的對象
是C++中的合法對象,也是bug的來源

#include <stdio.h>

class Test
{
    int mi;
    int mj;
    bool mStatus;
public:
    Test(int i, int j) : mStatus(false)
    {
        mi = i;

        //return;

        mj = j;

        mStatus = true;
    }
    int getI()
    {
        return mi;
    }
    int getJ()
    {
        return mj;
    }
    int status()
    {
        return mStatus;
    }
};

int main()
{  
    Test t1(1, 2);

    if( t1.status() )
    {
        printf("t1.mi = %d\n", t1.getI());
        printf("t1.mj = %d\n", t1.getJ());

    }

    return 0;
}

二階構造

工程開發中的構造過程可分為:
第一階段構造:(真正的構造函數)
資源無關的初始化操作,不可能出現異常的操作
第二階段構造:(返回值表示初始化狀態的普通函數)
需要時用系統資源的操做,可能出現異常情況(內存申請,訪問文件)
如圖所示:(27-2)
技術分享圖片

#include <stdio.h>

class TwoPhaseCons 
{
private:
    TwoPhaseCons() // 第一階段構造函數
    {   
    }
    bool construct() // 第二階段構造函數(普通函數,返回值表示系統資源初始化狀態)
    { 
        return true; 
    }
public:
     TwoPhaseCons* NewInstance() // 對象創建函數
    {
        TwoPhaseCons* ret = new TwoPhaseCons();

        // 若第二階段構造失敗,返回 NULL    
        if( !(ret && ret->construct()) ) 
        {
            delete ret;
            ret = NULL;
        }

        return ret;
    }
};

//TwoPhaseCons* TwoPhaseCons::NewInstance() 

int main()
{
    TwoPhaseCons* obj = TwoPhaseCons::NewInstance();

    printf("obj = %p\n", obj);

    delete obj;

    return 0;
}

總結:
(1)二階構造認為的將初始化分為兩部分,能夠確保創建的對象都是完整的
(2)二階構造的構造函數都是私有的,並提供了一個用於創建對象的靜態函數指針???(通過類名直接訪問,然後創建對象),所以最終的對象分配在堆區
(3)實際工程中需要初始化的數據都是比較多的,所以對象創建在堆區是合理的
使用二階構造完善之前的數組類

對象的構造順序

析構函數的調用順序
析構函數與對應的構造函數的調用順序相反,所以我們之只要知道構造函數的調用順序就可以知道析構的順序
(1)單個函數創建時構造函數的調用順序(先父母,後他人,再自己)
1、調用父類的構造過程
2、調用成員變量的構造函數
3、調用類自身的構造函數
(2)對於棧對象和全局對象,類似於入棧和出棧的順序,最先構造的對象最後被析構
(3)堆對象的析構發生在使用delete的時候,與delete的使用順序相關

/**
析構函數與對應的構造函數的調用順序相反,所以我們之只要知道構造函數的調用順序就可以知道析構的順序
(1)單個函數創建時構造函數的調用順序
    1、調用父類的構造過程
    2、調用成員變量的構造函數
    3、調用類自身的構造函數
**/
#include <stdio.h>

class Member
{
    const char* ms;
public:
    Member(const char* s)
    {
        printf("Member(const char* s): %s\n", s);

        ms = s;
    }
    ~Member()
    {
        printf("~Member(): %s\n", ms);
    }
};

class Test
{
    Member mA;      //調用成員變量的構造函數
    Member mB;
public:
    Test() : mB("mB"), mA("mA")     //初始化列表對成員進行初始化,與對象的的構造順序無關
    {
        printf("Test()\n");
    }
    ~Test()
    {
        printf("~Test()\n");
    }
};

Member gA("gA");

int main()
{                                   
    Test t;     //調用類自身的構造函數

            //對象的構造函數掉順序:
            //gA, mA, mB, Test()
            //對象的析構順序與構造順序相反

    return 0;
}

C++語言(03)——對象的構造