建構子與解構子
建構子定義
- 在定義物件時,如何進行初始化?物件的初始化方法需要在程式中規定好,為此,C++ 語法提供了一個特殊的機制——建構子。
例 4_1-2:建構子
// 類別定義 class Clock { public: Clock(int newH, int newM, int newS); // 建構子 void setTime(int newH, int newM, int newS); void showTime(); private: int hour, minute, second; };
建構子的實現
Clock::Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) { } int main() { Clock c(0, 0, 0); // 自動調用建構子 c.showTime(); return 0; }
類別的定義
class Clock { public: Clock(int newH, int newM, int newS); // 建構子 Clock(); // 預設建構子 void setTime(int newH, int newM, int newS); void showTime(); private: int hour, minute, second; }; Clock::Clock() : hour(0), minute(0), second(0) { } // 預設建構子
int main() { Clock c1(0, 0, 0); // 調用有參數的建構子 Clock c2; // 調用無參數的建構子 ...... }
建構子的作用
- 在物件被創建時使用特定的值來構造物件,將物件初始化為一個特定的初始狀態。
- 例如:希望在構造一個
Clock
類別物件時,將初始時間設為0:0:0
,就可以通過建構子來設定。
建構子的形式
- 函數名稱與類別名稱相同;
- 不能定義返回值類型,也不能有
return
語句; - 可以有形式參數,也可以沒有形式參數;
- 可以是內聯函數;
- 可以重載;
- 可以帶預設參數值。
建構子的調用時機
- 在物件創建時自動調用。例如:
Clock myClock(0, 0, 0);
預設建構子
- 調用時不需要實參的建構子
- 參數表為空的建構子
- 全部參數都有預設值的建構子
- 下面兩個都是預設建構子,如在類別中同時出現,將產生編譯錯誤:
Clock(); Clock(int newH = 0, int newM = 0, int newS = 0);
隱含生成的建構子
如果程式中未定義建構子,編譯器將在需要時自動生成一個預設建構子。
- 參數列表為空,不為資料成員設定初始值;
- 如果類別內定義了成員的初始值,則使用類別內定義的初始值;
- 如果沒有定義類別內的初始值,則以預設方式初始化;
- 基本類型的資料預設初始化的值是不確定的。
“=default”
- 如果類別中已定義建構子,預設情況下編譯器就不再隱含生成預設建構子。如果此時依然希望編譯器隱含生成預設建構子,可以使用 “=default”。
- 範例:
class Clock { public: Clock() = default; // 指示編譯器提供預設建構子 Clock(int newH, int newM, int newS); // 建構子 private: int hour, minute, second; };
委託建構子
- 類別中往往有多個建構子,只是參數表和初始化列表不同,其初始化演算法都是相同的,這時,為了避免程式碼重複,可以使用委託建構子。
回顧
Clock
類別的兩個建構子:Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) { } Clock::Clock() : hour(0), minute(0), second(0) { } // 預設建構子
委託建構子
- 委託建構子使用類別的其他建構子執行初始化過程。
- 範例:
Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) { } Clock() : Clock(0, 0, 0) { }
複製建構子
我們經常需要用一個已存在的物件,去初始化新的物件,這時就需要一種特殊的建構子——複製建構子。
- 預設的複製建構子可以實現對應資料成員的一一複製;
- 自訂的複製建構子可以實現特殊的複製功能。
例 4-2:Point 類別
class Point { // Point 類別的定義 public: Point(int xx = 0, int yy = 0) { x = xx; y = yy; } // 建構子,內聯 Point(const Point& p); // 複製建構子 void setX(int xx) { x = xx; } void setY(int yy) { y = yy; } int getX() const { return x; } // 常函數(第5章) int getY() const { return y; } // 常函數(第5章) private: int x, y; // 私有資料 };
//複製建構子的實現 Point::Point(const Point& p) { x = p.x; y = p.y; cout << "Calling the copy constructor " << endl; } //形參為 Point 類別物件的函數 void fun1(Point p) { cout << p.getX() << endl; } //返回值為 Point 類別物件的函數 Point fun2() { Point a(1, 2); return a; } int main() { Point a(4, 5); Point b(a); // 用 a 初始化 b。 cout << b.getX() << endl; fun1(b); // 物件 b 作為 fun1 的實參 b = fun2(); // 函數的返回值是類別物件 cout << b.getX() << endl; return 0; }
複製建構子的定義
- 複製建構子是一種特殊的建構子,其形參為本類別的物件引用。作用是用一個已存在的物件去初始化同類型的新物件。
class 類名 { public: 類名(形參); // 建構子 類名(const 類名& 物件名); // 複製建構子 // ... }; 類名::類名(const 類名& 物件名) // 複製建構子的實現 { 函數體 }
隱含的複製建構子
- 如果程式設計師沒有為類別聲明複製建構子,則編譯器自己生成一個隱含的複製建構子。
- 這個建構子執行的功能是:用作為初始值的物件的每個資料成員的值,初始化將要建立的物件的對應資料成員。
“=delete”
如果不希望物件被複製建構:
- C++98 做法:將複製建構子聲明為
private
,並且不提供函數的實現。 - C++11 做法:用 “=delete” 指示編譯器不生成預設複製建構子。
- 範例:
class Point { // Point 類別的定義 public: Point(int xx = 0, int yy = 0) { x = xx; y = yy; } // 建構子,內聯 Point(const Point& p) = delete; // 指示編譯器不生成預設複製建構子 private: int x, y; // 私有資料 };
複製建構子被調用的三種情況
- 定義一個物件時,以本類別的另一個物件作為初始值,發生複製建構;
- 如果函數的形參是類別物件,調用函數時,將使用實參物件初始化形參物件,發生複製建構;
- 如果函數的返回值是類別物件,函數執行完成返回主調函數時,將使用
return
語句中的物件初始化一個臨時無名物件,傳遞給主調函數,此時發生複製建構。- 這種情況如何避免不必要的複製?
左值與右值
- 左值:是位於賦值語句左側的物件變數。
- 右值:是位於賦值語句右側的值,右值不依附於物件。
int i = 42; // i 是左值,42 是右值 int fooBar(); int j = fooBar(); // fooBar() 是右值
- 左值和右值之間的轉換
int i = 5, j = 6; // i 和 j 是左值 int k = i + j; // 自動轉化為右值表達式
右值引用
- 對持久存在變數的引用稱為 左值引用,用
&
表示(即第3章引用類型)。 - 對短暫存在可被移動的右值的引用稱之為 右值引用,用
&&
表示。float n = 6; float &lr_n = n; // 左值引用 float &&rr_n = n; // 錯誤,右值引用不能綁定到左值 float &&rr_n = n * n; // 右值表達式綁定到右值引用
- 通過標準庫
<utility>
中的move
函數可將左值物件移動為右值:float n = 10; float &&rr_n = std::move(n); // 將 n 轉化為右值
- 使用
move
函數承諾除對 n 重新賦值或銷毀外,不以rr_n
以外方式使用。
- 使用
移動建構子
- 基於右值引用,移動建構子通過移動資料的方式構造新物件。與複製建構子類似,移動建構子的參數為該類別物件的右值引用。範例如下:
#include<utility> class astring { public: std::string s; astring(astring&& o) noexcept : s(std::move(o.s)) // 顯式移動所有成員 { 函數體 } }
- 移動建構子不分配新記憶體,理論上不會報錯,為配合異常捕獲機制,需聲明
noexcept
表明不會拋出異常(將於第12章異常處理介紹)。 - 被移動的物件不應再使用,需要銷毀或重新賦值。
- 移動建構子不分配新記憶體,理論上不會報錯,為配合異常捕獲機制,需聲明
解構子
- 解構子是在物件的生命週期結束時自動執行的特殊成員函數,主要用來完成物件被銷毀前的一些清理工作。
- 自動調用:當物件的作用域結束或被顯式刪除時,系統會自動調用解構子,然後釋放該物件所佔用的記憶體空間。
- 預設解構子:如果程式中沒有明確定義解構子,編譯器將自動產生一個預設的解構子,其函數體為空。
- 範例:
#include <iostream> using namespace std; class Point { public: Point(int xx, int yy); // 建構子 ~Point(); // 解構子 //...其他函數原型 private: int x, y; }; Point::Point(int xx, int yy) { x = xx; y = yy; } Point::~Point() { cout << "Destructor called for Point(" << x << ", " << y << ")" << endl; }
在這個例子中,
Point
類別定義了一個建構子和一個解構子。當物件被創建時,建構子會被調用以初始化資料成員;當物件的生命週期結束時,解構子會被自動調用以執行一些清理工作。