1. 程式人生 > >EffictiveC++筆記 第3章

EffictiveC++筆記 第3章

個數 管理器 剔除 del 不能 會有 AR ati ffi

我根據自己的理解,對原文的精華部分進行了提煉,並在一些難以理解的地方加上了自己的“可能比較準確”的「翻譯」。

Chapter 3 資源管理

條款13: 以對象管理資源

有時即使你順利地寫了對應對象的delete語句,但是前面的區域可能會有一個過早的return語句或者拋出了異常.它們一旦執行,控制流絕不會觸及delete語句,造成內存泄漏

事實上我們可以將需要的動態資源放進對象內,因為對象的析構函數會自動釋放那些資源

c++對應的解決方案有 auto_ptr,也就是所謂的智能指針,它其實是類指針(pointer-like)對象,其析構函數自動對其所指對象調用delete,可一定程度地避免潛在的資源泄漏可能性

先假設一個名叫func的函數,它會new一個xx類型的對象並返回其指針,你可以這樣使用auto_ptr :
std::auto_ptr<xx> pt(func());

這個簡單例子示範兩個關鍵想法:

::獲得資源後立即放進管理對象; 管理對象運用析構函數確保資源被釋放::

由於auto_ptr被銷毀會自動刪除所指之物,所以註意別讓多個auto_ptr同時指向一個對象。後果是對象很可能會被刪除一次以上,那將使你的程序搭上駛向“未定義行為”的列車上。

為預防這個問題,auto_ptr有一個性質: 若通過copying函數復制它們,它們會變成null,而復制所得指針將取得資源唯一擁有權:

std::auto_ptr<xx> pt1(func());
std::auto_ptr<xx> pt2(pt1); //現在pt2指向原本pt1所指對象,而pt1變成
//nullptr
pt1 = pt2; //現在pt1指向原本pt2所指對象,而pt2變成
//nullptr

可以看出auto_ptr的底層條件:受管理的資源必須絕對沒有一個以上的auto_ptrs指向它。對於STL容器,這個特性不是很好。

替代方案是“引用計數型智能指針(reference-counting smart pointer)”,
shared_ptr
它其實也是一個智能指針,持續追蹤共有多少對象指向某筆資源,::並在無人指向它時自動刪除該資源。::

對於RCSP,你可以這麽寫:std::shared_ptr<xx> pInv(func());
對於以下操作,RCSP相對於auto_ptr正常多了:

...
std::tr1::shared_ptr<xx> pInv1(func());
std::tr1::shared_ptr<xx>. pInv2(pInv1); //現在pInv2與1指向同一個對象
pInv1 = pInv2; //同上,無任何改變
...
//pInv1和pInv2被銷毀後,它們所指的對象也就被自動銷毀

綜上,RCSP很適合STL容器的操作。

值得註意的是,auto和shared_ptr兩者都在其析構函數內做delete而不是delete[]動作。這意味著在動態分配而得的array身上使用auto_ptr和tr1::shared_ptr是餿主意,然而這樣會通過編譯!

std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[1024]);

條款14: 在資源管理類中小心copying行為

有可能你偶爾會發現,你需要建立自己的資源管理類

假設使用C API函數處理類型為Mutex的互斥器對象,共有lock和unlock兩函數可用:

void lock(Mutek* pm); void unlock(Mutek* pm);

可能需要建立一個class來管理機鎖:

class Lock{
public:
    explicit Lock(Mutex* pm)
        :mutexPtr(pm)
    { lock(mutexPtr); } //獲得資源
    ~Lock() { unlock(mutexPtr); } //釋放資源
private:
    Mutex* mutexPtr;
};

現在導入一個觀念:

資源取得時機便是初始化時機(Resource Acquisition Is Initialization; RAII)
並以此作為“資源管理類的脊柱”

「客戶」對Lock的用法符合RAII方式:

Mutex m; //定義需要的互斥器
...
{
    Lock ml(&m); //鎖定互斥器
      ... 
    //在區塊末尾解除鎖定
}

此時假設Lock對象被復制:

Lock ml1(&m); 鎖定m
Lock ml2(ml1); 將ml1復制到ml2身上,會發生啥?

大多數情況你會選擇下面兩種操作:

  1. 禁止復制. 許多時候允許RAII對象被復制並不合理,如果你發現不合理,就應該禁止之。
    根據條款6,我們發現可以:將copying操作聲明為private.對Lock而言看起來是這樣:
class Lock: private Uncopyable{
public:
    …
};
  1. 對底層資源祭出“引用計數法”. 有時我們希望保有資源,直到它的最後一個使用者(某對象)被銷毀。此時復制RAII對象時應將該資源的“被引用數”遞增。也就是使用tr1::shared_ptr

通常只需內含一個tr1::shared_ptr成員變量,RAII classes便可實現出reference-counting copying行為.

存在一個問題:shared_ptr缺省行為是“當引用次數為0時刪除所指物”,然而我們想要的動作是解除鎖定而非剔除。

