C++編程規範 4 類

來源:互聯網
上載者:User

4 類

  4.1  類的設計

類是物件導向設計的基礎,一個好的類應該職責單一,介面清晰、少而完備,類間低耦合、類內高內

聚,並且很好地展現封裝、繼承、多態、模組化等特性。

原則4.1 類職責單一

說明:類應該職責單一。如果一個類的職責過多,往往難以設計、實現、使用、維護。

隨著功能的擴充,類的職責範圍自然也擴大,但職責不應該發散。

用小類代替巨類。小類更易於編寫,測試,使用和維護。用小類體現簡單設計的概念;巨類會削弱封

裝性,巨類往往承擔過多職責,試圖提供“完整”的解決方案,但往往難以真正成功。

如果一個類有10個以上資料成員,類的職責可能過多。

原則4.2 隱藏資訊

說明:封裝是物件導向設計和編程的核心概念之一。隱藏實現的內部資料,減少調用者代碼與具體實

現代碼之間的依賴。

盡量減少全域和共用資料;

禁止成員函數返回成員可寫的引用或者指標;
 將資料成員設為私人的(struct除外),並提供相關存取函數;

避免為每個類資料成員提供訪問函數;

運行時多態,將內部實現(衍生類別提供)與對外介面(基類提供)分離。

原則4.3 盡量使類的介面正交、少而完備

說明:應該圍繞一個核心去定義介面、提供服務、與其他類合作,從而易於實現、理解、使用、測試

和維護。介面函數功能正交,盡量避免一個介面功能覆蓋另一個介面功能。介面函數太多,會難以理

解、使用和維護。如果一個類包含20個以上的非私人成員函數,類的介面可能不夠精簡。

規則4.1 模組間對外介面類不要暴露私人和保護成員

說明:對外介面類暴露受保護或者私人成員則破壞了封裝,一旦因為類的設計變更(增加,刪除,修改

內部成員)會導致關聯組件或系統的代碼重新編譯,從而增加系統編譯時間,也產生了二進位相容問題,

導致關聯升級和打補丁。所以除非必要,不要在介面類中暴露私人和保護成員。

有如下幾種做法:

使用純虛類作為介面類,用實作類別完成實現,使用者只看到介面類,這種做法缺點是:

    代碼結構相對複雜。

    新增介面必須放在原有介面後面,不能改變原有介面的順序。否則,因為虛函數表的原因,

       會導致客戶代碼重新編譯。

介面類使用PIMPL模式(只有一個指向實作類別指標的私人資料成員),所有私人成員都封裝在實作類別

    中(實作類別可以不暴露為標頭檔,直接放在實現檔案中)。

    代碼結構簡單,容易理解。

    可以節省虛函數開銷,但是有間接訪問開銷。

    修改實現不會導致客戶代碼重新編譯。

    class Interface
    {
    public:
       void function();
    private:
       Implementation* impl_;
    };

    class Implementation
    {
    public:
       int i;
       int j;
    };

    void Interface:: function ()
    {
       ++impl_->i;
    }

規則4.2 避免成員函數返回成員可寫的引用或者指標

說明:破壞了類的封裝性,對象本身不知道的情況下對象的成員被修改。
樣本:不好的例子

    class Alarm
    {
    public:
       string&  getname(){return name;}  //破壞類的封裝性,成員name被暴露
    private:
       string name;
    };
例外:某些情況下確實需要返回可寫引用或者指標的,例如單件模式的一種寫法:

    Type& Type::Instance()
    {
       static Type instance;
       return instance;
    }

規則4.3 禁止類之間循環相依性

說明:循環相依性會導致系統耦合度大大增加,所以類之間禁止循環相依性。類A依賴類B,類B依賴類A。

出現這種情況需要對類設計進行調整,引入類C:

升級:將關聯業務提到類C,使類C依賴類A和類B,來消除循環相依性

降級:將關聯業務提到類C,使類A和類B都依賴類C,來消除循環相依性。

樣本:類Rectangle和類Window互相依賴

    class Rectangle
    {
    public:
       Rectangle(int x1, int y1, int x2, int y2);
       Rectangle(const Window& w);
    };

    class Window
    {
    public:
       Window(int xCenter, int yCenter, int width, int height);
       Window(const Rectangle& r);
    };
可以增加類BoxUtil做為轉換,不用產生相互依賴

    class BoxUtil
    {
    public:
       static Rectangle toRectangle(const Window& w);
       static Window toWindow(const Rectangle& r);
    };

