程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Effective C++讀書筆記(條款24-29)

Effective C++讀書筆記(條款24-29)

編輯:C++入門知識

Effective C++讀書筆記(條款24-29)


Effective C++第四篇,擴展的有點多...


(四).設計與聲明

____________________________________________________________________________________________________________________________________

條款24:若所有參數皆需類型轉換,請為此采用non-member函數

#1.如果你需要為某個函數的所有參數(包括被 this指針所指的那個隱喻參數)進行
類型轉換,那麼這個函數必須是個 non-member。
//假設有個有理數類:
class Rational{
public:
    Rational(int numrator=0, int denomination=1);
    int numerator()const;
    int denomination()const;
    ...
};
//如果operator*寫成Rational成員函數的話:
class Rational{
public:
    ...
    const Rational operator*(const Rational& rhs)const;
    ...
};

Rational oneEigth(1,8);  //很好
Rational oneHalf(1,2);   //很好
Rational result = oneHalf * oneEighth;  //很好
result = result * oneEighth //很好

//當嘗試混合運算時:
result = oneHalf*2;   //result = oneHalf.operator*(2);很好
result = 2*oneHalf;   //result = 2.operator*(oneHalf);出錯,繼續嘗試non-member函數

//當編譯器試圖調用non-member函數 result = operator*(2, oneHalf);時
//依舊沒有找到對應的non-member函數,因此最終導致出錯。

//【所以我們應該實現一個這樣的non-member函數】:
const Rational operator*(const Rational& lhs,const Rational& rhs)
{
    return Rational(lhs.numerator()*rhs.numerator(),
                lhs.denomination()*rhs.denomination());
}

#2.決定上述non-member函數是non-friend函數還是friend函數的理由是
“是否必須對class內部private成員或protected成員進行訪問”,
若需要,則該non-member函數應是friend函數,
若不需要,為滿足最大封裝性原則,該non-member函數應是non-friend函數。
____________________________________________________________________________________________________________________________________
條款25:考慮寫出一個不拋出異常的swap函數
#1.如果swap的缺省實現碼對你的class或class template提供了可接受的效率,
請使用該缺省實現版:
namespace std{
    template
    void swap(T&a, T&b)        //std:swap的典型實現
    {                           //置換a和b的值
        T temp(a);
        a = b;
        b = temp;
    }
}
反之,如果因class或class template使用了pImp手法(pointer to implementation):
class WidgetImpl{
public:
    ...
private:
    int a, b, c;
    std::vector v;
    ...
};
class Widget{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget&rhs)
    {
        ...
        *pImv = *(rhs.pImpl);
        ...
    }
    ...
private:
    WidgetImpl* pImv;
};
從而引起了效率不足,請做以下幾件事來提高效率:
(1).提供一個public swap member函數:
//pImv是private成員變量,所以要從內部來swap,
//當然也可用friend,但這樣的寫法和STL一致
class Widget{
public:
    ...
    void swap(Widget& other)
    {
        using std::swap;
        swap(pImpl, other.pImpl);
    }
    ...
};
//該swap函數保證了異常安全性和高效性,因為swap置換的是指針,對於指針
//和內置類型的置換操作絕不會拋出異常,並且還很高效。如果是對自定義類型
//執行swap函數,則會因為copy構造函數和copy assignment操作符而允許拋出異常。

(2).在你的class或template所在的namespace(如果沒有,則是globe)內提供一個
non-member swap函數,並令它調用上述的swap函數,例如:
namespace WidgetStuff{
    ...
    template
    class Widget {...}
    ...
    template
    void swap(Widget& a, Widget& b)
    {
        a.swap(b);
    }
}

(3).如果正在編寫的是一個class(而非class template),為你的class特化std::swap,
並讓它調用你的swap member函數,例如:
namespace std{
    template<>
    void swap(Widget& a, Widget& b)
    {
        a.swap(b);
    }
}
//=======================================================================
// 理由:
// 好處是即使客戶不慎直接調用了std::swap,至少也能獲得一個全特化版的swap函數,
// 另外,C++只運行class template偏特化,但不允許function template的偏特化,
// 因此無法通過編譯(雖然某些編譯器錯誤的接受了它)。
//=======================================================================