幸運的是tr1::shared_ptr允許指定所謂的“刪除器(deleter)”——當引用次數為0時便被調用,它是一個函數或函數對象,對於指針是可有可無的第二參數:

class Lock{
public:
    explicit Lock(Mutex* pm)
        :mutexPtr(pm,unlock)
        {
            lock(mutexPtr.get()); //以後談到“get”
          }
private:
    std::tr1::shared_ptr<Mutex> mutexPtr;
};

註意,不再聲明析構函數,因為沒必要,編譯器有缺省行為


條款15: 在資源管理器中提供對原始資源的訪問

假設有這種情況:

使用RCSP智能指針保存factory函數的調用結果:

std::tr1::shared_ptr<Investment> pInv(createInvestment());

假設有一個函數處理investment對象:

int dayHeld(const Investment* pi); //返回投資天數

而你想這麽調用:

int days = daysHeld(pInv);

這樣通不過編譯,因為即使pInv指向的是investment對象,但是pInv本身是類型為tr1::shared_ptr

這時你需要一個函數來將RAII class對象(本例為tr1::shared_ptr) 轉換為其內含原始資源(本例為Investment*)。

有兩種辦法:顯式轉換和隱式轉換。

tr1::shared_ptr和auto_ptr都提供一個叫get的成員函數,用來執行顯式轉換,它將返回智能指針內部的原始指針的復件 :

int days = daysHeld(pink.get())

這兩種指針還重載了指針取值操作符(operator->和operator*),這將允許隱式轉換至原始指針:

class Investment{
public:
    bool isTaxFree() const;
};
...
std::tr1::shared_ptr<Investment> pInv(createInvestment());
bool ok = pInv->isTaxFree();
bool ok1 = *(pInv).isTaxFree();
...

很多傳統的做法便是xx.get()來獲取內部資源,如果這種操作很繁瑣的話,有一種更輕松的做法(隱式轉換):

class XXX{
public:
    ...
    operator xxx() const    //隱式轉換函數,xxx是返回類型
    { return f; }
    ...
};

比如某個函數參數原來得這麽寫

func(xx.get())

現在你可以寫成func(xx)

但這種轉換可能會引發錯誤:

假設類A裏儲存的是B類對象,當B類對象想拷貝一個A類對象時,卻發生了隱式轉換:

A obj(getA());
...
B obj2 = obj; //喔唷!原意是拷貝一個A對象,卻將obj隱式轉換為其底部的B對象了,然後再復制

很多時候顯式轉換更受歡迎


條款16: 成對使用new和delete時要采取相同形式

看一下這段代碼:

std::string* stringArray = new std::string[100];
…
delete stringArray;

很明顯此程序行為不明確。對象數組所含的100個string對象中的99
個不太可能被適當刪除,因為它們的析構函數很可能沒被調用。

delete最大的問題在於:::即將被刪除的內存之內究竟有多少對象?::這個問題決定了有多少對象的析構函數必須被調用起來。

也就是, 即將被刪除的指針,所指的是單一對象或對象數組? 數組所用的內存通常還包括“數組大小”的記錄,以便delete知道需要調用多少次析構函數。

所以記住,刪除對象數組要用 delete[] xxx;

這種規則對typedef的使用也很重要:

typedef std::string AddressLines[4]; 
std::string* pal = new AddressLines;

此時AddressLines是一個數組,如果這樣new:

std::string* pal = new AddressLines;

就必須匹配 delete[] pal

盡量不要對數組做typedef動作,你大可用STL中的vector等templates


條款17: 以獨立語句將newed對象置入智能指針

假設有這樣一個函數:

void process(std::tr1::shared_ptr<Widget> pw);

process將對傳來的Widget對象運用智能指針
現在考慮調用函數:

process(new Widget);

嘿嘿,這是不能通過編譯的。因為tr1::shared_ptr構造函數需要一個原始指針(raw pointer),但該構造函數是explicit的,無法隱式轉換,可以這麽寫:

process(std::tr1::shared_ptr<Widget>(new Widget));//實際上仍有風險

假設process函數有第二個參數func(),它是一個函數的返回結果:

process(std::tr1::shared_ptr<Widget>(new Widget),func());

調用process之前,編譯器必須創建代碼,做以下三件事:

  1. 調用func()
  2. 執行”new Widget”
  3. 調用tr1::shared_ptr構造函數

對於c++,此行編譯時,有可能編譯器先執行”new Widget”操作:

  1. 執行”new Widget”
  2. 調用func()
  3. 調用tr1::shared_ptr構造函數

然而如果此時func()調用導致異常,”new Widget”返回的指針將會遺失,因為它尚未被置入tr1::shared_ptr內,這將引發資源泄漏。

我們可以使用分離語句,將異常幹擾減小:

std::tr1::shared_ptr<Widget> pw(new Widget);
process(pw,func());

OVER

EffictiveC++筆記 第3章