你好,C++(33)對象生死兩茫茫 6.2.3 一個對象的生與死:構造函數和析構函數


6.2.2  使用類創建對象

完成某個類的聲明並且定義其成員函數之后,這個類就可以使用了。一個定義完成的類就相當於一種新的數據類型,我們可以用它來定義變量,也就是創建這個類所描述的對象,表示現實世界中的各種實體。比如前面完成了Teacher類的聲明和定義,就可以用它來創建一個Teacher類的對象,用它來表示某一位具體的老師。創建類的對象的方式跟定義變量的方式相似,只需要將定義完成的類當作某種數據類型,用之前我們定義變量的方式來定義對象,而定義得到的變量就是這個類的對象。其語法格式如下:

類名 對象名;

其中,類名就是定義好的類的名字,對象名就是要定義的對象的名字,例如:

// 定義一個Teacher類型的對象MrChen,代表陳老師
Teacher MrChen;

這樣就得到了一個Teacher類的對象MrChen,它代表學校中具體的某位陳老師。得到類的對象后,就可以通過“.”操作符訪問這個類提供的公有成員,包括讀寫其公有成員變量和調用其公有成員函數,從而訪問其屬性或者是完成其動作。其語法格式如下:

對象名.公有成員變量;
對象名.公有成員函數;

例如,要讓剛才定義的對象MrChen進行“上課”的動作,就可以通過“.”調用它的表示上課行為的成員函數:

// 調用對象所屬類的成員函數,表示這位老師開始上課
MrChen.GiveLesson();

這樣,該對象就會執行Teacher類中定義的GiveLesson()成員函數,完成上課的具體動作。

除了直接使用對象之外,跟普通的數據類型可以使用相應類型的指針來訪問它所指向的數據一樣,對於自己定義的類,我們同樣可以把它當作數據類型來定義指針,把它指向某個具體的對象,進而通過指針來訪問該對象的成員。例如:

// 定義一個可以指向Teacher類型對象的指針pMrChen,初始化為空指針
Teacher* pMrChen = nullptr;
// 用“&”操作符取得MrChen對象的地址賦值給指針pMrChen,
// 也就是將pMrChen指針指向MrChen對象
pMrChen = &MrChen;

這里,我們首先把Teacher類當作數據類型,使用像普通數據類型定義指針一樣的形式,定義了一個可以指向Teacher類型對象的指針pMrChen,然后通過“&”操作符取得MrChen對象的地址並賦值給該指針,這樣就將該指針指向了MrChen對象。

除了可以使用“&”操作符取得已有對象的地址,並用這個地址對指針進行賦值來將指針指向某個對象之外,還可以使用“new”關鍵字直接創建一個對象並返回該對象的地址,再用這個地址對指針進行賦值,同樣可以創建新的對象並將指針指向這個新的對象。例如:

// 創建一個新的Teacher對象
// 並讓pMrChen指針指向這個新對象
Teacher* pMrChen = new Teacher();

這里,“new”關鍵字會負責完成Teacher對象的創建,並返回這個對象的地址,然后再將這個返回的對象地址賦值給pMrChen指針,這樣就同時完成了對象的創建和指針的賦值。

有了指向對象的指針,就可以利用“->”操作符(這個操作符是不是很像一根針?)通過指針訪問該對象的成員。例如:

// 通過指針訪問對象的成員
pMrChen->GiveLesson();

這里需要特別注意的是,跟普通的變量不同,使用“new”關鍵字創建的對象無法在其生命周期結束后自動銷毀,所以我們必須在對象使用完畢后,用“delete”關鍵字人為地銷毀這個對象,釋放其占用的內存。例如:

// 銷毀指針所指向的對象
delete pMrChen;
pMrChen = nullptr;  // 指向的對象銷毀后,重新成為空指針

“delete”關鍵字首先會對pMrChen所指向的Teacher對象進行一些Teacher類所特有的清理工作,然后釋放掉這個對象所占用的內存,整個對象也就銷毀了。當對象被銷毀后,原來指向這個對象的指針就成了一個指向無效地址的“野指針”,為了防止這個“野指針”被錯誤地再次使用,在用delete關鍵字銷毀對象后,緊接着我們通常將這個指針賦值為nullptr,使其成為一個空指針,避免它的再次使用。

