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

Effective C++筆記:實現

編輯:C++入門知識

條款26:盡可能延後變量定義式的出現時間

博客地址:http://www.cnblogs.com/ronny/ 轉載請注明出處

有些對象,你可能過早的定義它,而在代碼執行的過程中發生了導常,造成了開始定義的對象並沒有被使用,而付出了構造成本來析構成本。

所以我們應該在定義對象時,盡可能的延後,甚至直到非得使用該變量前一刻為止,應該嘗試延後這份定義直到能夠給它初值實參為止。

這樣做的好處是:不僅可以避免構造(析構)非必要對象,還可以避免無意義的default構造行為。

遇到循環怎麼辦?此時往往我們會有兩個選擇:

做法A:1個構造函數+1個析構函數+n個賦值操作 // 在循環外面定變量,在循環內賦值

做法B:n個構造函數+n個析構函數   // 在循環內定義並初始化變量

這時候要估計賦值的成本低還是構造+析構的成本低,另外值得考慮的是對象作用域的問題。

條款27:盡量少做轉型動作

轉型語法通常有三種不同的形式:

1,C風格的轉型動作:(T)expression

2,函數風格的轉型動作:T(expression)

3,上面的第二種被稱為“舊式轉型”,C++提供了四種新式轉型:

const_cast<T>(expression)  //通常用來將對象的常量性轉除

dynamic_cast<T>(expression)  // 轉換為子類,用來決定某對象是否歸屬繼承體系中的某個類型,但是耗費重大運行成本

reinterpret_cast<T>(expression) // 執行低級轉型,實際動作取決於編譯器,這也就表示它不可移植。如將一個point to int 轉型為一個int

static_cast<T>(expression)  // 強迫隱式轉換

在很多派生類的設計中,派生類的virtual函數需要去調用基類的virtual函數,下面是一個例子,window是一個基類,它定義了一個虛函數onResize,而Special Window是一個派生類。

// 博客地址:http://www.cnblogs.com/ronny/ 轉載請注明出處!
class Window
{
public:
    virtual void onResize();
};

class SpecialWindow :public Window
{
public:
    virtual void onResize()
    {
        static_cast<Window>(*this).onResize(); // 將*this轉型為Window,然後調用其onResize
        // ... SpecialWindow專屬動作
    }
};

但是代碼中的相通過轉型來調用基類的virtual函數,事實上static_cast<Window>(*this).onResize()調用的是一個*this的基類成份的一份拷貝的onResize函數,所以onResize操作所能影響到的成員只屬於一個臨時對象。解決的辦法是把轉型拿掉即可,替換成Window::onResize()。

請記住:

如果可以,盡量避免轉型,特別是在注重效率的代碼中避免dynamic_cast。如果有個設計需要轉型動作,試著發展無需轉型的設計。

如果轉型是必需要,試著將它隱藏於某個函數背後。客戶隨後可以調用該函數,而不需將轉型放進他們自己的代碼。

寧可使用C++style(新式)轉型,不要使用舊式轉型,前者很容易被辨識,而且也比較角著分門別類的職掌。

條款28:避免返回handles指向對象的內部成分

class Point
{
public:
    Point(int x, int y);
    void SetX(int newVal);
    void SetY(int newVal);
private:
    int x_cor;
    int y_cor;
};

struct RectData
{
    Point ulhc;    // 矩形左上角的點
    Point lrhc;    // 矩形右上角的點
};
class Rectangle
{
private:
    shared_ptr<RectData> pData;
public:
    Point& upperLeft()const{ return pData->lrhc; }
    Point& lowerRight()const{ return pData->ulhc; }
};

上面的代嗎中Point是表示坐標系中點的類,RectData表示一個矩形的左上角與右下角點的點坐標。Rectangle是一個矩形的類,包含了一個指向RectData的指針。

我們可以看到了uppLeft和lowerRight是兩個const成員函數,它們的功能只是想向客戶提供兩個Rectangle相關的坐標點,而不是讓客戶修改Rectangle。但是兩個函數卻都返回了references指向了private內部數據,調用者於是可以通過references更改內部數據。

這給了我們一些警示:成員變量的封裝性只等於“返回其reference”的函數的訪問級別;如果const成員函數傳出一個reference,後者所指數據與對象自身有關聯,而它又被存儲於對象之外,那麼這個函數的調用者可以修改那筆數據。

handles(號碼牌,用於取得某個對象)指reference、指針和迭代器,它們返回一個“代表對象內部數據”的handle。

我們可以對上面的成員函數返回類型上加上const來解決問題:

public:
    const Point& upperLeft()const{ return pData->lrhc; }
    const Point& lowerRight()const{ return pData->ulhc; }

但是函數返回一個handle代表對象內部成分還總是危險的,因為可能會造成dangling handles(空懸的號牌)。比如某個函數返回GUI對象的外框(bounding box)。