(4).如果你調用swap,請使用using聲明式,以便讓std::swap在函數中曝光可見,從
而在swap直接書寫調用時可獲得如下的效率查找:
<1>.在與class相同的 namespace 中查找swap函數。
<2>.在globe壞境中查找swap函數。
<3>.在namesapce std中查找全特化swap函數(如果是class而非template,並且予以了實現)
<4>.在namesapce std中查找一般swap函數。

#2.在std內進行std templates全特化是好的,但千萬不要在std內加入一些對於std
來說全新的東西,因為C++標准委員會禁止加入那些已經聲明好的東西,即便通過了
編譯和執行,其行為也不能得到明確。
____________________________________________________________________________________________________________________________________

(五).實現

____________________________________________________________________________________________________________________________________

條款26:盡可能延後變量定義式的出現時間
#1.盡可能延後變量定義式的出現,如果可以,要直到初始化為止。
理由1:增加程序的可讀性,使程序結構更清晰。
理由2:對於用戶(或系統)定義類型,過早的定義變量,可能會導致程序在拋出異常,
函數返回等情況下變量沒有使用,因此會帶來額外的構造和析構成本。
(內置類型的定義,可以用C風格初始化,也可以延遲變量定義時間,為了統一起見,
最好延遲變量定義的時間。)
例如:
std::string encryptPassword(const std::string& password)
{
    using namespace std;
    string encrypted;
    if(password.length() < MinimumPasswordLength)
        throw logic_error("Password is too short");
    ...
    
    return encrypted;
}
//若拋出logic_error異常,encrypted完全未被使用。
//所以應改為:
std::string encryptPassword(const std::string& password)
{
    
    if(password.length() < MinimumPasswordLength)
        throw logic_error("Password is too short");
    using namespace std;
    string encrypted(password);
    ...
    return encrypted;
}