最佳實踐:無須在“new”之后或者“delete”之前測試指針是否為nullptr

很多有經驗的C++程序員都會強調,為了增加代碼的健壯性,我們在使用指針之前,應該先判斷指針是否為nullptr,確定其有效之后才能使用。應當說,在使用指針訪問類的成員時,這樣的檢查是必要的。而如果是在“new”創建對象之后和“delete”銷毀對象之前進行檢查,則完全是畫蛇添足。

一方面,使用“new”創建新對象時,如果系統無法為創建新的對象分配足夠的內存而導致創建對象失敗,則會拋出一個std::bad_alloc異常,“new”操作永遠不會返回nullptr。另一方面,C++語言也保證,如果指針p是一個nullptr,則“delete p”不作任何事情,自然也不會有錯誤發生。所以,在使用“new”創建對象之后和“delete”關鍵字銷毀對象之前,都無需對指針的有效性進行檢查,直接使用就好。

// 創建對象
Teacher* p = new Teacher();
// 直接使用指針p訪問對象…
// 銷毀對象
delete p;
// 銷毀對象之后,才需要將指針賦值為nullptr,避免“野指針”的出現
 p = nullptr;

6.2.3  一個對象的生與死:構造函數和析構函數

在現實世界中,每個事物都有其生命周期,會在某個時候出現也會在另外一個時候消亡。程序是對現實世界的反映,其中的對象就代表了現實世界的各種事物,自然也就同樣有生命周期,也會被創建和銷毀。一個對象的創建和銷毀,往往是其一生中非常重要的時刻,需要處理很多復雜的事情。例如,在創建對象的時候,需要進行很多初始化工作,設置某些屬性的初始值;而在銷毀對象的時候,需要進行一些清理工作,最重要的是把申請的資源釋放掉,把打開的文件關閉掉,就像一個人離開人世時,應該把該還的錢還了,干干凈凈地走。為了完成對象的生與死這兩件大事,C++中的類專門提供了兩個特殊的函數——構造函數(Constructor)和析構函數(Destructor),它們的特殊之處就在於,它們會在對象創建和銷毀的時候被自動調用,分別用來處理對象的創建和銷毀的復雜工作。

由於構造函數會在對象創建的時候被自動調用,所以我們可以用它來完成很多不便在對象創建完成后進行的事情,比如可以在構造函數中對對象的某些屬性進行初始化,使得對象一旦被創建就有比較合理的初始值。這就像人的性別是在娘胎里確定的,一旦出生就有了明確的性別。C++規定每個類都必須有構造函數,如果一個類沒有顯式地聲明構造函數,那么編譯器也會為它產生一個默認的構造函數,只是這個默認構造函數沒有參數,也不做任何額外的事情而已。而如果我們想在構造函數中完成一些特殊的任務,就需要自己為類添加構造函數了。可以通過如下的方式為類添加構造函數:

class 類名
{
public:
    類名(參數列表)
    {
        // 對類進行構造,完成初始化工作
    }
};

因為構造函數具有特殊性,所以它的聲明也比較特殊。

首先,在大多數情況下構造函數的訪問級別應該是公有(public)的,因為構造函數需要被外界調用以創建對象。只有在少數的特殊用途下,才會使用其他訪問級別。例如,在后文6.4.4小節介紹的單件模式中,我們就將構造函數設置為私有(private)的,從而防止外界直接通過構造函數創建對象。

其次是返回值類型,構造函數只是完成對象的創建,並不需要返回數據,自然也就無所謂返回值類型了。

再其次是函數名,構造函數必須跟類同名,也就是用類的名字作為構造函數的名字。

最后是參數列表,跟普通函數一樣,在構造函數中我們也可以擁有參數列表,利用這些參數傳遞進來的數據來完成對象的初始化工作,從而可以用不同的參數創建得到有差別的對象。根據參數列表的不同,一個類可以擁有多個構造函數,以適應不同的構造方式。

