標籤:
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,使其成為一個null 指標,避免它的再次使用。
最佳實務:無須在“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類對象的結束。一般來說,會將那些需要在對象被銷毀之前自動完成的事情放在解構函式中來處理。例如,對象建立時申請的記憶體資源,在對象銷毀後就不能再繼續佔用了,需要在解構函式中進行合理地釋放,歸還給作業系統。就像一個有信譽的人在離開人世之前,要把欠別人的錢還清一樣,乾乾淨淨地離開。
你好,C++(33)對象生死兩茫茫 6.2.3 一個對象的生與死:建構函式和解構函式