#2.方法A和方法B的定義方式各有千秋,如果你知道賦值的成本比“構造+析構”的成本低,
並且注重效率,那麼你應該使用方法A。否則,為了保證程序的可讀性和易維護性,
你應該使用方法B。
//方法A:定義於循環外
Widget w;
for(int i=0;i____________________________________________________________________________________________________________________________________
條款27:盡量少做轉型動作
#1.const_cast,dynamic_cast,reinterpret_cast,static_cast轉型操作符。
const_cast:常量性轉除,唯一有此能力的C++-style轉型操作符。
dynamic_cast:主要用來執行”安全向下轉型“,也就是用來決定某對象是否歸屬
於繼承體系中的某個類型。它是唯一無法由舊式語句執行的動作,也是唯一可能
耗費重大運行成本的轉型動作。
reinterpret_cast:意圖執行低級轉型,實際動作(及結果)可能取決於編譯器,
這也表示它不可移植,例如將一個 pointer to int 轉型為一個 int。這一類
轉型在低級代碼以為很少見。
static_cast:強迫隱式轉換,例如
將 non-contst 轉型為 const,
將 int 轉型為 double,
將 void*指針與 typed指針互轉,
將 pointer-to-base 與 pointer-to-derived互轉,
但無法將 const 轉型為 non-const,因為這個只有const_cast辦的到。

#2.如果可以,盡量避免轉型,特別是在注重效率的代碼中避免dynamic_cast,如果
有個設計需要轉型動作,試著發展無需轉型的替代設計。
理由1:C++規則的設計目標之一是,保證“類型錯誤”絕不發生,為避免在任何對象

身上執行不安全,荒謬的操作,請盡量避免轉型。

理由2:任何一個類型轉換往往令編譯器編譯出運行期間執行的碼,例如:

class Base{...};
class Derived:public Base{...};
Derived d;
Base* pb = &d;
//為獲取Base*指針值,在運行期間會有個偏移量(offset)添加在Derived*指針身上,
//編譯器會為此額外編譯出運行期間執行的碼。(注意:這裡的offset並不明確,
//因為對象的布局方式和地址計算方式隨編譯器的不同而不同。)

理由3:dynamic_cast會耗費重大的運行期成本,在深度繼承中,單單多次的
”class名稱之字符串“的strcmp比較就會耗費一定的效率成本。

【兩種避免dynamic_cast轉型的替代策略】:
(1).在單一繼承中,直接用 derived class 指針替代 base class 接口處理的derived
class 對象,例如:
//假設先Window/SpecialWindow繼承體系中只有SpecialWindows才支持閃爍效果,
//試著不要這樣做:
class Window {...};
class SpecialWindow:public Window{
public:
    void blink();
    ...
};

typedef std::vector> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();++iter){
    if(Specialwindow* psw=dynamic_cast(iter->get()))
        psw->blink();
}
//應該改成這樣:
typedef std::vector> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();++iter){
    (*iter)->blink();
}
(2).多重繼承中,用virtual函數代替dynamic_cast所做的事
//用dynamic_cast時如下所示:
class Window {...};
...
typedef std::vector> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();++iter){
    if(SpecialWindow* psw1=
        dynamic_cast(iter->get()))psw1>blink();
    else if(SpecialWindow* psw2=
        dynamic_cast(iter->get()))psw2->blink();
    else if(SpecialWindow* psw3=
        dynamic_cast(iter->get()))psw3->blink();
}
//以上的做法中有一連串dynamic_cast,這會使產生出來的代碼又長又慢,
//而且基礎不穩,因為繼承體系一有改變,所有這一類代碼都要檢查看看
//是否需要修改,例如添加一個新的derived class,則要在一連串判斷
//中加入新的條件分支,及其不易維護。
//所以應該改成這樣:
class Window {
public:
    virtual void blink();{}   //缺省實現碼什麼也沒做
    ...
};
class SpecialWindow:public Window{
public:
    virtual void blink(){...}; //blink做某些事
    ...
};
...
typedef std::vector> VPW;
VPW winPtrs;
...          //容器,內含Window指針,指向所有可能的派生類對象
for(VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();++iter){
    (*iter)->blink();
}


#3.如果轉型無可避免,請將它隱藏在函數背後,這樣客戶調用該函數時,不需要將

轉型動作放入它們的代碼裡,保證了代碼的封裝性和易維護性。

#4.盡量用C++-style的轉型替換舊式轉型(構造函數除外),因為
(1).它們很容易在代碼中辨別出來,因此簡化了“找出類型系統破壞點”的過程。
(2).轉型動作分門別類,因此更有利於編譯器找出錯誤的運用。
____________________________________________________________________________________________________________________________________
條款28:若返回 handles 指向對象內部成分,請保證返回值 const 約束大於等於成員函數
#1.References, 指針和迭代器通通都是 handles,用來取得某個對象。
#2.若返回handles指向內部對象,並且成員函數是const,請保證該返回值也是const 。
class Point{
public:
    Point(int x, int y);
    ...
    void setX(int newVal);
    void setY(int newVal);
    ...
};
struct RectData{
    Point ulhc;
    Point lrhc;
};
class Rectangle{
    ...
private:
    std::tr1::shared_ptr pData;
    Point& upperLeft()const{return pData->ulhc;}
    Point& lowerRight()const{return pData->lrhc;}
    ...
};
Point coord1(0,0);Point coord2(100, 100);
const Rectangle rec(coord1, coord2);

rec.upperLeft().setX(50);
//這裡返回的handles可以改變,但與upperLeft const()成員函數的本意是相反的。
//所以應改成:
class Rectangle{
    ...
private:
    ...
    const Point& upperLeft()const{return pData->ulhc;}
    const Point& lowerRight()const{return pData->lrhc;}
    ...
};
//這樣就可以在保證讀寫權的同時禁止塗寫權。

