Effective C++讀書筆記
阿新 • • 發佈:2019-02-18
但是,我們不該令拷貝賦值操作符呼叫拷貝建構函式,也不該令拷貝建構函式呼叫拷貝賦值操作符。想想,一個是拷貝(建立物件),一個是賦值(物件已經存在)。
請記住:
所謂資源就是,一旦用了它,將來必須還給系統。C++程式中最常使用的資源就好似動態分配記憶體(如果你new了,卻忘了delete,會導致記憶體洩露),但記憶體只是你必須管理的眾多資源之一。其它常見的有檔案描述符(file descriptors)、互斥器(mutex)、圖形介面中的字形和畫刷。資料庫連線以及網路sockets。當你不使用它們時,記得還給系統。
當你考慮到異常、函式內多重回傳路徑、程式維護員改動軟體卻沒能充分理解隨之而來的衝擊,那麼資源管理就顯得複雜的多。
條款13:以物件管理資源
例:
void f()
{
Investment *pInv = createInvestment();
... //這裡存在諸多“不定因素”,可能造成delete pInv;得不到執行,這可能就存在潛在的記憶體洩露。
delete pInv;
}
解決方法:把資源放進物件內,我們便可依賴C++的“解構函式自動呼叫機制”確保資源被釋放。
許多資源被動態分配於堆內而後被用於單一區塊或函式內。它們應該在控制流離開那個區塊或函式時被釋放。標準程式庫提供的auto_ptr 正是針對這種形勢而設計的特製產品。auto_ptr是個“類指標物件”,也就是所謂的“智慧指標”,其解構函式自動對其所指物件呼叫delete。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //函式退出,auto_ptr呼叫解構函式自動呼叫delete,刪除pInv;無需顯示呼叫delete。
“以物件管理資源”的兩個關鍵想法:
看下面例子:
std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::auto_ptr<Investment> pInv2(pInv1); //現在pInv2指向物件,而pInv1被設為NULL;
pInv1 = pInv2; //現在pInv1指向物件,而pIn2被設為NULL;
受auto_ptr管理的資源必須絕對沒有一個以上的auto_ptr同時指向它。即“有你沒我,有我沒你”。
auto_ptr的替代方案是“引用計數型智慧指標”(reference-counting smart pointer;SCSP)、它可以持續跟蹤共有多少物件指向某筆資源,並在無人指向它時自動刪除該資源。
TR1的tr1::shared_ptr就是一個"引用計數型智慧指標"。
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv1,pInv2指向同一個物件;
pInv1 = pInv2; //同上,無變化
...
} //函式退出,pInv1,pInv2被銷燬,它們所指的物件也竟被自動釋放。
auto_ptr和tr1::shared_ptr都在其解構函式內做delete而不是delete[],也就意味著在動態分配而得的陣列身上使用auto_ptr或tr1::shared_ptr是個潛在危險,資源得不到釋放。也許boost::scoped_array和boost::shared_array能提供幫助。還有,vector和string幾乎總是可以取代動態分配而得的陣列。
請記住:
我們在條款13中討論的資源表現在堆上申請的資源,而有些資源並不適合被auto_ptr和tr1::shared_ptr所管理。可能我們需要建立自己的資源管理類。
例:
void lock(Mutex *pm); //鎖定pm所指的互斥量
unlock(Mutex *pm); //將pm解除鎖定
我們建立的資源管理類可能會是這樣:
class Lock
{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
但是,如果Lock物件被複制,會發生什麼事???
“當一個RAII物件被複制,會發生什麼事?”大多數時候你會選擇一下兩種可能:
class Lock
{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm, unlock) //由於tr1::shared_ptr預設行為是”當引用計數為0時刪除其所指物“,幸運的是 //我們可以指定”引用計數“為9時被呼叫的所謂”刪除器“,即第二個引數unlock
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
本例中,並沒說明解構函式,因為沒有必要。編譯器為我們生成的解構函式會自動呼叫其non-static成員變數(mutexPtr)的解構函式。而mutexPtr的解構函式會在互斥量”引用計數“為0時自動呼叫tr1::shared_ptr的刪除器(unlock)。
Copying函有可能被編譯器自動創建出來,因此除非編譯器所生成版本做了你想要做的事,否則你得自己編寫它們。
請記住:
前幾個條款提到的資源管理類很棒。它們是你對抗資源洩漏的堡壘。但這個世界並不完美,許多APIs直接指涉資源,這時候我們需要直接訪問原始資源。
這時候需要一個函式可將RAII物件(如tr1::shared_ptr)轉換為其所內含之原始資源。有兩種做法可以達成目標:顯示轉換和隱式轉換。
tr1::shared_ptr和auto_ptr都提供一個get成員函式,用來執行顯示轉換,也就是返回智慧指標內部的原始指標(的復件)。就像所有智慧指標一樣, tr1::shared_ptr和auto_ptr也過載了指標取值操作符(operator->和operator*),它們允許隱式轉換至底部原始指標。(即在對智慧指標物件實施->和*操作時,實際被轉換為被封裝的資源的指標。)
class Font
{
public:
...
FontHandle get() const //FontHandle 是資源; 顯示轉換函式
{
return f;
}
operator FontHandle() const //隱式轉換 這個值得注意,可能引起“非故意之型別轉換”
{
return f;
}
...
};
是否該提供一個顯示轉換函式(例如get成員函式)將RAII類轉換為其底部資源,或是應該提供隱式轉換,答案主要取決於RAII類被設計執行的特定工作,以及它被使用的情況。
顯示轉換可能是比較受歡迎的路子,但是需要不停的get,get;而隱式轉換又可能引起“非故意之型別轉換”。
請記住:
先看下一下程式碼:
std::string *stringArray = new std::string[100];
...
delete stringArray;
使用了new動態申請了資源,也呼叫了delete釋放了資源。但這程式碼存在“不明確行為”。stringArray物件中的99個不太可能被適當刪除,因為它們的解構函式很可能沒被呼叫。
當我們使用new,有兩件事情發生:第一,記憶體被分配出來;第二,針對此記憶體會有一個(或更多)建構函式被呼叫。當你使用delete,也有兩件事發生:針對此記憶體會有一個(或多個)解構函式被呼叫,然後記憶體才被釋放。delete的最大問題在於:即將被刪除的記憶體之內究竟有多少物件?這個問題的答案決定了有多少個解構函式必須被呼叫起來。
解決以上問題事實上很簡單:如果你呼叫new時使用[],你必須在對應呼叫delete時也使用[]。如果你呼叫new時沒有使用[],那麼也不該在對應呼叫delete時使用[]。
最好儘量不要對陣列形式作typedefs動作。因為這樣容易引起delete操作的“疑惑”(需不需要[]呢???)。
請記住:
為了避免資源洩漏的危險,最好在單獨語句內以智慧指標儲存newed所得物件。
即:
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
std::tr1::shared_ptr<Widget> pw(new Widget); //即在傳入函式之前對智慧指標初始化,而不是在傳入引數中 //對其初始化,因為那樣可能引起操作序列的問題。
processWidget(pw, priority());
請記住:
所謂軟體設計,是“令軟體做出你希望它做的事情”的步驟和做法,通常以頗為一般性的構想開始,最終變成十足的細節,以允許特殊介面的開發。
條款18:讓介面容易被正確使用,不易被誤用
理想上,如果客戶企圖使用某個介面而卻沒有獲得他所預期的行為,這個程式碼不該通過編譯;如果程式碼通過了編譯,它的作為就該是客戶所想要的。
欲開發一個“容易被正確使用,不容易被誤用”的介面,首先必須考慮客戶可能做出什麼樣的錯誤。
許多客戶端錯誤可以因為匯入新型別而獲得預防。在防範“不值得擁有的程式碼”上,型別系統是你的主要同盟國。
struct Day
{
explicit Day(int d) //explicit 避免隱式的轉換。
:val(d) {}
int val;
}; 對日期進行類似的型別封裝,能有效地避免不恰當的日期賦值。
“除非有好的理由,否則應該儘量令你的型別(定義的類)的行為與內建型別一致”。
請記住:
- Copying函式應該確保複製“物件內的所有成員變數”及“所有基類成員”;
- 不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。
所謂資源就是,一旦用了它,將來必須還給系統。C++程式中最常使用的資源就好似動態分配記憶體(如果你new了,卻忘了delete,會導致記憶體洩露),但記憶體只是你必須管理的眾多資源之一。其它常見的有檔案描述符(file descriptors)、互斥器(mutex)、圖形介面中的字形和畫刷。資料庫連線以及網路sockets。當你不使用它們時,記得還給系統。
當你考慮到異常、函式內多重回傳路徑、程式維護員改動軟體卻沒能充分理解隨之而來的衝擊,那麼資源管理就顯得複雜的多。
條款13:以物件管理資源
例:
void f()
{
Investment *pInv = createInvestment();
... //這裡存在諸多“不定因素”,可能造成delete pInv;得不到執行,這可能就存在潛在的記憶體洩露。
delete pInv;
}
解決方法:把資源放進物件內,我們便可依賴C++的“解構函式自動呼叫機制”確保資源被釋放。
許多資源被動態分配於堆內而後被用於單一區塊或函式內。它們應該在控制流離開那個區塊或函式時被釋放。標準程式庫提供的auto_ptr
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //函式退出,auto_ptr呼叫解構函式自動呼叫delete,刪除pInv;無需顯示呼叫delete。
“以物件管理資源”的兩個關鍵想法:
- 獲得資源後立刻放進管理物件內(如auto_ptr)。每一筆資源都在獲得的同時立刻被放進管理物件中。“資源取得時機便是初始化時機”(Resource Acquisition Is Initialization;RAII)。
- 管理物件運用解構函式確保資源被釋放。即一旦物件被銷燬,其解構函式被自動呼叫來釋放資源。
看下面例子:
std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::auto_ptr<Investment> pInv2(pInv1); //現在pInv2指向物件,而pInv1被設為NULL;
pInv1 = pInv2; //現在pInv1指向物件,而pIn2被設為NULL;
受auto_ptr管理的資源必須絕對沒有一個以上的auto_ptr同時指向它。即“有你沒我,有我沒你”。
auto_ptr的替代方案是“引用計數型智慧指標”(reference-counting smart pointer;SCSP)、它可以持續跟蹤共有多少物件指向某筆資源,並在無人指向它時自動刪除該資源。
TR1的tr1::shared_ptr就是一個"引用計數型智慧指標"。
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv1,pInv2指向同一個物件;
pInv1 = pInv2; //同上,無變化
...
} //函式退出,pInv1,pInv2被銷燬,它們所指的物件也竟被自動釋放。
auto_ptr和tr1::shared_ptr都在其解構函式內做delete而不是delete[],也就意味著在動態分配而得的陣列身上使用auto_ptr或tr1::shared_ptr是個潛在危險,資源得不到釋放。也許boost::scoped_array和boost::shared_array能提供幫助。還有,vector和string幾乎總是可以取代動態分配而得的陣列。
請記住:
- 為防止資源洩漏,請使用RAII物件,它們在建構函式中獲得資源並在解構函式中釋放資源。
- 兩個常被使用的RAII類分別是auto_ptr和tr1::shared_ptr。後者通常是較佳選擇,因為其拷貝行為比較直觀。若選擇auto_ptr,複製動作會使他(被複制物)指向NULL。
我們在條款13中討論的資源表現在堆上申請的資源,而有些資源並不適合被auto_ptr和tr1::shared_ptr所管理。可能我們需要建立自己的資源管理類。
例:
void lock(Mutex *pm); //鎖定pm所指的互斥量
unlock(Mutex *pm); //將pm解除鎖定
我們建立的資源管理類可能會是這樣:
class Lock
{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
但是,如果Lock物件被複制,會發生什麼事???
“當一個RAII物件被複制,會發生什麼事?”大多數時候你會選擇一下兩種可能:
- 禁止複製。如果複製動作對RAII類並不合理,你便應該禁止之。禁止類的copying函式參見條款6。
- 對底層資源使用”引用計數法“。有時候我們又希望保有資源,直到它的最後一個使用者被銷燬。這種情況下複製RAII物件時,應該將資源的”被引用計數“遞增。tr1::shared_ptr便是如此。
class Lock
{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm, unlock) //由於tr1::shared_ptr預設行為是”當引用計數為0時刪除其所指物“,幸運的是 //我們可以指定”引用計數“為9時被呼叫的所謂”刪除器“,即第二個引數unlock
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
本例中,並沒說明解構函式,因為沒有必要。編譯器為我們生成的解構函式會自動呼叫其non-static成員變數(mutexPtr)的解構函式。而mutexPtr的解構函式會在互斥量”引用計數“為0時自動呼叫tr1::shared_ptr的刪除器(unlock)。
Copying函有可能被編譯器自動創建出來,因此除非編譯器所生成版本做了你想要做的事,否則你得自己編寫它們。
請記住:
- 複製RAII物件必須一併複製它所管理的資源,所以資源的copying行為決定RAII物件的copying行為。
- 普遍而常見的RAII類拷貝行為是:抑制拷貝,施行引用計數法。不過其它行為也可能被實現。
前幾個條款提到的資源管理類很棒。它們是你對抗資源洩漏的堡壘。但這個世界並不完美,許多APIs直接指涉資源,這時候我們需要直接訪問原始資源。
這時候需要一個函式可將RAII物件(如tr1::shared_ptr)轉換為其所內含之原始資源。有兩種做法可以達成目標:顯示轉換和隱式轉換。
tr1::shared_ptr和auto_ptr都提供一個get成員函式,用來執行顯示轉換,也就是返回智慧指標內部的原始指標(的復件)。就像所有智慧指標一樣, tr1::shared_ptr和auto_ptr也過載了指標取值操作符(operator->和operator*),它們允許隱式轉換至底部原始指標。(即在對智慧指標物件實施->和*操作時,實際被轉換為被封裝的資源的指標。)
class Font
{
public:
...
FontHandle get() const //FontHandle 是資源; 顯示轉換函式
{
return f;
}
operator FontHandle() const //隱式轉換 這個值得注意,可能引起“非故意之型別轉換”
{
return f;
}
...
};
是否該提供一個顯示轉換函式(例如get成員函式)將RAII類轉換為其底部資源,或是應該提供隱式轉換,答案主要取決於RAII類被設計執行的特定工作,以及它被使用的情況。
顯示轉換可能是比較受歡迎的路子,但是需要不停的get,get;而隱式轉換又可能引起“非故意之型別轉換”。
請記住:
- APIs往往要求訪問原始資源,所以每一個RAII類應該提供一個“取得其所管理之資源”的方法。
- 對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。
先看下一下程式碼:
std::string *stringArray = new std::string[100];
...
delete stringArray;
使用了new動態申請了資源,也呼叫了delete釋放了資源。但這程式碼存在“不明確行為”。stringArray物件中的99個不太可能被適當刪除,因為它們的解構函式很可能沒被呼叫。
當我們使用new,有兩件事情發生:第一,記憶體被分配出來;第二,針對此記憶體會有一個(或更多)建構函式被呼叫。當你使用delete,也有兩件事發生:針對此記憶體會有一個(或多個)解構函式被呼叫,然後記憶體才被釋放。delete的最大問題在於:即將被刪除的記憶體之內究竟有多少物件?這個問題的答案決定了有多少個解構函式必須被呼叫起來。
解決以上問題事實上很簡單:如果你呼叫new時使用[],你必須在對應呼叫delete時也使用[]。如果你呼叫new時沒有使用[],那麼也不該在對應呼叫delete時使用[]。
最好儘量不要對陣列形式作typedefs動作。因為這樣容易引起delete操作的“疑惑”(需不需要[]呢???)。
請記住:
- 如果你在new表示式中使用[],必須在相應的delete表示式中也使用[]。如果你在new表示式中不使用[],一定不要在相應的delete表示式中使用[]。
為了避免資源洩漏的危險,最好在單獨語句內以智慧指標儲存newed所得物件。
即:
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
std::tr1::shared_ptr<Widget> pw(new Widget); //即在傳入函式之前對智慧指標初始化,而不是在傳入引數中 //對其初始化,因為那樣可能引起操作序列的問題。
processWidget(pw, priority());
請記住:
- 以獨立語句將newed物件儲存於(置入)智慧指標內。如果不這樣做,一旦異常丟擲,有可能導致難以察覺的資源洩漏。
所謂軟體設計,是“令軟體做出你希望它做的事情”的步驟和做法,通常以頗為一般性的構想開始,最終變成十足的細節,以允許特殊介面的開發。
條款18:讓介面容易被正確使用,不易被誤用
理想上,如果客戶企圖使用某個介面而卻沒有獲得他所預期的行為,這個程式碼不該通過編譯;如果程式碼通過了編譯,它的作為就該是客戶所想要的。
欲開發一個“容易被正確使用,不容易被誤用”的介面,首先必須考慮客戶可能做出什麼樣的錯誤。
許多客戶端錯誤可以因為匯入新型別而獲得預防。在防範“不值得擁有的程式碼”上,型別系統是你的主要同盟國。
struct Day
{
explicit Day(int d) //explicit 避免隱式的轉換。
:val(d) {}
int val;
}; 對日期進行類似的型別封裝,能有效地避免不恰當的日期賦值。
“除非有好的理由,否則應該儘量令你的型別(定義的類)的行為與內建型別一致”。