上文中的Teacher類就沒有顯式(所謂顯式,是相對於隱式而言的,它通常指的是我們用代碼明確地表達我們的意圖而產生的事物。比如,用戶自己定義的構造函數。而隱式則是用戶並沒有用代碼明確定義,由編譯器自動為其生成的事物。比如,一個默認的構造函數)地聲明構造函數,而是使用了編譯器為它生成的默認構造函數,所以其創建的對象都是千篇一律一模一樣的,所有新創建對象的m_strName成員變量都是那個在類聲明中給出的固定初始值。換句話說,也就是所有“老師”都是同一個“名字”,這顯然是不合理的。下面改寫這個Teacher類,為它添加一個帶有string類型參數的構造函數,使其可以在創建對象的時候通過構造函數來完成對成員變量的合理初始化,創建有差別的對象:

class Teacher
{
public:
// 構造函數
// 參數表示Teacher類對象的名字
    Teacher(string strName)    // 帶參數的構造函數
    {
        // 使用參數對成員變量賦值,進行初始化
        m_strName = strName;
    };

    void GiveLesson();     // 備課
protected:
    string m_strName = "ChenLiangqiao"; // 類聲明中的初始值         // 姓名
private:
};

現在就可以在定義對象的時候,將參數寫在對象名之后的括號中,這種定義對象的形式會調用帶參數的構造函數Teacher(string strName),進而給定這個對象的名字屬性。

// 使用參數,創建一個名為“WangGang”的對象
Teacher MrWang("WangGang");

在上面的代碼中,我們使用字符串“WangGang”作為構造函數的參數,它就會調用Teacher類中需要string類型為參數的Teacher(string strName)構造函數來完成對象的創建。在構造函數中,這個參數值被賦值給了類的m_strName成員變量,以代替其在類聲明中給出的固定初始值“ChenLiangqiao”。當對象創建完成后,參數值“WangGang”就會成為MrWang對象的名字屬性的值,這樣我們就通過參數創建了一個有着特定“名字”的Teacher對象,各位“老師”終於可以有自己的名字了。

在構造函數中,除了可以使用“=”操作符對對象的成員變量進行賦值以完成初始化之外,還可以使用“:”符號在構造函數后引出初始化屬性列表,直接利用構造函數的參數或者其他的合理初始值對成員變量進行初始化。其語法格式如下:

class 類名
{
public:
    // 使用初始化屬性列表的構造函數
    類名(參數列表)
     : 成員變量1(初始值1),成員變量2(初始值2)…  // 初始化屬性列表
    {
    }

// 類的其他聲明和定義
};

在進入構造函數執行之前,系統將完成成員變量的創建並使用其后括號內的初始值對其進行賦值。這些初始值可以是構造函數的參數,也可以是成員變量的某個合理初始值。如果一個類有多個成員變量需要通過這種方式進行初始化,那么多個變量之間可以使用逗號分隔。例如,可以利用初始化屬性列表將Teacher類的構造函數改寫為:

class Teacher
{
public:
    // 使用初始化屬性列表的構造函數
    Teacher(string strName)
        // 初始化屬性列表,使用構造函數的參數strName創建並初始化m_strName
        : m_strName(strName)  
    {
     // 構造函數中無需再對m_strName賦值
    }
protected:
    string m_strName;
};

使用初始化屬性列表改寫后的構造函數,利用參數strName直接創建Teacher類的成員變量m_strName並對其進行初始化,這樣就省去了使用“=”對m_strName進行賦值時的額外工作,可以在一定程度上提高對象構造的效率。另外,某些成員變量必須在創建的同時就給予初始值,比如某些使用const關鍵字修飾的成員變量,這種情況下使用初始化屬性列表來完成成員變量的初始化就成了一種必須了。所以,在可以的情況下,最好是使用構造函數的初始化屬性列表中完成類的成員變量的初始化。

這里需要注意的是,如果類已經有了顯式定義的構造函數,那么編譯器就不會再為其生成默認構造函數。例如,在Teacher類擁有顯式聲明的構造函數之后,如果還是想采用如下的形式定義對象,就會產生一個編譯錯誤。

// 試圖調用默認構造函數創建一個沒有名字的老師
Teacher MrUnknown;

這時編譯器就會提示錯誤,因為這個類已經沒有默認的構造函數了,而唯一的構造函數需要給出一個參數,這個創建對象的形式會因為找不到合適的構造函數而導致編譯錯誤。因此在實現類的時候,一般都會顯式地寫出默認的構造函數,同時根據需要添加帶參數的構造函數來完成一些特殊的構造任務。

在C++中,根據初始條件的不同,我們往往需要用多種方式創建一個對象,所以一個類常常有多個不同參數形式的構造函數,分別負責以不同的方式創建對象。而在這些構造函數中,往往有一些大家都需要完成的工作,一個構造函數完成的工作很可能是另一個構造函數所需要完成工作的一部分。比如,Teacher類有兩個構造函數,一個是不帶參數的默認構造函數,它會給Teacher類的m_nAge成員變量一個默認值28,而另一個是帶參數的,它首先需要判斷參數是否在一個合理的范圍內,然后將合理的參數賦值給m_nAge。這兩個構造函數都需要完成的工作就是給m_nAge賦值,而第一個構造函數的工作也可以通過給定參數28,通過第二個構造函數來完成,這樣,第二個構造函數的工作就成了第一個構造函數所要完成工作的一部分。為了避免重復代碼的出現,我們只需要在某個特定構造函數中實現這些共同功能,而在需要這些共同功能的構造函數中,直接調用這個特定構造函數就可以了。這種方式被稱為委托調用構造函數(delegating constructors)。例如:

class Teacher
{
public:
    // 帶參數的構造函數
    Teacher(int x)
    {
        // 判斷參數是否合理,決定賦值與否
       if (0 < x && x <= 100)
             m_nAge = x;
        else
              cout<<"錯誤的年齡參數"<<endl;
    }

    // 構造函數Teacher()委托調用構造函數Teacher(int x)
    // 這里我們錯誤地把出生年份當作年齡參數委托調用構造函數Teacher(int x),
     // 直接實現了參數合法性驗證並賦值的功能
Teacher() : Teacher(1982)
{
    // 完成特有的創建工作
}

// ...
private:
    int m_nAge;   // 年齡
};

在這里,我們在構造函數之后加上冒號“:”,然后跟上另外一個構造函數的調用形式,實現了一個構造函數委托調用另外一個構造函數。在一個構造函數中調用另外一個構造函數,把部分工作交給另外一個構造函數去完成,這就是委托的意味。不同的構造函數各自負責處理自己的特定情況,而把最基本的共用的構造工作委托給某個基礎構造函數去完成,實現分工協作。

當一個使用定義變量的形式創建的對象使用完畢離開其作用域之后,這個對象會被自動銷毀。而對於使用new關鍵字創建的對象,則需要在使用完畢后,通過delete關鍵字主動銷毀對象。但無論是哪種方式,對象在使用完畢后都需要銷毀,也就是完成一些必要的清理工作,比如釋放申請的內存、關閉打開的文件等。

跟對象的創建比較復雜,需要專門的構造函數來完成一樣,對象的銷毀也比較復雜,同樣需要專門的析構函數來完成。同為類當中負責對象創建與銷毀的特殊函數,兩者有很多相似之處。首先是它們都會被自動調用,只不過一個是在創建對象時,而另一個是在銷毀對象時。其次,兩者的函數名都是由類名構成,只不過析構函數名在類名前加了個“~”符號以跟構造函數名相區別。再其次,兩者都沒有返回值,兩者都是公有的(public)訪問級別。最后,如果沒有必要,兩者在類中都是可以省略的。如果類當中沒有顯式地聲明構造函數和析構函數,編譯器也會自動為其產生默認的函數。而兩者唯一的不同之處在於,構造函數可以有多種形式的參數,而析構函數卻不接受任何參數。下面來為Teacher類加上析構函數完成一些清理工作,以替代默認的析構函數:

class Teacher
{
public: // 公有的訪問級別
    //// 析構函數
    // 在類名前加上“~”構成析構函數名
~Teacher()  // 不接受任何參數
    {
        // 進行清理工作
        cout<<"春蠶到死絲方盡,蠟炬成灰淚始干"<<endl;
    };
   
//
};

因為Teacher類不需要額外的清理工作,所以在這里我們沒有定義任何操作,只是輸出一段信息表示Teacher類對象的結束。一般來說,會將那些需要在對象被銷毀之前自動完成的事情放在析構函數中來處理。例如,對象創建時申請的內存資源,在對象銷毀后就不能再繼續占用了,需要在析構函數中進行合理地釋放,歸還給操作系統。就像一個有信譽的人在離開人世之前,要把欠別人的錢還清一樣,干干凈凈地離開。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM