---
toc:
? ? depth_from: 1
? ? depth_to: 6
? ? ordered: false
html:
? ? embed_local_images: true
? ? embed_svg: true
? ? offline: false
? ? toc: true
print_background: false
export_on_save:
? ? html: true
---
# 讓自己習慣C++
## 確定對象使用前已經初始化
1. 內置類型
? ? 對于內置類型,賦值和初始化成本相同。建議使用前賦初始值。
2. 對于自定義類型
? ? C++規定,類中成員變量的初始化動作發生在進入構造函數本體之前。如果直接在構造函數中直接賦值,成員變量是經歷過初始化和然后在構造函數中賦值兩個操作。
? ? 所以建議,在構造函數中使用成員初始化列表(member initialization list)。
? ? ```C++
? ? class MyObject
? ? {
? ? ? ? public:
? ? ? ? ? ? Object(Object object, int x)
? ? ? ? ? ? ? ? :object(object),
? ? ? ? ? ? ? ? x(x)
? ? ? ? ? ? {}
? ? ? ? ? ? Object()
? ? ? ? ? ? ? ? :object(),
? ? ? ? ? ? ? ? x(0)
? ? ? ? ? ? {}
? ? ? ? private:
? ? ? ? ? ? Object object;
? ? ? ? ? ? int x;
? ? }
? ? ```
? ? 注意:如果有構造函數沒有入參,記得把成員內置對象初始化。同時,成員初始化列表中變量順序并不是初始化順序,而是成員變量在類中定義的順序決定。為了避免迷惑,建議和定義順序保持一致。
3. non-local static對象
? ? C++對定義不同編譯單元內的non-local static對象的初始化次序沒有明確定義。而相反,對local static對象初始化時機有明確規定,就是對一次調用它的時候。
? ? 注:編譯單元是產生單一目標文件的一些源碼,基本是它的單一源碼文件和include的頭文件。
? ? 所以,使用包含local static對象的reference-returning函數來代替non-local static對象。
? ? ```C++
? ? Object& Object()
? ? {
? ? ? ? static Object object;
? ? ? ? return object;
? ? }
? ? ```
? ? 但是,在多線程中因為這些reference-returning函數含有static對象,導致行為不確定。應該來說,只要是non-const static對象在多線程都有問題。???
? ? 建議:在程序單線程啟動階段,手工調用所有的reference-returning函數,這樣就可消除與初始化有關的競速問題了。
# 構造/析構/賦值運算
## 請給基類聲明virtual析構函數
### 問題
例如,factory函數一般返回基類指針,指針指向heap中的一片內存。但是最后使用完畢后delete對調用基類的析構函數。
但是C++指出,**當derived對象經由base指針delete掉,而該基類帶有一個no-virtual析構函數,其行為未定義。一般是調用基類的析構函數,銷毀掉基類部分。這樣就導致內存泄露的問題。**
### 方法
請給為了**多態用途**的基類聲明virtual析構函數。
一般的,一個class帶有virtual函數,表示它被當作base class。所以,任何class只要帶有virtual函數幾乎應該有一個virtual析構函數。
**注意:有時想把一個class聲明為抽象類,但手頭上沒有pure virtual函數,可以把析構函數設為pure virtual。因為抽象類一般是多態用途的base類,而base類幾乎都建議析構是virtual的**
并非所有base類都是多態用途,例如uncopyable類,這些基類就用需要virtual析構函數。
### 原理
如果析構函數不是virtual,則調用的函數在編譯時已經確定,由于指針是基類指針,所以調用的就是基類的析構函數;
如果析構函數是virtual,則調用的函數在運行期間確定,對象會有一個虛表指針,指向一個虛表數組,元素是函數指針,指向對應的子類virtual函數,所以調用的是子類的析構函數。
## Uncopyable類
### 問題
### 方法
* 方法一
? ? 把拷貝構造函數和賦值操作符聲明為私有的。
? ? 優點是實現簡單,但是類的成員函數和友元函數可以使用私有函數,解決方法是拷貝構造函數和賦值操作符只聲明不定義,這樣在鏈接期會報錯。
? ? ```C++
? ? class MyClass{
? ? ? ? private:
? ? ? ? ? ? MyClass(const MyClass&);? ? ? ? ? ? //只聲明,不定義
? ? ? ? ? ? MyClass& operator=(const MyClass&); //只聲明,不定義
? ? ? ? ...
? ? }
? ? ```
* 方法二
? ? 繼承禁止拷貝類Uncopyable。這樣如果使用MyClass類的拷貝構造函數或者賦值操作符,會調用基類對應的函數,由于基類是私有函數,則編譯器會報錯。
? ? 優點是把鏈接期報錯問題提前到編譯期。
? ? ```C++
? ? class Uncopyable{
? ? ? ? protected:
? ? ? ? ? ? Uncopyable(){}
? ? ? ? ? ? ~Uncopyable(){}
? ? ? ? private:
? ? ? ? ? ? Uncopyable(const Uncopyable&);
? ? ? ? ? ? Uncopyable& operator=(const Uncopyable&);
? ? }
? ? class MyClass:public Uncopyable{}
? ? ```
? ? **或者你可以使用Boost提供的版本,叫做noncopyable的class。**
# 資源管理
## RAII類管理資源(13 14)
為防止資源泄露,請使用RAII(Resource Acquisition Is Initialization,取得資源就初始化,被銷毀就釋放資源)對象。
### 智能指針
兩個常被使用的RAII類是tr1::shared_ptr(regerence-counting smart pointer引用計數型智能指針)和auto_ptr(智能指針)。
* auto_ptr,是個類指針對象,也就是所謂的智能指針,含義是當指針銷毀,會自動delete所指對象。注意,拷貝時會把對象地址傳給拷貝者,而原有指針為null。
? ? ```C++
? ? void f()
? ? {
? ? ? ? std::auto_ptr<Resource> pRsc1(resourceFactory());
? ? ? ? /*使用pRsc資源*/
? ? ? ? std::auto_ptr<Resource> pRsc2(pRsc1);//現在pRsc2指向資源,而pRsc1為null
? ? ? ? pRsc1 = pRsc2;//現在pRsc1指向資源,而pRsc2為null
? ? ? ? /*運行到最后銷毀,會調用Resource析構函數釋放資源*/
? ? }
? ? ```
* tr1::shared_ptr,是regerence-counting smart pointer引用計數型智能指針。與auto_ptr不同的是,可以拷貝,只有當所有指針都銷毀時,delete所指對象。**通常share_ptr是RAII類的最佳選擇**
? ? ```C++
? ? void f()
? ? {
? ? ? ? std::tr1::shared_ptr<Resource> pRsc1(resourceFactory());
? ? ? ? /*使用pRsc資源*/
? ? ? ? std::auto_ptr<Resource> pRsc2(pRsc1);//現在pRsc1、pRsc2都指向資源
? ? ? ? pRsc1 = pRsc2;//同上,無任何改變
? ? ? ? /*運行到最后,當pRsc1、pRsc2都銷毀,會調用Resource析構函數釋放資源*/
? ? }
? ? ```
? ? 當然,如果釋放資源不是簡單的delete內存,還需要做其他事情,比如文件關閉,數據庫關閉,這時智能指針shared_ptr可以指定某一函數為刪除器。形式是:`std::tr1::shared_ptr<Resource> pRsc1(resourceFactory(), deleter)`其中deleter是某一函數指針,當沒有智能指針指向該資源,會調用deleter函數。**而auto_ptr則沒有這個設定**。
注意:這兩個智能指針都是delete對象,當對象是個對象數組時,則不會調用delete[],則可以使用boost::scoped_array和boost::shared_array類來代替。
### 自定義RAII類
自定義的好處是可以定義想要的行為。
首先看下RAII類:
```C++
class ResourceManage
{
? ? public:
? ? ? ? explicit ResourceManage()
? ? ? ? {
? ? ? ? ? ? pRsc = resourceFactory();
? ? ? ? }
? ? ? ? ~ResourceManage()
? ? ? ? {
? ? ? ? ? ? resouceDestroy(pRsc);
? ? ? ? }
? ? private:
? ? ? ? Resource *pRsc;
}
```
自定義行為:
1. 禁止復制
? ? 方法是,使用繼承禁止拷貝類來實現`class ResourceManage:private Uncopyable{...}`
2. 對底層資源使用引用計數
? ? 方法是,RAII內部的私有資源指針換成share_ptr來實現
3. 轉移底部資源的擁有權
? ? 方法是,把RAII內部的私有資源指針換成auto_ptr來實現
4. 復制底部資源
? ? 方法是,重載拷貝構造函數和賦值運算符
## 通過RAII類使用資源
RAII類并不是封裝資源,而是為了確保資源釋放一定會發生。但是由于把資源或資源指針作為私有成員,這個類也阻礙我們對資源的使用。例如:
```C++
class Resource
{
? ? ...
? ? doSomeThing(){...}//資源類有個api會對資源做些事情
? ? ...
}
void f()
{
? ? ResourceManage rscMag;//資源獲取
? ? rscMag->doSomeThing()//錯誤!!!doSomeThing不是ResourceManage的成員方法
}
```
方法:
1. 顯示轉換
? ? 顯示的提供get函數,把內部資源或資源指針返回。
? ? 注意,由于ResourceManage類不是為了封裝資源,所以通過公有函數把私有成員返回也很正常。這樣不僅隱藏客戶不需要看到的部分,但同時也為客戶全面準備好所有東西。
? ? ```C++
? ? rscMag.get()->doSomeThing()
? ? ```
? ? 相似地,智能指針tr1::shared_ptr和auto_ptr也有get成員函數,把自己轉為普通指針。
2. 隱式轉換
? ? RAII類,改良版
? ? ```C++
? ? class ResourceManage
? ? {
? ? ? ? public:
? ? ? ? ? ? explicit ResourceManage()
? ? ? ? ? ? {
? ? ? ? ? ? ? ? pRsc = resourceFactory();
? ? ? ? ? ? }
? ? ? ? ? ? ~ResourceManage()
? ? ? ? ? ? {
? ? ? ? ? ? ? ? resouceDestroy(pRsc);
? ? ? ? ? ? }
? ? ? ? ? ? operator Resource() const{return pRsc;}//定義隱式轉換Resource類型函數
? ? ? ? private:
? ? ? ? ? ? Resource *pRsc;
? ? }
? ? ```
注意:
? ? 雖然隱式轉換更符合書寫,但是畢竟是隱私轉換,可能增加錯誤轉換的風險。所以一般顯式轉換比較安全,隱式轉換比較方便。
## 以獨立語句將newed對象置入智能指針
用RAII類管理資源,或多或少的使用智能指針。而資源對象初始化后賦值給智能指針,需要單獨一句。如下:
```C++
//這里資源初始化和使用放到一條語句中。
//C++并沒有定義同一條語句,邏輯上沒有關聯的步驟的執行先后順序。
//如果在new Resource執行后執行getPara()異常終端,資源指針還沒有傳給智能指針。這樣會導致后面資源始終沒有釋放。
doSomeThing(std::tr1::shared_ptr<Resource>(getResource()), getPara());
```
應該這樣做:
```C++
std::tr1::shared_ptr<Resource> pRsc(getResource());//像這些關鍵步驟最后單獨一個語句
doSomeThing(pRsc, getPara());
```
注意:其中getResource是factory函數,其返回Resource的指針。為了防止用戶沒有及時把指針傳給智能指針,可以把factory函數設計成返回智能指針。
# 設計與聲明
## 讓接口容易被正確使用,不易被誤用
比如一個接口是設置日期,但是參數容易誤用。
```C++
void setDate(int month, int day, int year);//setDate的聲明
setDate(30, 3, 1999);//錯誤,應該是3, 30, 1999的
setDate(3, 32,1995);//錯誤,3月沒有32號
```
一種方法是把入參class化或者struct化。
```C++
void setDate(const Month& m, const Day& d, const Year& y);//setDate的聲明
struct Day{
? ? explicit Day(int d):val(d){}
? ? int val;
}
...
setDate(Day(30), Month(3), Year(1995));
```
但有時候,參數表示的意義不能用基本類型來表示。比如獲取日期的其中一個字段。
```C++
int getNumByField(String field);
int year = getNumByField("yeer");//錯誤,應該是year的
```
大部分人會想到使用宏或者枚舉,但是這些都是基本類型,還是容易犯錯。可以使用如下:
```C++
int getNumByField(const DateField& dateField);
class DateField{
? ? public:
? ? ? ? static DateField year(){return DateField("year");}
? ? ...
? ? private:
? ? ? ? explicit Month(String field):field(field){};
? ? ? ? String field;
}
getNumByField(DateField.year());
```
## 用pass-by-reference 替換 pass-by-value
### 問題
```c++
//函數聲明
void doSomeThing(Base b);
class Base{};
class Drive:public Base{};
//函數使用
void doSomeThing(Drive d);
```
其中Drive類為實參,而Base類為形參。參數傳遞時會調用Base類的拷貝構造函數,進而會把Drive的特性丟失掉,造成“切割問題”。
### 方法
用pass-by-reference 替換 pass-by-value。其實C++編譯器底層就是用指針實現引用的。改為指針也能解決,但會引入指針相關的問題。
注意:內置類型大部分比指針簡單,所以內置類型還是推薦使用pass-by-value