當(dāng)前位置:首頁 > 嵌入式培訓(xùn) > 嵌入式學(xué)習(xí) > 講師博文 > 淺析C++的構(gòu)造函數(shù)和析構(gòu)函數(shù)
在現(xiàn)實世界中,每個事物都有其生命周期,會在某個時候出現(xiàn)也會在另外一個時候消亡。程序是對現(xiàn)實世界的反映,其中的對象就代表了現(xiàn)實世界的各種事物,自然也就同樣有生命周期,也會被創(chuàng)建和銷毀。一個對象的創(chuàng)建和銷毀,往往是其一生中非常重要的時刻,需要處理很多復(fù)雜的事情。例如,在創(chuàng)建對象的時候,需要進行很多初始化工作,設(shè)置某些屬性的初始值;而在銷毀對象的時候,需要進行一些清理工作,重要的是把申請的資源釋放掉,把打開的文件關(guān)閉掉,為了完成對象的生與死這兩件大事,C++中的類專門提供了兩個特殊的函數(shù)—— 構(gòu)造函數(shù)(Constructor)和析構(gòu)函數(shù)(Destructor),它們的特殊之處就在于,它們會在對象創(chuàng)建和銷毀的時候被自動調(diào)用,分別用來處理對象的創(chuàng)建和銷毀的復(fù)雜工作。
構(gòu)造函數(shù)
由于構(gòu)造函數(shù)會在對象創(chuàng)建的時候被自動調(diào)用,所以我們可以用它來完成很多不便在對象創(chuàng)建完成后進行的事情,比如可以在構(gòu)造函數(shù)中對對象的某些屬性進行初始化,使得對象一旦被創(chuàng)建就有比較合理的初始值。C++規(guī)定每個類都必須有構(gòu)造函數(shù),如果一個類沒有顯式地聲明構(gòu)造函數(shù),那么編譯器也會為它產(chǎn)生一個默認的構(gòu)造函數(shù),只是這個默認構(gòu)造函數(shù)沒有參數(shù),也不做任何額外的事情而已。而如果我們想在構(gòu)造函數(shù)中完成一些特殊的任務(wù),就需要自己為類添加構(gòu)造函數(shù)了。可以通過如下的方式為類添加構(gòu)造函數(shù):
class Teacher
{
public:
Teacher(參數(shù)列表)
{
// 對Teacher類進行構(gòu)造,完成初始化工作
}
private:
string m_strName ;
};
因為構(gòu)造函數(shù)具有特殊性,所以它的聲明也比較特殊。
首先,在大多數(shù)情況下構(gòu)造函數(shù)的訪問級別應(yīng)該是公有(public)的,因為構(gòu)造函數(shù)需要被外界調(diào)用以創(chuàng)建對象。只有在少數(shù)的 特殊用途下,才會使用其他訪問級別。
其次是返回值類型,構(gòu)造函數(shù)只是完成對象的創(chuàng)建,并不需要返回數(shù)據(jù),自然也就無所謂返回值類型了。
再其次是函數(shù)名,構(gòu)造函數(shù)必須跟類同名,也就是用類的名字作為構(gòu)造函數(shù)的名字。
后是參數(shù)列表,跟普通函數(shù)一樣,在構(gòu)造函數(shù)中我們也可以擁有參數(shù)列表,利用這些參數(shù)傳遞進來的數(shù)據(jù)來完成對象的初始化工作,從而可以用不同的參數(shù)創(chuàng)建得到有差別的對象。根據(jù)參數(shù)列表的不同,一個類可以擁有多個構(gòu)造函數(shù),以適應(yīng)不同的構(gòu)造方式。
如果Teacher類就沒有顯式地聲明構(gòu)造函數(shù),就會使用編譯器為它生成的默認構(gòu)造函數(shù),所以其創(chuàng)建的對象都是千篇一律一模一樣的,所有新創(chuàng)建對象的m_strName成員變量都是那個在類聲明中給出的固定初始值。換句話說,也就是所有“老師”都是同一個“名字”,這顯然是不合理的。下面改寫這個Teacher類,為它添加一個帶有string類型參數(shù)的構(gòu)造函數(shù),使其可以在創(chuàng)建對象的時候通過構(gòu)造函數(shù)來完成對成員變量的合理初始化,創(chuàng)建有差別的對象:
class Teacher
{
public:
// 構(gòu)造函數(shù)
// 參數(shù)表示Teacher類對象的名字
Teacher(string strName) // 帶參數(shù)的構(gòu)造函數(shù)
{
// 使用參數(shù)對成員變量賦值,進行初始化
m_strName = strName;
};
void GiveLesson(); // 備課
private:
string m_strName = "qulu"; // 類聲明中的初始值
// 姓名
};
現(xiàn)在就可以在定義對象的時候,將參數(shù)寫在對象名之后的括號中,這種定義對象的形式會調(diào)用帶參數(shù)的構(gòu)造函數(shù)Teacher(string strName),進而給定這個對象的名字屬性。
// 使用參數(shù),創(chuàng)建一個名為“WangGang”的對象
Teacher MrWang("WangGang");
在上面的代碼中,我們使用字符串“WangGang”作為構(gòu)造函數(shù)的參數(shù),它就會調(diào)用Teacher類中需要string類型 為參數(shù)的Teacher(string strName)構(gòu)造函數(shù)來完成對象的創(chuàng)建。在構(gòu)造函數(shù)中,這個參數(shù)值被賦值給了類的m_strName成員變量,以代替其在類聲明中給出的固定初始值 “qulu”。當(dāng)對象創(chuàng)建完成后,參數(shù)值“WangGang”就會成為MrWang對象的名字屬性的值,這樣我們就通過參數(shù)創(chuàng)建了一個有著特定“名字”的Teacher對象,各位“老師”終于可以有自己的名字了。
在構(gòu)造函數(shù)中,除了可以使用“=”操作符對對象的成員變量進行賦值以完成初始化之外,還可以使用“:”符號在構(gòu)造函數(shù)后引出初始化屬性列表,直接利用構(gòu)造函數(shù)的參數(shù)或者其他的合理初始值對成員變量進行初始化。其語法格式如下:
class 類名
{
public:
// 使用初始化屬性列表的構(gòu)造函數(shù)
類名(參數(shù)列表) : 成員變量1(初始值1),成員變量2(初始值2)…
// 初始化屬性列表
{
}
// 類的其他聲明和定義
};
在進入構(gòu)造函數(shù)執(zhí)行之前,系統(tǒng)將完成成員變量的創(chuàng)建并使用其后括號內(nèi)的初始值對其進行初始化。這些初始值可以是構(gòu)造函數(shù)的參數(shù),也可以是成員變量的某個合理初始值。如果一個類有多個成員變量需要通過這種方式進行初始化,那么多個變量之間可以使用逗號分隔。例如,可以利用初始化屬性列表將Teacher類的構(gòu)造函數(shù)改寫為:
class Teacher
{
public:
// 使用初始化屬性列表的構(gòu)造函數(shù)
Teacher(string strName) : m_strName(strName)
{
// 構(gòu)造函數(shù)中無需再對m_strName賦值
}
private:
string m_strName;
};
使用初始化屬性列表改寫后的構(gòu)造函數(shù),利用參數(shù)strName直接創(chuàng)建Teacher類的成員變量m_strName并對其進行初始化,這樣就省去了使用“=”對m_strName進行賦值時的額外工作,可以在一定程度上提高對象構(gòu)造的效率。另外,某些成員變量必須在創(chuàng)建的同時就給予初始值,比如某些使用const關(guān)鍵字修飾的成員變量或引用類型的成員變量,這種情況下使用初始化屬性列表來完成成員變量的初始化就成了一種必須了。所以,在可以的情況下,好是使用構(gòu)造函數(shù)的初始化屬性列表中完成類的成員變量的初始化。
這里需要注意的是,如果類已經(jīng)有了顯式定義的構(gòu)造函數(shù),那么編譯器就不會再為其生成默認構(gòu)造函數(shù)。例如,在Teacher類擁有顯式聲明的構(gòu)造函數(shù)之后,如果還是想采用如下的形式定義對象,就會產(chǎn)生一個編譯錯誤。
// 試圖調(diào)用默認構(gòu)造函數(shù)創(chuàng)建一個沒有名字的老師
Teacher MrUnknown;
這時編譯器就會提示錯誤,因為這個類已經(jīng)沒有默認的構(gòu)造函數(shù)了,而唯一的構(gòu)造函數(shù)需要給出一個參數(shù),這個創(chuàng)建對象的形式會因為找不到合適的構(gòu)造函數(shù)而導(dǎo)致編譯錯誤。因此在實現(xiàn)類的時候,一般都會顯式地寫出默認的構(gòu)造函數(shù),同時根據(jù)需要添加帶參數(shù)的構(gòu)造函數(shù)來完成一些特殊的構(gòu)造任務(wù)。
在C++中,根據(jù)初始條件的不同,我們往往需要用多種方式創(chuàng)建一個對象,所以一個類常常有多個不同參數(shù)形式的構(gòu)造函數(shù),分別負責(zé)以不同的方式創(chuàng)建對象。而在這些構(gòu)造函數(shù)中,往往有一些大家都需要完成的工作,一個構(gòu)造函數(shù)完成的工作很可能是另一個構(gòu)造函數(shù)所需要完成工作的一部分。比如,Teacher類有兩個構(gòu)造函數(shù),一個是不帶參數(shù)的默認構(gòu)造函數(shù),它會給Teacher類的m_nAge成員變量一個默認值28,而另一個是帶參數(shù)的,它首先需要判斷參數(shù)是否在一個合理的范圍內(nèi),然后將合理的參數(shù)賦值給m_nAge。這兩個構(gòu)造函數(shù)都需要完成的工作就是給m_nAge賦值,而第一個構(gòu)造函數(shù)的工作也可以通過給定參數(shù)28,通過第二個構(gòu)造函數(shù)來完成,這樣,第二個構(gòu)造函數(shù)的工作就成了第一個構(gòu)造函數(shù)所要完成工作的一部分。為了避免重復(fù)代碼的出現(xiàn),我們只需要在某個特定構(gòu)造函數(shù)中實現(xiàn)這些共同功能,而在需要這些共同功能的構(gòu)造函數(shù)中,直接調(diào)用這個特定構(gòu)造函數(shù)就可以了。這種方式被稱為委托調(diào)用構(gòu)造函數(shù)(delegating constructors)。例如:
class Teacher
{
public:
// 帶參數(shù)的構(gòu)造函數(shù)
Teacher(int x)
{
// 判斷參數(shù)是否合理,決定賦值與否
if (0 < x && x <= 100)
m_nAge = x;
else
cout<<"錯誤的年齡參數(shù)"<
}
private:
int m_nAge;
}
// 構(gòu)造函數(shù)Teacher()委托調(diào)用構(gòu)造函數(shù)Teacher(int x)
// 這里我們錯誤地把出生年份當(dāng)作年齡參數(shù)委托調(diào)用構(gòu)造函數(shù)
// 直接實現(xiàn)了參數(shù)合法性驗證并賦值的功能
Teacher() : Teacher(1982)
{
// 完成特有的創(chuàng)建工作
}
private:
int m_nAge; // 年齡
};
在這里,我們在構(gòu)造函數(shù)之后加上冒號“:”,然后跟上另外一個構(gòu)造函數(shù)的調(diào)用形式,實現(xiàn)了一個構(gòu)造函數(shù)委托調(diào)用另外一個構(gòu)造函數(shù)。在一個構(gòu)造函數(shù)中調(diào)用另外一個構(gòu)造函數(shù),把部分工作交給另外一個構(gòu)造函數(shù)去完成,這就是委托的意味。不同的構(gòu)造函數(shù)各自負責(zé)處理自己的特定情況,而把基本的共用的構(gòu)造工作委托給某個基礎(chǔ)構(gòu)造函數(shù)去完成,實現(xiàn)分工協(xié)作。
析構(gòu)函數(shù)
當(dāng)一個使用定義變量的形式創(chuàng)建的對象使用完畢離開其作用域之后,這個對象會被自動銷毀。而對于使用new關(guān)鍵字創(chuàng)建的對象,則需要在使用完畢后,通過delete關(guān)鍵字主動銷毀對象。但無論是哪種方式,對象在使用完畢后都需要銷毀,也就是完成一些必要的清理工作,比如釋放申請的內(nèi)存、關(guān)閉打開的文件等。
跟對象的創(chuàng)建比較復(fù)雜,需要專門的構(gòu)造函數(shù)來完成一樣,對象的銷毀也比較復(fù)雜,同樣需要專門的析構(gòu)函數(shù)來完成。同為類當(dāng)中負責(zé)對象創(chuàng)建與銷毀的特殊函數(shù),兩者有很多相似之處。首先是它們都會被自動調(diào)用,只不過一個是在創(chuàng)建對象時,而另一個是在銷毀對象時。其次,兩者的函數(shù)名都是由類名構(gòu)成,只不過析構(gòu)函數(shù)名在類名前加了個“~”符號以跟構(gòu)造函數(shù)名相區(qū)別。再其次,兩者都沒有返回值,兩者都是公有的(public)訪問級別。后,如果沒有必要,兩者在類中都是可以省略的。如果類當(dāng)中沒有顯式地聲明構(gòu)造函數(shù)和析構(gòu)函數(shù),編譯器也會自動為其產(chǎn)生默認的函數(shù)。而兩者唯一的不同之處在于,構(gòu)造函數(shù)可以有多種形式的參數(shù),而析構(gòu)函數(shù)卻不接受任何參數(shù)。下面來為Teacher類加上析構(gòu)函數(shù)完成一些清理工作,以替代默認的析構(gòu)函數(shù):
class Teacher
{
public: // 公有的訪問級別
// …
// 析構(gòu)函數(shù)
// 在類名前加上“~”構(gòu)成析構(gòu)函數(shù)名
~Teacher() // 不接受任何參數(shù)
{
// 進行清理工作
cout<<"春蠶到死絲方盡,蠟炬成灰淚始干"<
};
// …
};
因為Teacher類不需要額外的清理工作,所以在這里我們沒有定義任何操作,只是輸出一段信息表示Teacher類對象的結(jié)束。一般來說,會將那些需要在對象被銷毀之前自動完成的事情放在析構(gòu)函數(shù)中來處理。例如,對象創(chuàng)建時申請的內(nèi)存資源,在對象銷毀后就不能再繼續(xù)占用了,需要在析構(gòu)函數(shù)中進行合理地釋放,歸還給操作系統(tǒng)。
注意析構(gòu)函數(shù)只能銷毀對象的非static成員,static成員要到程序結(jié)束后才會被釋放。由于析構(gòu)函數(shù)沒有入?yún)⒁矝]有返回值,所以析構(gòu)函數(shù)不能被重載,對于給定的類只有唯一的一個析構(gòu)函數(shù),但是構(gòu)造函數(shù)可以被重載。
什么時候會調(diào)用析構(gòu)函數(shù):
無論何時一個對象被銷毀,就會自動調(diào)用其析構(gòu)函數(shù):
1. 變量在離開其作用域時被銷毀。
2. 當(dāng)一個對象被銷毀時,其成員被銷毀
3. 容器(不論是標(biāo)準(zhǔn)庫容器還是數(shù)組)被銷毀時,其元素被銷毀
4. 對于動態(tài)分配的對象(new),當(dāng)對指向它的指針應(yīng)用delete運算符時被銷毀
5. 對于臨時對象,當(dāng)創(chuàng)建它的完整表達式結(jié)束時被銷毀
由于析構(gòu)函數(shù)自動運行,我們的程序可以按需要分配資源,而通常無需要擔(dān)心何時釋放這些資源。認識到析構(gòu)函數(shù)本身并不直接銷毀成員是非常重要的,成員是在析構(gòu)函數(shù)體后隱含的析構(gòu)階段中被銷毀的,在整個對象銷毀過程中,析構(gòu)函數(shù)體是作為成員銷毀步驟之外的另一部分而進行的。
如果顯示調(diào)用析構(gòu)函數(shù),析構(gòu)函數(shù)相當(dāng)于的一個普通的成員函數(shù),執(zhí)行析構(gòu)函數(shù)體中的語句,并沒有釋放內(nèi)存。
class aaa
{
public:
aaa(){}
~aaa(){cout<<"deconstructor"<
void disp(){cout<<"disp"<
private:
char *p;
};
void main()
{
aaa a;
a.~aaa();
a.~aaa();
a. disp();
}
這樣的話,顯示兩次deconstructor,前兩次調(diào)用析構(gòu)函數(shù)相當(dāng)于調(diào)用一個普通的成員函數(shù),執(zhí)行函數(shù)內(nèi)語句,顯示兩次deconstructor。
真正的析構(gòu)是編譯器隱式的調(diào)用,增加了釋放棧內(nèi)存的動作,這個類未申請堆內(nèi)存,所以對象干凈地摧毀了。
class aaa
{
public:
aaa(){p = new char[1024];}
~aaa()
{cout<<"deconstructor"<
void disp(){cout<<"disp"<
private:
char *p;
};
void main()
{
aaa a;
a.~aaa();
a.~aaa();
a.disp();
}
這樣的話,第一次顯式調(diào)用析構(gòu)函數(shù),相當(dāng)于調(diào)用一個普通成員函數(shù),執(zhí)行函數(shù)語句,釋放了堆內(nèi)存,但是并未釋放棧內(nèi)存,對象還存在(但已殘缺,存在不安全因素);
第二次調(diào)用析構(gòu)函數(shù),再次釋放堆內(nèi)存(此時報異常)并打印。
后隱式執(zhí)行析構(gòu)過程釋放棧內(nèi)存,對象銷毀。