【本條為個人觀點:】
#3.對於返回值為自定義(或系統定義)類型的non-const成員函數,返回內部成員
變量的handles(non-const)表示希望該內部變量是public,而對於返回值為內置
類型的成員變量,則該使用pass-by-valued的getXXX()函數和setXXX(X)來分別
區分讀寫行為。
例如:
//當upperLeft(),lowerRight()為non-const時:
class Rectangle{
    ...
private:
    ...
    Point& upperLeft(){return pData->ulhc;}
    Point& lowerRight(){return pData->lrhc;}
    ...
};

Point coord1(0,0);Point coord2(100, 100);
Rectangle rec(coord1, coord2);
//我們可以如下這樣用函數統一接口設置值和得到值:
rec.upperLeft().setX(50);
int val = rec.lowerRight().getY();
____________________________________________________________________________________________________________________________________
條款29:為”異常安全“而努力是值得的
#1.異常安全函數即使發生異常也不會洩露資源或允許任何數據敗壞。
這樣的函數區分三種可能的保證:基本型,強烈型,不拋異常性。
(1).基本型:異常拋出後,class一切約束條件滿足,程序內事物處於一種有效的狀態中。
(2).強烈保證型:異常拋出後,程序狀態不改變,函數回到調用前的狀態。
(3).不拋異常型:保證不拋出任何異常,對內置類型(int,指針,等)身上的所有操作
都提供nothrow保證。
(任何使用動態內存的東西在無法滿足內存需求下,都會拋出bad_alloc異常)

#2.除非調用傳統非安全異常安全碼,否則應保證異常安全,因為傳統非異常安全碼
不具備異常安全性,所以任何調用它的代碼都不具備異常安全性,因此為其撰寫
異常安全碼是麼有任何意義的。

#3.”強烈保證“往往能夠以 copy-and-swap實現出來。
//一個用copy-and-swap的示例:
struct PMImpl{            //PMImpl = PrettyMenu Impl
    std::tr1::shared_ptr bgImage;
    int imageChanges;
};
class PrettyMenu{
    ...
private:
    Mutex mutex;
    std::tr1::shared_ptr pIml;
};
void PrettyMenu::changeBack(std::istream& imgSrc)
{
    using std::swap;
    Lock ml(&mutex);        //獲得mutex的副本數據
    std::tr1::shared_ptr pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc)); //修改副本
    ++pNew->ImageChanges;
    swap(Impl, pNew);        //置換(swap)數據,釋放mutex
}
//,但”強烈保證“ 並非對所有函數都具有可實現或具備實現意義:
(1).因為如copy-and-swap這樣的強烈保證往往會耗費更多的時間和空間,為了
保證更好的效率,應以”基礎保證“來替換,這樣才更具現實意義。

(2).當函數對”非局部性數據”有影響時,提供強烈保證就很難了。
void someFunc{
    ...     //對local狀態做一份副本
    f1();
    f2();
    ...     //將修改後的狀態置換過來
}
//假設someFunc對“局部數據”提供強烈保證,但f1(),f2()對someFunc()來說是
//非局部性的”,因此f1(),f2()會對someFunc()的強烈保證期望造成影響,
//假設f1(),f2()提供一個基本保證,顯然someFunc將只會提供基本保證,
//而若f1(),f2()也提供強烈保證,但若f1(),f2()並未拋出異常,在此之後
//卻拋出了異常,那麼someFunc()也無法提供強烈保證,畢竟程序狀態改變了。

//另一方面,在someFunc中像某些數據庫之類的非局部數據一旦被改變,想恢復
//就很困難,這時someFunc想提供強烈保證就真的很難了。

#4.函數提供的“異常安全保證”最多只等於其所調用之各個函數的“異常安全保證”

中的最弱者(從上述例子中可以看出)。

____________________________________________________________________________________________________________________________________

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved