1. 程式人生 > 實用技巧 >c++ effective總結(一)

c++ effective總結(一)

條款一:視c++為一個語言聯邦

  c++可以認為是由C,Object-Oriented C++(面向物件),Template C++(模板),STL(c++標準模板庫)四種次語言組成的。

條款二:儘量以const,enum,inline替換#define

  c++中推薦使用其他的方法替換一些巨集定義操作,如常量定義,推薦使用const int MAX = 10 替換#define MAX 10。而對於#define定義的函式,也推薦使用inline(行內函數)替換。另外如在類中要宣告成員屬性的值。必須宣告成靜態常量,或者使用enum。如

class Stack{
public:
    ...
private: static const int size = 10; // 或者 enum { size = 10 }; int members[size]; }

條款三:儘可能使用const

  const在c++裡面可以說是用的非常多的一個修飾詞,const可以修飾值,指標,函式,函式引數,函式返回值等。

  如const char* p = "hello"; 指標指向的值為常量。char* const p = "hello";指向值的指標為常量。可以記為const在*左邊值為常量,const在*右邊指標為常量。如果*兩邊都有const,則值和指標都為常量。

  當你在使用stl容器時,使用迭代器訪問容器中的元素,而又不希望通過迭代器修改容器中的值,可以使用const_iterator,或者是遍歷常量容器物件,使用const_iterator。

  const修飾成員函式,該成員函式不可以修改成員屬性值,另外const常用來傳遞的引用引數,當你不希望修改引用引數中的內容時,可以用const修飾。

條款四:確定物件被使用前已先被初始化

  為內建物件進行手工初始化,因為c++不保證初始化它們,即你想初始化一個int物件,最好將int x;改寫成int x = 0;當然大部分情況下c++會為int x;分配 x = 0。

  建構函式初始化成員變數時,最好使用成員初值列,這樣效率比賦值要高一些,尤其是對非內建物件,內建物件兩者沒影響。如

//推薦使用
class Student{
public:
    Student(string& name, int
age):name_(name),age_(age) {}; private: string name_; int age_; } //不推薦 class Student{ public: Student(string& name, int age) { name_ = name; age_ = age; }; private: string name_; int age_; }

  上面第一種只有對於string物件只有copy操作,而第二種既有copy操作,還有賦值操作。

條款五:瞭解c++默默編寫並呼叫哪些函式

  你定義一個空類,編譯器會為它分配default建構函式,copy建構函式,copy assignment操作符合一個解構函式。如

class Empty {};

  實際上上面的類等價於

class Empty {
public:
    Empty() {...};
    Empty (const Empty& e) {...};
    ~Empty() {...};
    Empty& operator=(const Empty& e) {...};  
}

  如果你自己定義了建構函式,copy建構函式,copy assignment,解構函式,則編譯器不會再分配這些函式。

  編譯器分配的copy assignment有時候存在問題,如string& name這樣的成員屬性,編譯器分配的copy assignment是不可以賦值的,你需要自己重新定義copy assignment。

條款六:若不想使用編譯器自動生成的函式,就該明確拒絕

  如條款五所說編譯器會自動分配如copy建構函式,copy assignment這類的函式,如果你不想要這些函式,可以在private下宣告這些函式就行了。如

class Student{
private:
     Student (const Student&);
     Student& operator=(const Student&);
}

條款七:為多型基類宣告virtual解構函式

  帶有多型性質的基類,或者類中有帶vitural的成員函式,則其解構函式都應該定義為virtual解構函式。但如果該類不是設計為基類,則不要宣告virtual解構函式。如果基類中的解構函式沒有定義為vitural,則會出現一些問題,如在工廠模式中。有以下程式碼

class Person{
public:
    Person();
    ~Person();
}

class Student : public Person {...};

Person* getPerson();

Person* student = getPerson();
delete student;

  假設當前的getPerson()獲取一個派生類Student物件,當使用完之後delete student時因為指標是基類指標,所以會銷燬基類中的成員,但是不會銷燬派生類中的成員,所以會存在”部分銷燬“的現象,但是若基類中的解構函式是vitural,則會刪除派生類中的成員。

  另外如果一個類不是基類,不要宣告vitural函式,因為vitural會引入虛表指標和虛表,會佔用一部分記憶體。

條款八:別讓異常逃離解構函式

  這種通常指在解構函式中呼叫了一些函式,而這些函式可能引入異常,此時需要做一些處理,儘量保證不要讓解構函式的異常傳遞出來,或者說盡量確保解構函式中不要發生異常。

條款九:絕對不要在建構函式和解構函式中呼叫virtual函式

  因為上述這類呼叫絕對不會下降至下一層,即派生層,這種情況避免就好了。一般也不會這樣去呼叫

條款十:令operator= 返回一個reference to *this

  這是一種固定的assignment 操作符寫法,如

class Student{
public:
    Student& operator=(const Student& s){
        name_ = s.name_;
        age_ = s.age_;
        return *this;
    }
private:
    string name_;
    int age_;
}

  除了operator= 如operator+=等都可以寫成這樣。

條款十一:在operator= 中處理”自我賦值“

  自我賦值,即a = a;當然一般這種情況不會發生,但是這種*a = *b,而指標a和b都指向同一個值,這樣就存在問題。如果operator= 是下面這種寫法

class Bitmap {};

class Widget {
public:
    Widget& operator=(const Widget& rhs){
        delete pb;
        pb = new Bitmap(*rhs.pb);
        return *this;
    }
private:
    Bitmap* pb;
}

  如果rhs == this,則delete pb時也刪除了rhs中的pb。所以可以改寫成

class Bitmap {};

class Widget {
public:
    Widget& operator=(const Widget& rhs){
        Bitmap* pOrig = pb;
        pb = new Bitmap(*rhs.pb);
        delete pOrig;
        return *this;
    }
private:
    Bitmap* pb;
}

條款十二:複製物件時勿忘其每一個成分

  copy建構函式和copy assignment函式中不要忘了每一個成員變數,忘記了編譯器也不會報錯。如

class Student{
public: 
    Student (const Student& s):name_(s.name_) {};
private:
    string name_;
    int age_;
}

  派生類中的copy建構函式也不要忘了基類中的成員變數,可以直接呼叫基類的建構函式。

class Person{
public:
    Person (const Person& p):name_(p.name_), age_(p.age_) {};
private:
    string name_;
    int age_;
}

class Student : public Person {
public:
    Student (const Student& s) : Person(s), grade_(s.grade_){};
private:
    int grade_;
}

條款十三:以物件管理資源

  在c++中new和delete必須是同時存在的,但很多時候會忘記delete,或者說你很仔細的沒有忘記delete,但是在new和delete之間的程式碼可能會存在return,continue等這類操作,而跳過了delete,為了防止這種記憶體洩漏的情況發生,所以也就有了以物件管理資源,當物件唄釋放時,物件的解構函式會自動釋放這些資源,如auto_ptr就是這種的資源管理物件。而c++11中的unique_ptr,shared_ptr也是這一類物件,只不過unique_ptr不能多個物件共享一塊記憶體,而shared_ptr通過引用計數機制,可以多個物件共享一塊記憶體,只有當引用計數為0才會釋放記憶體。

條款十四:在資源管理類中小心copying行為

  RAII(Resource Acquisition Is Initailization,資源取得時便是初始化時,也是以物件管理資源的概念)物件複製時要一併複製它所管理的資源。而普遍常見的RAII class copying行為是:抑制copying,施行引用計數法等。

條款十五:在資源管理類中提供對原始資源的訪問

  APIs中往往需要訪問原始資源,所以每一個RAII class應該提供一個”取得其所管理之資源“的方法,如get成員函式可以獲得原始指標,過載了指標取值運算子,轉換到原始指標並取值。

  對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯式轉換比較安全,但隱式轉換對客戶比較方便。

條款十六:成對使用new和delete時要採取相同形式

  如下例子  

string* stringArray = new string[100];
...
delete stringArray;

  上面的new和delete並不是一個很好的應用,因為new出來的是一個數組,而delete很可能只釋放了一個元素,標準的用法是

string* stringArray = new string[100];
...
delete [] stringArray;

  所以說new和delete,new [] 和delete []。針對此問題要注意typedef的使用,最好不要對陣列使用typedef,否則很容易在delete時忘了帶[]。

條款十七:以獨立語句將newed物件置入智慧指標

  以獨立語句將newed物件儲存於智慧指標內。如果不這樣做,一旦異常丟擲,有可能導致難以察覺的資源洩漏。如

int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

//呼叫
processWidget(std::shared_ptr<Widget> (new Widget), priority());

  在c++中上述執行的順序並不嚴格,可能先執行new Widget,再執行priority(),最後才執行shared_ptr的呼叫。這樣一旦priority()呼叫報錯,就不會發生new Widget的記憶體洩漏。所以最好獨立語句將物件放入智慧指標。

std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

條款十八:讓介面容易被正確使用,不易被誤用

  總結成一句話就是在設計介面時,儘量簡潔明瞭,約束性強,不容易發生誤用。

條款十九:設計class猶如設計type

條款二十:寧以pass-by-reference-to-const替換pass-by-value

  在函式的引數傳遞過程中對於非內建型別物件,儘量以引用或指標傳遞,推薦引用,為了避免函式中修改傳遞的物件,可以加上const修飾符。對於非內建型別,引用的傳遞要比傳值高效很多,因為傳值的過程中相當於傳遞副本,是需要呼叫copy建構函式,而函式執行完之後還會呼叫解構函式銷燬copy的物件,而引用傳遞不存在這一問題。

條款二十一:必須返回物件時,別妄想返回其reference

  在函式的返回值時切勿返回reference、pointer,尤其是指向函式中的區域性物件,直接返回值即可,雖然這樣會耗時(呼叫建構函式),但至少是正確的。

條款二十二:將成員變數宣告為private

條款二十三:寧以non-member,non-friend替換member函式

條款二十四:若所有引數皆需型別轉換,請為此採用non-member函式

條款二十五:考慮寫出一個不拋異常的swap函式

條款二十六:儘可能延後變數定義式的出現時間

  定義的變數即使沒有被使用,也會存在構造和析構的操作,也就是或會存在構造和析構的成本,所以在定義變數時儘量在使用時定義。

條款二十七:儘量少做轉型動作

  如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_casts。

  如果轉型是必要的,試著將他隱藏於某個函式背後。客戶隨後可以呼叫該函式,而不需將轉型放入到他們自己的程式碼內。

  寧可使用c++風格的新式轉型,不要使用舊式轉型。

條款二十八:避免返回handles指向物件內部成分

  對於下面的例子,返回引用(handles,指標,迭代器也可以歸為這一類)會導致內部資料的不安全.

class Point {
public:
    Point(int x, int y);
    void setX(int newVal);
    void setY(int newVal);
}

struct RectData{
    Point ulhc;
    Point lrhc;
}

class Rectangle{
public:
    Point& upperLeft () const {return pData->ulhc};
    Point& lowerRight () const {return pData->lrhc};
private:
    std::shared_ptr<RectData> pData;
}

  上面的Rectangle類中,雖然upperLeft函式是const函式,但是返回的結果是Point的引用,通過該引用是可以修改Point內部的值,包括返回指標和迭代器都是會發生類似的情況,所以如果硬要返回引用這類handles,可以返回const references

class Rectangle{
public:
    const Point& upperLeft () const {return pData->ulhc};
    const Point& lowerRight () const {return pData->lrhc};
private:
    std::shared_ptr<RectData> pData;
}

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

條款三十:透徹瞭解inlining的裡裡外外

  總之只有在小型且頻繁被呼叫的函式身上才使用inlining。這樣可以保證程式碼膨脹的問題最小化,程式的速度提升最大化,如max函式。

條款三十一:將檔案間的編譯依存關係降至最低

條款三十二:確定你的public繼承塑模出is-a關係

  基類和派生類之間一定要有is-a關係,即基類的屬性和方法,在派生類中一定是合理存在的。

條款三十三:避免遮掩繼承而來的名稱

  只要熟悉作用域,根據作用域去定義變數就可以了,知道在使用變數時作用域的先後順序。

條款三十四:區分介面繼承和實現繼承

  這裡涉及到基類中的純虛擬函式,虛擬函式和非虛擬函式以及繼承的概念。

  純虛擬函式:含有純虛擬函式的類是抽象類,不可以被new出來。對於純虛擬函式,子類只繼承了介面,子類中必須宣告和實現純虛擬函式,當然父類中也可以定義純虛擬函式,但要指定父類名稱才可以呼叫。

  虛擬函式:子類會繼承虛擬函式的介面和預設實現,子類中也可以重寫虛擬函式。

  非虛擬函式:子類會繼承非虛擬函式的介面和強制實現,不建議子類中重寫非虛擬函式。如果硬要重寫,呼叫時基類指標會呼叫基類中的函式,想呼叫子類中的重寫函式,必須使用子類指標。

條款三十五:考慮virtual函式以外的其他選擇

  在子類繼承父類的功能時,可以引入一些設計模式來使得繼承更靈活。如使用模板模式,策略模式。

條款三十六:絕不重新定義繼承而來的非虛擬函式

  正如上面所說如果子類重寫了父類的非虛擬函式,父類指標即使是指向子類,當呼叫非虛擬函式時,還是會呼叫父類的非虛擬函式,所以非虛擬函式的呼叫和指標型別繫結。所以為了避免出現這樣的迷惑行為,還是不要重寫非虛擬函式。

條款三十七:絕不重新定義繼承而來的預設引數值

  對於繼承而來的虛擬函式或純虛擬函式,如果函式中有定義的預設引數值,繼承時不要修改預設引數值,因為預設引數值是靜態繫結(發生在編譯期)的。而虛擬函式或純虛擬函式時動態繫結(發生在執行期)的。

條款三十八:通過複合塑模出has-a或”根據某物實現出“

條款三十九:明智而審慎地使用private繼承

  private並不會繼承介面,而是繼承基類的實現,總之儘量使用public,除非特殊實現上,因為private繼承本質上不屬於is-a關係。

條款四十:明智而審慎地使用多重繼承

  總之能使用單一繼承的儘量使用單一繼承,多重繼承太過複雜,容易引入歧義,且應用於多重繼承的虛繼承既要提前設計也會引入大小、速度等成本。

條款四十一:瞭解隱式介面和編譯器多型

  c++中的模板創造了隱式介面和編譯期的多型。

條款四十二:瞭解typename的雙重意義

  在宣告模板時,template<typename T> 和 template<class T>的含義是一樣的,但是typename可以表示巢狀從屬型別,如T::setX到底是一個型別還是成員名稱,如果加上typename就很明確是一個型別了,typename T::setX。