建議4.1 將資料成員設為私人的(struct除外),並提供相關存取函數

說明:資訊隱藏是良好設計的關鍵,應該將所有資料成員設為私人,精確的控製成員變數的讀寫,對

外屏蔽內部實現。否則意味類的部分狀態可能無法控制、無法預測,原因是:

非private成員破壞了類的封裝性,導致類本身不知道其資料成員何時被修改;

任何對類的修改都會延伸影響到使用該類的代碼。

將資料成員私人化,必要時提供相關存取函數,如定義變數foo_及存取子foo()、賦值操作符

set_foo()。 存取函數一般內聯在標頭檔中定義成內嵌函式。如果外部沒有需求,私人資料成員可以
不提供存取函數,以達到隱藏和保護的目的。不要通過存取函數來訪問私人資料成員的地址(見規則

4.2)。

建議4.2 使用PIMPL模式,確保私人成員真正不可見

說明:C++將私人成員成員指定為不可訪問,但還是可見的,可以通過PIMPL慣用法使私人成員在當前

類的範圍中不可見。PIMPL主要是通過前置聲明,達到介面與實現的分離的效果,降低編譯時間,降低

耦合。

樣本:

    class Map
    {
    private:
        struct  Impl;
        shared_ptr<Impl> pimpl_;
    };

  4.2  構造、賦值和析構

規則4.4 包含成員變數的類,須定義建構函式或者預設建構函式

說明:如果類有成員變數,沒有定義建構函式,又沒有定義預設建構函式,編譯器將自動產生一個構

造函數,但編譯器產生的建構函式並不會對成員變數進行初始化,對象狀態處於一種不確定性。

例外:如果這個類是從另一個類繼承下來,且沒有增加成員變數,則不用提供預設建構函式

樣本:如下代碼沒有建構函式,私人資料成員無法初始化:

    class CMessage
    {
    public:
       void ProcessOutMsg()
        {
           //…
        }
    private:
        unsigned int  msgid;
        unsigned int  msglen;
        unsigned char *msgbuffer;
    };
    CMessage msg;   //msg成員變數沒有初始化
    msg.ProcessOutMsg();   //後續使用存在隱患

    //因此,有必要定義預設建構函式,如下:
    class CMessage
    {
    public:
        CMessage ():
         msgid(0),
         msglen (0),
         msgbuffer (NULL)
         {
         }
         //...
    };

規則4.5 為避免隱式轉換,將單參數建構函式聲明為explicit
說明:單參數建構函式如果沒有用explict聲明,則會成為隱式轉換函式。

樣本:

    class Foo
    {
    public:
       explicit Foo(const string &name):m_name(name)
        {
       }
    private:
       string m_name;
    };

    ProcessFoo("zhangsan"); //函數調用時,編譯器報錯,因為顯式禁止隱式轉換
定義了Foo::Foo(string &name),當形參是Foo對象實參為字串時,建構函式Foo::Foo(string &name)

被調用並將該字串轉換成一個Foo臨時對象傳給調用函數,可能導致非預期的隱式轉換。解決辦法:

在建構函式前加上explicit限制隱式轉換。

規則4.6 包含資源管理的類應自訂拷貝建構函式、賦值操作符和解構函式

說明:如果使用者不定義,編譯器預設會產生拷貝建構函式、賦值操作符和解構函式。自動產生的拷貝

建構函式、賦值操作符只是將所有來源物件的成員簡單賦值給目的對象,即淺拷貝(shallow copy);自

動產生解構函式是空的。這對於包含資源管理的類來說是不夠的:比如從堆中申請的資源,淺拷貝會

使得來源物件和目的對象的成員指向同一記憶體,會導致資源重複釋放。空的解構函式不會釋放已申請內

存。

如果不需要拷貝建構函式和賦值操作符,可以聲明為private屬性,讓它們失效。

樣本:如果結構或對象中包含指標,定義自己的拷貝建構函式和賦值操作符以避免野指標。

    class GIDArr
    {
    public:
        GIDArr()
        {
           iNum = 0;
           pGid = NULL;
        }
        ~GIDArr()
        {
           if (pGid)
           {
               delete [] pGid;
           }
        }
    private:
        int iNum;
        char *pGid;
        GIDArr(const GIDArr& rhs);
        GIDArr& operator = (const GIDArr& rhs) ;
    }GIDArr;

規則4.7 讓operator=返回*this的引用

說明:符合連續賦值的常見用法和習慣。

樣本:
 String& String::operator=(const String& rhs)
     {
        //...
        return *this;            //返回左邊的對象
     }
    string w, x, y, z;
    w = x = y = z = "Hello";

規則4.8 在operator=中檢查給自己賦值的情況

說明:自己給自己賦值和普通賦值有很多不同,若不防範會出問題。

樣本:

    class String
     {
    public:
        String(const char *value);
        ~String();
        String& operator=(const String& rhs);
    private:
        char *data;
     };

    //自賦值,合法
    String a;
    a=a;
    //不好的例子:忽略了給自己賦值的情況,導致訪問野指標
    String& String::operator=(const String& rhs)
     {
        delete [] data;     //刪除data

        //分配新記憶體,將rhs的值拷貝給它
        data = new char[strlen(rhs.data) + 1]; //rhs.data已經刪除,變成野指標
        strcpy(data, rhs.data);

        return *this;
     }

    //好的例子:檢查給自己賦值的情況
    String& String::operator=(const String& rhs)
    {
        if(this != &rhs)
        {
           delete [] data;
           data = new char[strlen(rhs.data) + 1];
           strcpy(data, rhs.data);
        }
        return *this;
    }

規則4.9 在拷貝建構函式、賦值操作符中對所有資料成員賦值

說明:確保建構函式、賦值操作符的對象完整性,避免初始化不完全。

規則4.10 通過基類指標來執行刪除操作時,基類的解構函式設為公有且虛擬

說明:只有基類解構函式是虛擬,才能保證衍生類別的解構函式被調用。
樣本:基類定義中無虛解構函式導致的記憶體流失。

    //如下平台定義了基類A,完成擷取版本號碼的功能。
    class A
    {
    public:
       virtual std::string getVersion()=0;
    };
    //產品衍生類別B,實現其具體功能,其定義如下:
    class B:public A
    {
    public:
       B()
        {
           cout<<"B()"<<endl;
           m_int = new int [100];
        }

        ~B()
        {
           cout<<"~B()"<<endl;
           delete [] m_int;
        }
        std::string getVersion(){ return std::string("hello!");}

    private:
        int *m_int;
    };

    //類比該介面的調用代碼如下:
    int main(int argc, char* args[])
    {
       A *p = new B();
       delete p;
        return 0;
    }
衍生類別B雖然在解構函式中進行了資源清理,但不幸的是該衍生類別解構函式永遠不會被調用。由於基類

A沒有定義解構函式,更沒有定義虛解構函式,當對象被銷毀時,只會調用系統預設的解構函式,故導

致記憶體流失。

規則4.11 避免在建構函式和解構函式中調用虛函數

說明:在建構函式和解構函式中調用虛函數,會導致未定義的行為。

在C++中,一個基類一次只構造一個完整的對象。

樣本:類BaseA是基類,DeriveB是衍生類別

    class BaseA       //基類BaseA
    {
    public:
       BaseA();
       virtual void log() const=0;    //不同的衍生類別調用不同的記錄檔
    };

    BaseA::BaseA()        //基類建構函式
    {
       log();          //調用虛函數log
    }
   class DeriveB:public BaseA //衍生類別
    {
   public:
       virtual void log() const;
    };

當執行如下語句:

DeriveB B;

會先執行DeriveB的建構函式,但首先調用BaseA的建構函式,由於BaseA的建構函式調用虛函數log,

此時log還是基類的版本,只有基類構造完成後,才會完成衍生類別的構造,從而導致未定義的行為。

同樣的道理也適用於解構函式。

建議4.3 拷貝建構函式和賦值操作符的參數定義成const參考型別

說明:拷貝建構函式和賦值操作符不可以改變它所引用的對象。

建議4.4 在解構函式中集中釋放資源

說明:使用解構函式來集中處理資源清理工作。如果在解構函式之前,資源被釋放 (如release函數),

請將資源設定為NULL,以保證解構函式不會重複釋放。

  4.3  繼承

繼承是物件導向語言的一個基本特性。理解各種繼承的含義: “public繼承”意味"是...一個",純虛

函數只繼承介面,一般的虛函數繼承介面並提供預設實現,非虛函數繼承介面和實現但不允許修改。

繼承的層次過多導致理解困難;多重繼承會顯著增加代碼的複雜性,還會帶來潛在的混淆。

原則4.4 用組合代替繼承

說明:繼承和組合都可以複用和擴充現有的能力。如果組合能表示類的關係,那麼優先使用組合。

繼承實現比較簡單直觀,但繼承在編譯時間定義,無法在運行時改變;繼承對衍生類別暴露了基類的實現

細節,使衍生類別與基類別結合程度性非常強。一旦基類發生變化,衍生類別隨著變化,而且因為衍生類別無法修

改基類的非虛函數,導致修改基類會影響到各個衍生類別。

而組合的靈活性較高,代碼耦合小,所以優先考慮組合。

但是並非絕對,往往組合和繼承是一起使用的,例如組合的元素是抽象的,通過實現抽象來修改組合

的行為。

繼承在一般情況下有兩類:實現繼承(implementation inheritance)和介面繼承(interface

inheritance),儘可能不要使用實現繼承而考慮用組合替代。

介面繼承:只繼承成員函數的介面(也就是聲明),例如純虛(pure virtual)函數;實現繼承:繼

承成員函數的介面和實現,例如虛函數同時繼承介面和預設實現,又能夠覆寫它們所繼承的實現;非

虛函數繼承介面,強制性繼承實現。

樣本:組合是指一個類型嵌入另一個類型的成員變數,即"有一個" 或 "由...來實現"。例如:

    class Address{ ... };       //某人居住之處
    class PhoneNumber{ ... };    //某人電話號碼
    class Person
    {
    private:
       string name;              //組合成員變數
       Address address;           //同上
       PhoneNumber voiceNumber;    //同上
       PhoneNumber faxNumber;      //同上
    };

原則4.5 避免使用多重繼承

說明: 相比單繼承,多重實現繼承可重用更多代碼;但多重繼承會顯著增加代碼的複雜性,程式可維

護性差,且父類轉換時容易出錯,所以除非必要,不要使用多重實現繼承,使用組合來代替。

多重繼承中基類都是純介面類,至多隻有一個類含有實現。

規則4.12 使用public繼承而不是protected/private繼承

說明:public繼承與private繼承的區別:

private繼承體現"由...來實現"的關係。編譯器不會把private繼承的衍生類別轉換成基類,也就是

    說,私人繼承的基類和衍生類別沒有"是...一個"的關係。

public繼承體現"是...一個"的關係,即類B public繼承於類A,則B的對象就是A的對象,反之則

    不然。例如“白馬是馬,但馬不是白馬”。

對繼承而言,努力做到"是...一個"的關係,否則使用組合代替。

private繼承意味"由...來實現",它通常比組合的層級低,與組合的區別:

private繼承可以訪問基類的protected成員,而組合不能。

private繼承可以重新定義基類的虛函數,而組合不能。

盡量用組合代替private繼承,因為private繼承不如組合簡單直觀,且容易和public繼承混淆。

規則4.13 繼承層次不超過4層

說明:當繼承的層數超過4層時,對軟體的可維護性大大降低,可以嘗試用組合替代繼承。

規則4.14 虛函數絕不使用預設參數值

說明:在C++中,虛函數是動態綁定的,但函數的預設參數卻是在編譯時間就靜態繫結的。這意味著你最

終執行的函數是一個定義在衍生類別,但使用了基類中的預設參數值的虛函數。因此只要在基類中定義

預設參數值即可,絕對不要在衍生類別中再定義預設參數值。

樣本:虛函數display預設參數值strShow是由編譯時間刻決定的,而非運行時刻,沒有達到多態的目的:

    class Base
    {
    public:
       virtual void display(const std::string& strShow = "I am Base class !")
       {
          std::cout << strShow << std::endl;
       }
       virtual ~Base(){}
    };
    class Derive: public Base
    {
    public:
       virtual void display(const std::string& strShow  = "I am Derive class !")
       {
           std::cout << strShow << std::endl;
        }
       virtual ~Derive(){}
    };
    int main()
    {
       Base* pBase = new Derive();
       Derive* pDerive = new Derive();
       pBase->display();  //程式輸出結果: I am base class !而期望輸出:I am Deriveclass !
       pDerive->display();//程式輸出結果: I am Derive class !
       delete pBase;
       delete pDerive;
       return 0;
    };

規則4.15 絕不重新定義繼承而來的非虛函數

說明:因為非虛函數無法實現動態綁定,只有虛函數才能實現動態綁定:只要操作基類的指標,即可

獲得正確的結果。

樣本:pB->mf()和pD->mf()兩者行為不同。

    class B
    {
    public:
       void mf();
       //...
    };

    class D:public B
    {
    public:
       void mf();
       //...
    };

    D x;                         //x is an object of type D
    B *pB = &x;                   //get pointer to x
    D *pD = &x;                   //get pointer to x

    pB->mf();                     //calls B::mf
    pD->mf();                     //calls D::mf

