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