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
刪除