類這個概念無非是數據和方法的集合,為什麼我一直困惑呢?為什麼不弄清楚呢?
C++中的類這個概念裡有4個函數比較特殊,像我這種以前有C經驗的人可能一時難以適應,它們是構造函數、析構函數、復制構造函數和賦值操作符。這四個函數有它獨特的地方,總讓人摸不著頭腦。這次看《C++ 沉思錄》之後有心進行一次整理,這裡就分享給大家。
1、為什麼需要一個構造函數?有些類非常簡單,完全無需構造函數,所以並非是所有的類都需要構造函數。但是有些復雜的類,他們需要構造函數來隱藏它們內部的工作方式,這個時候就需要創建一個了。
/* 一個例子,說明為什麼不使用一個綁定來代替getLength()函數 const引用確實能夠實現目的,但是必須在每個構造函數的初始化表裡增加一句 略顯麻煩 */ class Stick { public: const int &length; // 一個const的引用,能夠讀取但無法修改 Stick(): true_length(1), length(true_length) {} Stick(Stick &st): true_length(st.true_length), length(true_length) {} Stick(int a): true_length(a), length(true_length) {} void setLength(int b) { true_length = b; } // int getLength() { return true_length; } private: int true_length; };
很多時候,當我寫了一個我認為需要的構造函數之後,總會添加一個無參的構造函數,但很少去想為什麼我需要這麼個無參的構造函數,它給我帶來了什麼功能呢?事實上一個無參的構造函數,能夠讓你像這樣定義類Stick a; 如果Stick沒有無參的構造函數,那麼這樣的定義就是錯的,同樣的,當你需要一個Stick的數組的時候,如果沒有無參構造函數也是不能成功的,定義數組的時候比較多,比如 Stick a[1024]; 但是很少去想這裡居然還有無參構造函數的什麼事。
面向對象的設計其實是用類來包裝了狀態,雖然這種設計也有它自身的缺點,但是對象確實是通過數據成員來反映狀態的。是否所有的數據成員都必須有一個初始狀態呢?這就不一定了,所以我的看法是,當你在使用之前做了初始化就OK,或者即使不初始化也沒有問題就OK。
可能問這個問題比問構造函數更傻,但實際上析構函數是跟類的數據成員息息相關的,不是所有的類都需要析構函數的。只有當你的類申請了資源,並且這些資源不會通過成員函數自動釋放,這個時候就需要一個析構函數來擦屁股。
class B { public: int s; }; class D: public B { public: int t; ~D() { printf("~D\n"); } }; int _tmain(int argc, _TCHAR* argv[]) { B *bp = new D; delete bp; return 0; }
這裡我定義了一個D的析構函數並進行打印,只是為了顯示一下D的析構函數是否被調用了,事實是沒有,也就是說如果父類不定義析構函數,那麼一個指向子類的父類指針在刪除時是不會去調用無論父類還是子類的析構函數的,同樣,如果父類的析構函數不是虛函數,那麼也不會去調用子類的析構函數,而是直接調用父類的析構函數。所以,為了完整析構函數調用鏈,讓子類的析構函數能夠被調用,父類的析構函數只得定義成虛的。但是不是所有父類的析構函數都必須是虛函數呢?這個也不見得。我們公司在測試之間流傳著幾句話:如果看到父類的析構函數沒有定義成虛函數,那麼就會得意的笑著說又一個bug;如果你的類裡有一個虛函數,那麼析構函數也必須是虛的等等。
在《Effective C++》第三版第七條裡面,有這麼句話“任何class只要帶有virtual函數都幾乎確定應該也有一個virtual析構函數”,我想上面的口號估計就來自這裡。但是如果我的類雖然是父類,但是不會像上面那樣使用一個父類的指針去釋放子類,也就是不會有多態的特性,那麼這個要求父類有虛析構函數的做法就顯得不通情理了。在《C++ 沉思錄》裡面也說了句話“使所有的類都自動包含虛析構函數會亵渎C++‘只為用到的東西付出代價’的哲學”。
最後總結,只有當你想使用多態特性時父類才要求定義一個虛析構函數。關於析構函數的調用順序這裡不談,其實很簡單,自己google吧。
7、什麼時候需要一個復制構造函數?
在解釋這個問題之前,我們可能需要明白,如果你沒有定義一個復制構造函數,那麼編譯器會自動為你添加一個復制構造函數。其實編譯器不光會自動添加復制構造函數,它還會在你沒有定義的情況下添加賦值操作符、析構函數,甚至構造函數,關於這點可以參看《Effective C++》。編譯器添加的復制構造函數直接復制了數據成員和基類的對象,如果你不是想這樣做的話,你就得自己定義一個復制構造函數來防止編譯器添加的那個。舉個例子:
class String { public: String(); String(const char *s); private: char *data; };
這麼一個類的定義,沒有定義復制構造函數,那麼編譯器會添加一個,但是這會造成在復制這個類時,data的地址被保存在了1個以上的對象中,那麼在釋放data的時候就可能多次釋放,所以這個類不定義復制構造函數是極其危險的。如果你想阻止復制對象這種行為,該怎麼辦呢?《C++ 沉思錄》中給出了一種做法,即把復制構造函數定義成私有的,同時保證你的成員函數中不會有用到復制對象就OK了:
class String { public: ...... private: String(const String &); String &operator=(const String &); ...... };
8、什麼時候需要一個賦值操作符?
這個理由很簡單,當你需要用賦值操作符復制對象時就需要定義一個,這裡需要注意一點,賦值操作符只有在對象已經構造完成之後才會被調用,這是什麼意思呢?看下面的例子:
class A { public: A() {} ~A() {} A(const A &) { printf("copy construct\n"); } A &operator=(const A &) { printf(" = operator\n"); return *this; } }; int _tmain(int argc, _TCHAR* argv[]) { A a; A b(a); A c = a; A d; d = a; return 0; }
這裡只有最後那個d = a;會調用operator=(),A b(a);和A c = a;都調用的是拷貝構造函數,前一個好理解,為什麼A c = a;也調用的是拷貝構造函數呢?其實這種寫法只是一種語法糖,是為了兼容C的寫法。賦值操作符還有什麼可說的呢?當然,就是在自己賦值給自己的時候,這個行為你需要注意,你最好在賦值之前先判斷是否是自己賦值給自己,然後再進行處理。
本文出自 “菜鳥浮出水” 博客,請務必保留此出處http://rangercyh.blog.51cto.com/1444712/1287993