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