學習C++ -> 構造函數與析構函數
一、構造函數的介紹
1. 構造函數的作用
構造函數主要用來在創建對象時完成對對象屬性的一些初始化等操作, 當創建對象時, 對象會自動調用它的構造函數。一般來說, 構造函數有以下三個方面的作用:
■ 給創建的對象建立一個標識符;
■ 為對象數據成員開辟內存空間;
■ 完成對象數據成員的初始化。
2. 默認構造函數
當用戶沒有顯式的去定義構造函數時, 編譯器會為類生成一個默認的構造函數, 稱為 "默認構造函數", 默認構造函數不能完成對象數據成員的初始化, 只能給對象創建一標識符, 並為對象中的數據成員開辟一定的內存空間。
3. 構造函數的特點
無論是用戶自定義的構造函數還是默認構造函數都主要有以下特點:
①. 在對象被創建時自動執行;
②. 構造函數的函數名與類名相同;
③. 沒有返回值類型、也沒有返回值;
④. 構造函數不能被顯式調用。
#給Python程序員的注釋: C++中的構造函數類似於Python中的 __init__ 方法.
二、構造函數的顯式定義
由於在大多數情況下我們希望在對象創建時就完成一些對成員屬性的初始化等工作, 而默認構造函數無法滿足我們的要求, 所以我們需要顯式定義一個構造函數來覆蓋掉默認構造函數以便來完成必要的初始化工作, 當用戶自定義構造函數后編譯器就不會再為對象生成默認構造函數。
在構造函數的特點中我們看到, 構造函數的名稱必須與類名相同, 並且沒有返回值類型和返回值, 看一個構造函數的定義:
1 #include <iostream> 2 3 using namespace std; 4 5 class Point 6 { 7 public: 8 Point() //聲明並定義構造函數 9 { 10 cout<<"自定義的構造函數被調用...\n"; 11 xPos = 100; //利用構造函數對數據成員 xPos, yPos進行初始化 12 yPos = 100; 13 } 14 void printPoint() 15 { 16 cout<<"xPos = " << xPos <<endl; 17 cout<<"yPos = " << yPos <<endl; 18 } 19 20 private: 21 int xPos; 22 int yPos; 23 }; 24 25 int main() 26 { 27 Point M; //創建對象M 28 M.printPoint(); 29 30 return 0; 31 }
編譯運行的結果:
自定義的構造函數被調用... xPos = 100 yPos = 100 Process returned 0 (0x0) execution time : 0.453 s Press any key to continue.
代碼說明:
在Point類的 public 成員中我們定義了一個構造函數 Point() , 可以看到這個Point構造函數並不像 printPoint 函數有個void類型的返回值, 這正是構造函數的一特點。在構造函數中, 我們輸出了一句提示信息, "自定義的構造函數被調用...", 並且將對象中的數據成員xPos和yPos初始化為100。
在 main 函數中, 使用 Point 類創建了一個對象 M, 並調用M對象的方法 printPoint 輸出M的屬性信息, 根據輸出結果看到, 自定義的構造函數被調用了, 所以 xPos和yPos 的值此時都是100, 而不是一個隨機值。
需要提示一下的是, 構造函數的定義也可放在類外進行。
三、有參數的構造函數
在上個示例中實在構造函數的函數體內直接對數據成員進行賦值以達到初始化的目的, 但是有時候在創建時每個對象的屬性有可能是不同的, 這種直接賦值的方式顯然不合適。不過構造函數是支持向函數中傳入參數的, 所以可以使用帶參數的構造函數來解決該問題。
1 #include <iostream> 2 3 using namespace std; 4 5 class Point 6 { 7 public: 8 Point(int x = 0, int y = 0) //帶有默認參數的構造函數 9 { 10 cout<<"自定義的構造函數被調用...\n"; 11 xPos = x; //利用傳入的參數值對成員屬性進行初始化 12 yPos = y; 13 } 14 void printPoint() 15 { 16 cout<<"xPos = " << xPos <<endl; 17 cout<<"yPos = " << yPos <<endl; 18 } 19 20 private: 21 int xPos; 22 int yPos; 23 }; 24 25 int main() 26 { 27 Point M(10, 20); //創建對象M並初始化xPos,yPos為10和20 28 M.printPoint(); 29 30 Point N(200); //創建對象N並初始化xPos為200, yPos使用參數y的默認值0 31 N.printPoint(); 32 33 Point P; //創建對象P使用構造函數的默認參數 34 P.printPoint(); 35 36 return 0; 37 }
編譯運行的結果:
自定義的構造函數被調用... xPos = 10 yPos = 20 自定義的構造函數被調用... xPos = 200 yPos = 0 自定義的構造函數被調用... xPos = 0 yPos = 0 Process returned 0 (0x0) execution time : 0.297 s Press any key to continue.
代碼說明:
在這個示例中的構造函數 Point(int x = 0, int y = 0) 使用了參數列表並且對參數進行了默認參數設置為0。在 main 函數中共創建了三個對象 M, N, P。
M對象不使用默認參數將M的坐標屬性初始化10和20;
N對象使用一個默認參數y, xPos屬性初始化為200;
P對象完全使用默認參數將xPos和yPos初始化為0。
三、構造函數的重載
構造函數也畢竟是函數, 與普通函數相同, 構造函數也支持重載, 需要注意的是, 在進行構造函數的重載時要注意重載和參數默認的關系要處理好, 避免產生代碼的二義性導致編譯出錯, 例如以下具有二義性的重載:
Point(int x = 0, int y = 0) //默認參數的構造函數 { xPos = x; yPos = y; } Point() //重載一個無參構造函數 { xPos = 0; yPos = 0; }
在上面的重載中, 當嘗試用 Point 類重載一個無參數傳入的對象 M 時, Point M; 這時編譯器就報一條 error: call of overloaded 'Point()' is ambiguous 的錯誤信息來告訴我們說 Point 函數具有二義性, 這是因為 Point(int x = 0, int y = 0) 全部使用了默認參數, 即使我們不傳入參數也不會出現錯誤, 但是在重載時又重載了一個不需要傳入參數了構造函數 Point(), 這樣就造成了當創建對象都不傳入參數時編譯器就不知道到底該使用哪個構造函數了, 就造成了二義性。
四、初始化表達式
對象中的一些數據成員除了在構造函數體中進行初始化外還可以通過調用初始化表來進行完成, 要使用初始化表來對數據成員進行初始化時使用 : 號進行調出, 示例如下:
Point(int x = 0, int y = 0):xPos(x), yPos(y) //使用初始化表 { cout<<"調用初始化表對數據成員進行初始化!\n"; }
在 Point 構造函數頭的后面, 通過單個冒號 : 引出的就是初始化表, 初始化的內容為 Point 類中int型的 xPos 成員和 yPos成員, 其效果和 xPos = x; yPos = y; 是相同的。
與在構造函數體內進行初始化不同的是, 使用初始化表進行初始化是在構造函數被調用以前就完成的。每個成員在初始化表中只能出現一次, 並且初始化的順序不是取決於數據成員在初始化表中出現的順序, 而是取決於在類中聲明的順序。
此外, 一些通過構造函數無法進行初始化的數據類型可以使用初始化表進行初始化, 如: 常量成員和引用成員, 這部分內容將在后面進行詳細說明。使用初始化表對對象成員進行初始化的完整示例:

1 #include <iostream> 2 3 using namespace std; 4 5 class Point 6 { 7 public: 8 Point(int x = 0, int y = 0):xPos(x), yPos(y) 9 { 10 cout<<"調用初始化表對數據成員進行初始化!\n"; 11 } 12 13 void printPoint() 14 { 15 cout<<"xPos = " << xPos <<endl; 16 cout<<"yPos = " << yPos <<endl; 17 } 18 19 private: 20 int xPos; 21 int yPos; 22 }; 23 24 int main() 25 { 26 Point M(10, 20); //創建對象M並初始化xPos,yPos為10和20 27 M.printPoint(); 28 29 return 0; 30 }
五、析構函數
與構造函數相反, 析構函數是在對象被撤銷時被自動調用, 用於對成員撤銷時的一些清理工作, 例如在前面提到的手動釋放使用 new 或 malloc 進行申請的內存空間。析構函數具有以下特點:
■ 析構函數函數名與類名相同, 緊貼在名稱前面用波浪號 ~ 與構造函數進行區分, 例如: ~Point();
■ 構造函數沒有返回類型, 也不能指定參數, 因此析構函數只能有一個, 不能被重載;
■ 當對象被撤銷時析構函數被自動調用, 與構造函數不同的是, 析構函數可以被顯式的調用, 以釋放對象中動態申請的內存。
#給Python程序員的注釋: C++中的析構函數類似於Python中的 __del__ 方法.
當用戶沒有顯式定義析構函數時, 編譯器同樣會為對象生成一個默認的析構函數, 但默認生成的析構函數只能釋放類的普通數據成員所占用的空間, 無法釋放通過 new 或 malloc 進行申請的空間, 因此有時我們需要自己顯式的定義析構函數對這些申請的空間進行釋放, 避免造成內存泄露。
1 #include <iostream> 2 #include <cstring> 3 4 using namespace std; 5 6 class Book 7 { 8 public: 9 Book( const char *name ) //構造函數 10 { 11 bookName = new char[strlen(name)+1]; 12 strcpy(bookName, name); 13 } 14 ~Book() //析構函數 15 { 16 cout<<"析構函數被調用...\n"; 17 delete []bookName; //釋放通過new申請的空間 18 } 19 void showName() { cout<<"Book name: "<< bookName <<endl; } 20 21 private: 22 char *bookName; 23 }; 24 25 int main() 26 { 27 Book CPP("C++ Primer"); 28 CPP.showName(); 29 30 return 0; 31 32 }
編譯運行的結果:
Book name: C++ Primer 析構函數被調用... Process returned 0 (0x0) execution time : 0.266 s Press any key to continue.
代碼說明:
代碼中創建了一個 Book 類, 類的數據成員只有一個字符指針型的 bookName, 在創建對象時系統會為該指針變量分配它所需內存, 但是此時該指針並沒有被初始化所以不會再為其分配其他多余的內存單元。在構造函數中, 我們使用 new 申請了一塊 strlen(name)+1 大小的空間, 也就是比傳入進來的字符串長度多1的空間, 目的是讓字符指針 bookName 指向它, 這樣才能正常保存傳入的字符串。
在 main 函數中使用 Book 類創建了一個對象 CPP, 初始化 bookName 屬性為 "C++ Primer"。從運行結果可以看到, 析構函數被調用了, 這時使用 new 所申請的空間就會被正常釋放。
自然狀態下對象何時將被銷毀取決於對象的生存周期, 例如全局對象是在程序運行結束時被銷毀, 自動對象是在離開其作用域時被銷毀。
如果需要顯式調用析構函數來釋放對象中動態申請的空間只需要使用 對象名.析構函數名(); 即可, 例如上例中要顯式調用析構函數來釋放 bookName 所指向的空間只要:
CPP.~Book();
--------------------
wid, 2013.02.19
上一篇: 學習C++ -> 類(Classes)的定義與實現