class GUIObject{
    //..
};
const Rectangle boundingBox(const GUIObject&obj);        
GUIObject* pgo;
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());

對boundingBox的調用返回的是一個臨時對象,這個對象沒有名稱,隨後我們調用了這個對象的upperLeft,返回了一個指向臨時對象內部數據的reference。但是在語句結束後,這個臨時對象會被銷毀,pUpperLeft會變成空懸的、虛吊的(dangling)!

請記住

避免返回handles(包括reference、指針、迭代器)指向對象內部。遵守這個條款可以增加封裝性,幫助const成員函數的行為像個const,並將發生“虛吊號碼牌”的可能性降至最低。

條款29:為“異常安全”而努力是值得的

異常安全函數即使發生異常也不會洩漏資源或允許任何數據結構敗壞。這樣的函數區分為三種可能的保證:基本型、強烈型、不拋異常型。

強烈保證往往能夠以copy-and-swap實現出來,但“強烈保證”並非對所有函數都可以實現或具備現實意義。

函數提供的“異常安全保證”通常最高只等於其所調用之各個函數的“異常安全保證”中最弱者。

條款30:透徹了解inlining的裡裡外外

關於inline:

1. inline函數的調用,是對函數本體的調用,是函數的展開,使用不當會造成代碼膨脹。

2. 大多數C++程序的inline函數都放在頭文件,inlining發生在編譯期。

3. inline函數只代表“函數本體”,並沒有“函數實質”,是沒有函數地址的。

值得注意的是:

1. 構造函數與析構函數往往不適合inline。因為這兩個函數都包含了很多隱式的調用,而這些調用付出的代價是值得考慮的。可能會有代碼膨脹的情況。

2. inline函數無法隨著程序庫升級而升級。因為大多數都發生在編譯期,升級意味著重新編譯。

3. 大部分調試器是不能在inline函數設斷點的。因為inline函數沒有地址。

請記住

1. 大多數inlining限制在小型、被頻繁調用的函數身上。這可使日後的調試過程和二進制升級更容易,也可以使潛在的代碼膨脹問題最小化,使程序的速度提升機會最大化。

2. 另外,對function templates的inline也要慎重,保證其所有實現的函數都應該inlined後再加inline。

條款31:將文件間的編譯依存關系降至最低

這個問題產生是源於希望編譯時影響的范圍盡量小,編譯效率更高,維護成本更低,這一需求。

實現這個目標首先第一個想到的就是,聲明與定義的分離,用戶的使用只依賴聲明,而不依賴定義(也就是具體實現)。

但C++的Class的定義式卻不僅僅只有接口,還有實現細目(這裡指實現接口需要的私有成員)。而有時候我們需要修改的通常是接口的實現方法,而這一修改可能需要添加私有變量,但這個私有變量對用戶是不應該可見的。但這一修改卻放在了定義式的頭文件中,從而造成了,使用這一頭文件的所有代碼的重新編譯。

於是就有了pimpl(pointer to implementation)的方法。用pimpl把實現細節隱藏起來,在頭文件中只需要一個聲明就可以,而這個poniter則作為private成員變量供調用。

這裡會有個有意思的地方,為什麼用的是指針,而不是具體對象呢?這就要問編譯器了,因為編譯器在定義變量時是需要預先知道變量的空間大小的,而如果只給一個聲明而沒有定義的話是不知道大小的,而指針的大小是固定的,所以可以定義指針(即使只提供了一個聲明)。

這樣把實現細節隱藏了,那麼實現方法的改變就不會引起別的部分代碼的重新編譯了。而且頭文件中只提供了impl類的聲明,而基本的實現都不會讓用戶看見,也增加了封裝性。

結構應該如下:

class AImpl;
class A {
public:
    ...
private:
    std::tr1::shared_ptr<AImpl> pImpl;
};

這一種類也叫handle class

另一種實現方法就是用帶factory函數的interface class。就是把接口都寫成純虛的,實現都在子類中,通過factory函數或者是virtual構造函數來產生實例。

聲明文件時這麼寫:

class Person
{
public:
    static shared_ptr<Person> create(const string&,
        const Data&,
        const Adress&);
};

定義實現的文件這麼寫

class RealPerson :public Person
{
public:
    RealPerson(...);
    virtual ~RealPerson(){}
    //...
private:
    // ...
};

以上說的為了去耦合而使用的方法不可避免地會帶上一些性能上的犧牲,但作者建議是發展過程中使用以上方法,當以上方法在速度與/或大小上的影響比耦合更大時,再寫成具體對象來替換以上方法。

請記住:

支持“編譯依存性最小化”的一般構想是:相依於聲明式,不要相信於定義式。基於此構想的兩個手段是Handles classes和Interface classes。

程序庫頭文件應該以“完全且僅有聲明式”的形式存在,這種做法不論是否涉及templates都適用。

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