在編寫C++程序的時候,我們會為特定某一類對象申明類類型,幾乎我們申明的每一個class都會有一個或多個構造函數、一個析構函數、一個賦值運算符重載=、以及拷貝構造函數。這些函數控制著類對象的基礎操作,確保新定義的對象的初始化、完成對象撤銷時的清理工作、賦予對象新值。如果這些函數的操作出錯,則會導致嚴重的後果,所以確保這些函數的操作行為正常是非常重要的。
一、編譯器默認生成的函數
如果我們編寫一個空類,編譯器會為我們默認生成構造函數、析構函數、賦值運算符、拷貝構造函數。
例如當我們定義
class Empty{ };
就好像我們寫下了如下代碼(紅色是編譯器默認生成)
class Empty{
public:
Empty(){....} //默認構造函數
Empty(const Empty &rhs){....} //默認拷貝構造函數
~Empty(){....} //默認析構構造函數
Empty& operator=(const Empty &rhs){....} //賦值運算符
Empty* operator&(){...} //取地址運算符
const Empty* operator&() const{...} //取地址運算法的const版本
};
1. 說明:(1)這些函數只有在被調用的時候,才會被編譯器創建出來;
(2)四個函數都public且inline的;
(3)如果顯示的定義了其中某一個函數,那麼編譯器就不會生成其對應的默認的版本;
(4)自定義的拷貝構造函數不僅會覆蓋默認的拷貝構造函數,同時也會覆蓋默認的構造函數,下面的函數class構造函數,不能通過編譯
1 #include <iostream> 2 using namespace std; 3 class Empty 4 { 5 public: 6 Empty(const Empty &Copy){}; 7 }; 8 int main(int argc, char** argv) 9 { 10 Empty a; 11 return 0; 12 }View Code
2. 實例:
下面的代碼會讓編譯器創建默認的構造函數 Empty e1; //默認構造函數
Empty e2(e1);//拷貝構造函數
e2 = e1;//賦值運算符
1 #include <iostream> 2 using namespace std; 3 4 class Empty{ 5 6 public: 7 Empty(){cout << "create" << endl;} 8 Empty(const Empty &Copy){ cout << "copy" << endl;} 9 Empty& operator=(const Empty &Assig){cout << "assign=" << 10 endl;} 11 Empty* operator&(){cout << "&" << endl;} 12 const Empty* operator&() const {cout << "&1" << endl;} 13 ~Empty(){cout << "delete" << endl;} 14 }; 15 int main() 16 { 17 Empty *e = new Empty(); // create 18 delete e; //delete 19 Empty e0; //create 20 const Empty e1; //create 21 Empty e2(e1); //copy 22 Empty e3; //create 23 e3 = e1;//assign= 24 cout << &e0 << endl;//& 0x602080 25 const Empty *p = &e1;//&1 26 cout << p << endl; //0x602080 27 return 0; 28 } 29 //e0,e1,e2,e3對象被撤銷時候刪除 30 delete 31 delete 32 delete 33 deleteView Code
二、構造函數
1. 構造函數的作用
構造函數是特殊的成員函數,用來在創建對象時完成對對象屬性的一些初始化等操作, 當創建對象時, 對象會自動調用它的構造函數。
2. 默認構造函數
正如第一部分所述,如果沒有為一個類顯示定義任何構造函數、編譯器將自動為這個類生成默認構造函數。默認構造函數將依據變量初始化的規則初始化類中的所有成員:
(1)對於具有類類型的成員,會調用該成員所屬類自身的默認構造函數實現初始化;
(2)內置類型成員的初值依賴於對象如何定義,如果對象在全局作用域中定義或定義為靜態局部對象,則這些成員將被初始化為0。如果對象在局部作用域中定義,則這些成員沒有初始化;
(3)默認構造函數一般適用於僅包含類類型的成員的類;
(4)由於默認構造函數不會初始化內置類型的成員,所以必須顯示定義類的構造函數。
1 #include <iostream> 2 using namespace std; 3 class Empty 4 { 5 public: 6 int a; 7 string s; 8 }; 9 10 int main(int argc, char** argv) 11 { 12 Empty a; 13 cout << a.a << endl;//輸出a的值隨機 14 cout << a.s.size() << endl;//s是類類型被初始化為空串 15 }View Code
3. 構造函數的特點
(1)在對象被創建時自動執行;
(2)構造函數的函數名與類名相同;
(3)沒有返回值類型、也沒有返回值;
(4)構造函數不能被顯式調用;
4. 重載構造函數
可以為一個類申明的構造函數的數量沒有限制,只要每個構造函數的形參表示唯一的。定義類對象的時候,實參指定使用哪個構造函數。比如我們定義類Sales_item,它的構造函數有三個,在定義類的新對象時,可以使 用這些構造函數中的任意 一個。
1 Class Sales_item{ 2 public: 3 Sales_item(const std::string&); 4 Sales_item(std::istream&); 5 Sales_item(); 6 }; 7 int main() 8 { 9 Sales_item empty;//使用缺省的無參構造函數 10 Sales_item Primer_3rd_Ed("0-201-82470-1"); 11 Sales_item Primer_4th_ed(cin); 12 return 0; 13 }View Code
5. 構造函數自動執行
只要創建對應類類型的一個對象,編譯器就運行一個構造函數。
1 Sales_item Primer_2nd("0-201-54848-8");//運行帶string參數的構造函數 2 Sales_item *p = new Sales_item();//通過默認構造函數初始化該對象View Code
6. 構造函數初始化列表
對象中的一些數據成員除了在構造函數體中進行初始化外,還可以通過構造函數初始化列表進行初始化,構造函數初始化列表只在構造函數的定義中而不是聲明中指定。從概念上將講,可以認為構造函數分兩個階段執 行:(1)初始化階段;(2)普通計算階段,計算階段由構造函數函數體中的所有語句組成;(3)構造函數就是按照成員定義的次序初始化成員的次序。
不管成員是否在構造函數初始化列表中顯式初始化,類類型的數據成員總是在初始化階段初始化。初始化階段發生在計算階段開始之前。
1 Sales_item::Sales_item(const string &book): isbn(book), units_sold(0), revenue(0.0){}View Code
說明:對於const類型成員、引用類型的成員變量都必須在構造函數初始化列表中進行初始化,例如下面的代碼就是錯誤的,必須在初始化列表中對類成員變量進行初始化。
1 class ConstRef{ 2 public: 3 ConstRef(int ii); 4 private: 5 int i; 6 const int ci; 7 int &ri; 8 }; 9 ConstRef::ConstRef(int ii) 10 { 11 //賦值 12 i = ii; 13 ci = ii; //錯誤,不能對const成員賦值 14 ri = i;//不能對引用變量賦值 15 } 16 記住,可以初始化const對象或引用類型的對象,但不能對它們賦值。在開始執行構造函數體之前,要完成初始化。初始化const或引用類型的唯一機會是在構造函數初始化列表中。編寫以上構造函數的正確方式為 17 ConstRef::ConstRef(int ii):i(ii), ci(i), ri(ii)View Code
二、析構函數
構造函數的一個作用是自動獲取資源。例如,構造函數可以分配一個緩沖區或打開一個文件,在構造函數中分配了資源之後,需要一個對應操作自動回收或釋放資源。析構函數就是這樣一個特殊函數,它可以完成所需資 源的回收,作為類的構造函數的補充。
1.何時調用析構函數
a.刪除指向動態分配對象的指針
b.實際對象(而不是對象的引用)超出作用域時
c.撤銷一個容器(不管是標准庫容器還是內置數組)時,即超出容器的作用范圍時
2.缺省析構函數
a.編譯器總會為我們合成一個析構函數,其按照對象創建時的逆序撤銷每個非static成員,因此,它按照成員在類中申明的次序的逆序撤銷成員。
b.缺省的析構函數並不刪除指針成員指向的對象
c.析構函數與賦值操作符和復制構造函數之間的一個重要區別是,及時我們自己編寫了自己的析構函數,缺省的析構函數任然運行
d.對於類類型的對象,合成析構函數調用其析構函數完成對象的釋放;對於內置類型的對象,合成析構函數則不做什麼操作
3.何時編寫顯式析構函數
許多類不需要顯式析構函數,尤其具有構造函數的類不一定需要定義自己的析構函數。僅在有些僅在有些工作需要析構函數完成時,才需要析構函數(顯式的)。析構函數並不僅限於用來釋放資源,一般而言,析構函數可以執行任意操作,該操作是類設計者希望該類對象在使用完畢後執行的。