建議4.5 避免衍生類別中定義與基類同名但參數類型不同的函數

說明:參數類型不同的函數實際是不同的函數。

樣本:如下三個類,類之間繼承關係如下:類Derive2繼承類Derive1,類Derive1繼承類Base。

三個類之中均實現了FOO函數,定義如下:

Base類:virtual long FOO(const A , const B , const C)=0;

Derive1類:long FOO(const A , const B , const C);

Derive2類:long FOO(const A , B , const C);

代碼中存在如下的調用:

Base* baseptr = new Derive2();

baseptr -> FOO(A,B,C);
代碼原意是期望通過如上的代碼調用Derive2::FOO函數,但是由於Derive2::FOO與Base::FOO的

參數類型不一致,Derive2::FOO對Base類來說不可見,導致實際啟動並執行時候,調用到

Derive1::FOO,出現調用錯誤。使得代碼邏輯異常。

解決方案:確保衍生類別Derive2::FOO定義和Base::FOO一致。

建議4.6 衍生類別重定義的虛函數也要聲明virtual關鍵字

說明:當重定義派生的虛函數時,在衍生類別中明確聲明其為virtual。如果遺漏virtual聲明,閱讀者

需要檢索類的所有祖先以確定該函數是否為虛函數。

  4.4  重載

C++的重載功能使得同名函數可以有多種實現方法,以簡化介面的設計和使用。但是,要合理運用防止帶

來二義性以及潛在問題。保持重載操作符的自然語義,不要盲目創新。

原則4.6 盡量不重載操作符,保持重載操作符的自然語義

說明:重載操作符要有充分理由,而且不要改變操作符原有語義,例如不要使用 ‘+’操作符來做減運算。

操作符重載令代碼更加直觀,但也有一些不足:

混淆直覺,誤以為該操作和內建類型一樣是高效能的,忽略了效能降低的可能;

問題定位時不夠直觀,按函數名尋找比按操作符顯然更方便。

重載操作符如果行為定義不直觀(例如將‘+’操作符來做減運算),會讓代碼產生混淆。

賦值操作符的重載引入的隱式轉換會隱藏很深的bug。可以定義類似Equals()、CopyFrom()等函數來

    替代=,==操作符。

規則4.16 僅在輸入參數類型不同、功能相同時重載函數

說明:使用重載,導致在特定調用處很難確定到底調用的是哪個函數;當衍生類別只重載函數的部分變

量,會對繼承語義產生困惑,造成不必要的費解。

如果函數的功能不同,考慮讓函數名包含參數資訊,例如,使用AppendName()、AppendID()而不是

Append()。

建議4.7 使用重載以避免隱式類型轉換

說明:隱式轉換常常建立臨時變數;如果提供類型精確匹配的重載函數,不會導致轉換。

樣本:

    class String
    {
       //…
       String( const char* text );  //允許隱式轉換
    };
    bool operator==( const String&, const String& );
    //…代碼中某處…
    if( someString == "Hello" ) {... }
上述例子中編譯器進行隱式轉換,好像someString == String( "Hello")一樣,形成浪費,因為並不需

要拷貝字元。使用操作符重載即可消除這種隱式轉換:
   bool operator==( const String& lhs, const String& rhs );   //#1
    bool operator==( const String& lhs, const char* rhs );     //#2
    bool operator==( const char* lhs, const String& rhs );     //#3

建議4.8 C/C++混用時,避免重載介面函數

說明:目前很多產品採用C模組與C++模組混合的方式,在這個情況下,應該避免模組之間介面函數的

重載。比如:傳遞給用C語言實現的組件的函數指標。

    stPdiamLiCallBackFun.pfCreateConn = PDIAM_COM_CreatConnect;
    stPdiamLiCallBackFun.pfDeleteConn = PDIAM_COM_DeleteConnect;
    stPdiamLiCallBackFun.pfSendMsg = PDIAM_COM_SendData;
    stPdiamLiCallBackFun.pfRematchConn = PDIAM_COM_RematchConn;
    stPdiamLiCallBackFun.pfSuCloseAcceptSocket = PDIAM_COM_CloseAcceptSocket;

    //註冊系統底層通訊函數
    ulRet = DiamRegLiFunc(&stPdiamLiCallBackFun);
    if (DIAM_OK != ulRet)
    {
       return PDIAM_ERR_FAILED_STACK_INIT;
    }
上面的代碼中,由於組件通過C語言實現,如果重載PDIAM_COM_CreatConnect等函數,將會導致該組件

無法初始化。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.