Effective C++ 比較偏向原則性的說明,什麼該做,什麼不該做,什麼做了必死無疑。書中的範例都很片段,總有些讓人意猶未盡。
今天推薦的這本 C++ 沉思錄,改從另外一種角度出發,先從一個很小的雛型開始,逐步添枝加葉,過程中告訴你為什麼要這樣做,會面臨哪些設計決策,最後完成一個完整的範例。
這樣的寫作方式,小弟一讀就上癮了,因為這更貼近程式員的實際工作狀況:「寫出一些 classes,拼湊他們,卡住了,回到上一步...」
作者 Andrew Koenig、 Barbara Moo 是 C++ 元老級人物,後者與 Stanley Lippman 寫了很有名的教科書 C++ Primer(印象中 Moo 為 Lippman 的研究所導師),Andrew Koenig 更是被 Scott Meyers 列為 C++ 史上影響力前五大人物,本書可看性可想而知。
以下就來談談這本書我認為最精華的部份
參考計數(Reference Counting)
在 C++ 之後流行的 OOPL,幾乎都提供垃圾收集機制,C++ 缺少此功能,同樣的程式與 Java 比起來就是又長又彆扭,因為 C++ 程式員總是要很小心的避免 memory leak。
參考計數可以說是 C++ 人工版垃圾收集,作者很強調這個技巧,本書第 6 章-10章都是此技巧的展現,在他的另外一本著作 Accelerated C++ 裡也提到了這個技巧。
這個技巧的核心精神就是不直接使用 class 產生物件,而是另外設計一個 handle class,這個 handle class 用一個指標指向前者 class 產生的物件。
這樣做有何好處?脫褲子放屁?其中的關鍵有兩個:
記憶體自動管理
如下圖所示,利用藏於 Foo 中的計數器,FooHandle destructor 可以檢查是否為最後一個使用者,如果是就把物件刪除:
當然在 2016 年的今天,我們已經有了 boost::shared_ptr,而應用此技巧最兇最有名的 framework 就是大名鼎鼎的 Qt,不過 Qt 的作法與 shared_ptr 還是有所差異的,例如 Qt 中最常用的 QString 修改字串時採取的是 copy-on-write 策略,一旦字串被修改,就會配置記憶體產生新字串,shared_ptr 無論怎樣讀寫物件都不會產生新物件出來,本書兩種作法都有提供範本。
Handle 與 shared_ptr 還有另外一個重大差異,以上圖來說,以 handle class 產生的物件在誕生時內部指標就會配置物件,以上圖來說 FooHandle fh 就會使得 FooHandle::f_ = new Foo,而 shared_ptr 並沒有此種行為,這是合理的,因為指標的語意不包含自動配置記憶體這一塊。
保留物件的結構資訊
當一個物件為複合物件,例如 Ch7-8 的加減乘除、Ch9-10 的字元繪圖,要表達這些樹狀結構,沒有用到指標是不可能的,用到指標就需要面對記憶體管理。弄到最後...好像也沒有比 C 輕鬆,很多人此時就會面臨鬼打牆的狀況,有些人就乾脆回頭用 C 了。
還有一派人的作法是用 std::map、std::list 來形塑自己需要的樹狀結構,這樣做程式為了有時候弄的削足適履,而且 std::map、std::list 也沒辦法刪除其中的節點後自動刪除節點指向的物件(除非配合 shared_ptr 這種具備參照計數能力的 smart pointer),刪除節點後也要小心 iterator 的失效問題。
所以 handle class 還是非常有用的技法,除了可以免除用大砲轟小鳥(+stl, boost),可以寫出更精緻的程式,也不用在程式中勉力記住何時該 new ,何時該 delete。甚至也有 smart pointer 不可取代的部份,此時可以把物件視為一般的數值傳遞,配合適當的 operator overloading 可以達成我們需要的物件複合運算。如果使用 smart pointer,為了配合指標的語意,就只能使用 *xx + *yy 這樣的語法。
參考計數的缺點
//foo_handle.h #ifndef FOO_HANDLE_H #define FOO_HANDLE_H class Foo; class BarHandle; class FooHandle { Foo *f_; public: FooHandle(Foo *f); FooHandle(); ~FooHandle(); FooHandle(const FooHandle& src); FooHandle& operator=(const FooHandle& src); void set(const BarHandle& b); }; #endif
//foo_handle.cpp #include "foo.h" #include "foo_handle.h" FooHandle::FooHandle(Foo *f):f_(f) { } FooHandle::FooHandle(): f_(0) { } FooHandle::~FooHandle() { if(f_ != 0 && --f_->use_ == 0) delete f_; } FooHandle::FooHandle(const FooHandle& src):f_(src.f_) { ++src.f_->use_; } FooHandle& FooHandle::operator=(const FooHandle& src) { ++src.f_->use_; if(f_ != 0 && --f_->use_ == 0) delete f_; f_ = src.f_; return *this; } void FooHandle::set(const BarHandle& b) { if(f_) f_->set(b); }
//foo.h #ifndef FOO_H #define FOO_H #include "bar_handle.h" class Foo { friend class FooHandle; int use_; BarHandle b_; public: void set(const BarHandle& b); Foo(); ~Foo(); }; #endif
//foo.cpp #include "foo.h" Foo::Foo(): use_(1) { } Foo::~Foo() { } void Foo::set(const BarHandle& b) { b_ = b; }
//bar_handle.h #ifndef BAR_HANDLE_H #define BAR_HANDLE_H class Bar; class FooHandle; class BarHandle { Bar *b_; public: BarHandle(Bar *b); BarHandle(); ~BarHandle(); BarHandle(const BarHandle& src); BarHandle& operator=(const BarHandle& src); void set(const FooHandle& f); }; #endif
//bar_handle.cpp #include "bar.h" #include "bar_handle.h" BarHandle::BarHandle(Bar *b):b_(b) { } BarHandle::BarHandle(): b_(0) { } BarHandle::~BarHandle() { if(b_ != 0 && --b_->use_ == 0) delete b_; } BarHandle::BarHandle(const BarHandle& src):b_(src.b_) { ++src.b_->use_; } BarHandle& BarHandle::operator=(const BarHandle& src) { ++src.b_->use_; if(b_ != 0 && --b_->use_ == 0) delete b_; b_ = src.b_; return *this; } void BarHandle::set(const FooHandle& f) { if(b_) b_->set(f); }
//bar.h #ifndef BAR_H #define BAR_H #include "foo_handle.h" class Bar { friend class BarHandle; int use_; FooHandle f_; public: void set(const FooHandle& f); Bar(); ~Bar(); }; #endif
//bar.cpp #include "bar.h" Bar::Bar(): use_(1) { } Bar::~Bar() { } void Bar::set(const FooHandle& f) { f_ = f; }
#include "bar_handle.h" #include "foo_handle.h" #include "foo.h" #include "bar.h" int main(int argc, char *argv[]) { FooHandle f(new Foo); BarHandle b(new Bar); f.set(b); b.set(f); return 0; }用 valgrind 檢查,沒證據不能亂說話:
的確發生了 memory leak,這個問題即使使用 shared_ptr 也會發生,需要另外搭配 weak_ptr,看來寫 C++ 永遠是件苦差事?
結語
其實看了 Ch1-Ch10,作者使用的技巧只是枝微末結,他真正厲害的地方是對問題的分析,總是能抓到問題的本質,此種「分析」的能力才是促使他晉升 C++ 名人堂的真正原因,雖然本書已有相當年紀,但仍十分具有啟發性,強力推薦!
買上購買來閱讀XD
回覆刪除回頭翻了一下已經放到生菇的 More Effective C++, 也有提到這個技巧, 但沉思錄比較淺顯易懂, Scott Meyers 的技巧較為完整華麗
刪除std::string的Copy-on-Write:不如想象中美好
回覆刪除http://www.cnblogs.com/promise6522/archive/2012/03/22/2412686.html
有 thread safe 的問題
Qt 也有同樣問題,不過我的原則就是盡量不用thread
刪除