《Effective C++》第1章 讓自己習慣C++-讀書筆記

QingLiXueShi發表於2015-04-16

章節回顧:

《Effective C++》第1章 讓自己習慣C++-讀書筆記

《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記

《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記

《Effective C++》第3章 資源管理(1)-讀書筆記

《Effective C++》第3章 資源管理(2)-讀書筆記

《Effective C++》第4章 設計與宣告(1)-讀書筆記

《Effective C++》第4章 設計與宣告(2)-讀書筆記

《Effective C++》第5章 實現-讀書筆記

《Effective C++》第8章 定製new和delete-讀書筆記


 

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

為了理解C++,你必須認識其主要的次語言。總共有四個:

(1)C

C++仍是以C為基礎。

(2)Object-Oriented C++
classes(類)(包括建構函式和解構函式),encapsulation(封裝),inheritance(繼承), polymorphism(多型),virtual functions (dynamic binding)(虛擬函式(動態繫結))等。

(3)Template C++

這是C++的generic programming(泛型程式設計)部分

(4)STL

STL 是一個 template library(模板庫)。

C++並不是一個帶有一組守則的一體語言:它是四個次語言組成的聯邦政府,每個次語言都有自己的規約。記住這四個次語言你就會發現C++容易瞭解多了。

請記住:C++高效程式設計守則視狀況而變化,取決於你使用C++的哪一部分。

-------------------------------------------------------------------------------------------------------------------

 

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

該條款最好稱為:“儘量用編譯器而不用預處理”,因為#define不被視為語言的一部分。

#define ASPECT_RATIO 1.653

編譯器永遠也看不到ASPECT_RATIO這個符號名,因為在原始碼進入編譯器之前,它會被預處理程式去掉,於是ASPECT_RATIO不會加入到符號列表中。

解決的辦法是:不用預處理巨集,定義一個常量:

const double AspectRatio = 1.653;    //大寫名稱通常用於巨集,所以這裡改變名稱寫法

說明:

(1)作為語言常量,AspectRatio會被編譯器看到,記入符號表。

(2)使用常量可能比使用#define導致較小量的碼。因為前處理器盲目地用1.653置換ASPECT_RATIO導致目的碼中存在多個1.653的拷貝。如果使用常量AspectRatio,就不會產生多於一個的拷貝。

 

要把常量限制在類中,首先要使它成為類的成員;為了保證常量最多隻有一份拷貝,還要把它定義為靜態成員:

class GamePlayer 
{
private:
    static const int NUM_TURNS = 5;            // constant declaration 
    int scores[NUM_TURNS];                    // use of constant
    ...
};

說明:

(1)語句是NUM_TURNS的宣告,而不是定義。

(2)C++要求對所使用的任何東西提供一個定義式。如果它是class專屬常量且為static整數型別(例如:ints,chars,bools等),只要不取它們的地址可以只宣告並使用而無須定義。

(3)NUM_TURNS的定義如下:

const int GamePlayer::NUM_TURNS;

由於class常量已在宣告時獲得初值,因此定義時可不設初值。

(4)沒有辦法使用#define來建立一個類屬常量,因為#defines不考慮作用域。一旦巨集被定義,它就在其後編譯過程中有效(除非後面某處存在#undefed)。

 

舊一點的編譯器認為類的靜態成員在宣告時定義初始值是非法的。可以在定義賦值:

class EngineeringConstants                // header file
{ 
private:        
    static const double FUDGE_FACTOR;
    ...
};
// this goes in the class implementation file
const double EngineeringConstants::FUDGE_FACTOR = 1.35;

如果在編譯器需要FUDGE_FACTOR的值例如,作為陣列維數,是不行的。因為編譯器必須在編譯器間知道陣列的大小。可以用enum解決:

class GamePlayer 
{
private:
    enum { NUM_TURNS = 5 };                    
    int scores[NUM_TURNS];
};

說明:取一個enum地址是非法的。

 

一個普遍的#define指令的用法是用它來實現那些看起來像函式而又不會導致函式呼叫的巨集。

#define max(a,b) ((a) > (b) ? (a) : (b))

注意:寫巨集時要對每個引數都要加上括號,否則會造成呼叫麻煩。但也會造成下面的錯誤:

int a = 5, b = 0;
max(++a, b);                // a 的值增加了2次
max(++a, b+10);                // a 的值只增加了1次

你可以用普通函式實現巨集的效率,再加上可預計的行為和型別安全。

template<typename T>
inline const T& max(const T& a, const T& b)
{ 
    return a > b ? a : b; 
}

請記住:

(1)對於單純常量,最好以const或enums替換#defines。

(2)對於形似函式的巨集,最好改用inline函式替換#defines。

-------------------------------------------------------------------------------------------------------------------

 

條款03:儘可能使用const

const允許你告訴編譯器某值保持不變,並獲得編譯器幫助,確保這條約束不被違反。在classes的外部,可以將它用於 global(全域性)或namespace(名稱空間)範圍的 constants(常量),或修飾檔案、函式、或區塊作用域中被宣告為static物件。修飾classes內部的static和non-static成員。修飾指標自身,指標所指物。

char greeting[] = "Hello";
char *p = greeting; // non-const pointer, non-const data
const char *p = greeting; // non-const pointer,const data
char * const p = greeting; // const pointer,non-const data
const char * const p = greeting; // const pointer,const data

說明:當指標指向的內容為常量時,const放在型別之前和型別之後意義相同。

void f1(const Widget *pw); // f1 takes a pointer to a constant Widget object
void f2(Widget const *pw); // so does f2

對於STL迭代器來說:

const std::vector<int>:: iterator iter = vec.begin();    // iter acts like a T* const
*iter = 10;                                                // OK, changes what iter points to
++iter;                                                    // error! iter is const
std::vector<int>:: const_iterator cIter = vec.begin();    // cIter acts like a const T*    
*cIter = 10;                                            // error! *cIter is const
++cIter;                                                // fine, changes cIter

 

const 成員函式

將const實施於成員函式的目的是確認該成員函式可作用於const物件身上。兩個函式如果只是常量性不同,可以被過載。

class TextBlock {
public:
    ...
    const char& operator[] (std::size_t position) const // operator[] for const objects
    { return text[position]; } 
    char& operator[] (std::size_t position)                // operator[] for non-const objects
    { return text[position]; } 
private:
    std::string text;
};

TextBlock tb("Hello");
const TextBlock ctb("World");
std::cout << tb[0]; // fine — reading a non-const TextBlock
tb[0] = 'x';        // fine — writing a non-const TextBlock
std::cout << ctb[0]; // fine — reading a const TextBlock
ctb[0] = 'x';        // error! — writing a const TextBlock

為了避免重複,可以利用轉型修改程式碼。根據const版本的operator[]實現其non-const版本。

class TextBlock {
public:
    ...
        const char& operator[](std::size_t position) const 
    {
        ...
            ...
            ...
            return text[position];
    }
    char& operator[](std::size_t position)        // now just calls const op[]
    {
        return
            const_cast<char&>(                    // cast away const on
                                                // op[]'s return type;
            static_cast<const TextBlock&>(*this) // add const to *this's type;
            [position]                            // call const version of op[]
        );
    }
    ...
};

注意:令const 版本呼叫non-const版本來避免重複是錯誤的。一個 const成員函式承諾絕不會改變它的邏輯狀態,但是一個non-const成員函式不會做這樣的承諾。從一個const成員函式呼叫一個non-const成員函式,將面臨承諾不會變化的物件被改變的風險。即const成員函式呼叫non-const成員函式是一種錯誤行為。

請記住:

(1)將某些東西宣告為const可幫助編譯器偵測出錯誤用法const可被施加於任何作用域內的物件、函式引數、函式返回型別、成員函式本體。

(2)當const和non-const成員函式有著實質等價的實現時,令non-const版本呼叫const版本可避免程式碼重複。

-------------------------------------------------------------------------------------------------------------------

 

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

讀取一個未初始化的值會引起未定義行為。因此,永遠在使用物件之前先將它初始化。對於無任何成員的內建型別,你必須手工完成此事。

int x = 0;                                // manual initialization of an int
const char * text = "A C-style string"; // manual initialization of a pointer 
double d;                                 
std::cin >> d;                    // "initialization" by reading from an input stream

對於內建型別以外的東西,由建構函式初始化,確保將物件的每一個成員都初始化。重要的是不要把賦值和初始化混淆

class PhoneNumber { ... };
class ABEntry {                                    // ABEntry = "Address Book Entry"
public:
    ABEntry(const std::string& name, const std::string& address,
        const std::list<PhoneNumber>& phones);
private:
    std::string theName;
    std::string theAddress;
    std::list<PhoneNumber> thePhones;
    int num TimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
    const std::list<PhoneNumber>& phones)
{
    theName = name;                    // these are all assignments,
    theAddress = address;            // not initializations
    thePhones = phones;
    numTimesConsulted = 0;
}

這樣做雖然使得ABEntry物件具有了你所期待的值,但不是最好的做法。C++規定物件的成員變數的初始化動作發生在進入建構函式本體之前。效率較高的寫法是:

ABEntry::ABEntry(const std::string& name, const std::string& address,
    const std::list<PhoneNumber>& phones)
    : theName(name) ,
    theAddress(address) ,            // these are now all initializations
    thePhones(phones) ,
    numTimesConsulted(0)
{}                                    // the ctor body is now empty

說明:

(1)版本1首先呼叫default建構函式為theName,theAddress和thePhones設初值,然後立即再對它們賦予新值。default建構函式的一切作為因此浪費了。

(2)版本2的成員初始化列表的做法避免了這個問題。初始化列表中針對各個成員變數而設的實參,被拿去作為各成員變數建構函式的實參。theName以name為初值進行拷貝構造,theAddress以address為初值進行拷貝構造, thePhones以phones為初值進行拷貝構造。

(3)對於大多數型別來說,只呼叫一次拷貝建構函式的效率比先呼叫一次default建構函式再呼叫一次copy assignment operator(拷貝賦值運算子)的效率要高(有時會高很多)。

(4)對於內建型別物件如numTimesConsulted,其初始化和賦值成本相同,但為了一致性最好也通過成員初始化列表來初始化。

(5)當想要構造一個default建構函式時,也可以使用成員初始化列表。

ABEntry::ABEntry()
    :theName() ,                // call theName's default ctor;
    theAddress() ,                // do the same for theAddress;
    thePhones() ,                // and for thePhones;
    numTimesConsulted(0)        // but explicitly initialize
{}

 當然,編譯器會為使用者自定義型別成員變數自動呼叫default建構函式,如果那些成員變數沒有在成員初始化列表中被指定初值。

(6)如果成員變數是const或引用,即使內建型別也一定需要初值,不能被賦值。

(7)C++有固定的成員初始化次序:基類早於派生類,class成員變數總是以其宣告次序被初始化。

 

static物件初始化問題:

所謂static物件,其壽命從構造出來直到程式結束為止。包括:global物件、定義於namespace作用域內的物件、在class內、函式內、以及在file作用域內被宣告為static的物件。函式內的static物件稱為local static物件,其他static物件稱為non-local static物件。程式結束時static物件會被自動銷燬,它們的解構函式會在main()結束時被自動呼叫。

問題是:如果某編譯單元內的某個non-local static物件的初始化動作使用了令一編譯單元內的某個non-local static物件,它所用到的這個物件可能尚未被初始化。C++對“定義於不同編譯單元內的non-local static物件”的初始化次序並無明確要求

改進方法:將每個non-local static物件搬到static函式中。C++保證,函式內的local static物件會在“該函式被呼叫期間”首次遇上該物件之定義式時被初始化

class FileSystem { ... };                // as before
FileSystem& tfs()                        // this replaces the tfs object; it could be
{                                        // static in the FileSystem class
    static FileSystem fs;                // define and initialize a local static object
    return fs;                            // return a reference to it
}
class Directory { ... };                // as before
Directory::Directory(params)            // as before, except references to tfs are
{                                        // now to tfs()
    ...
        std::size_t disks = tfs().numDisks();
    ...
}
Directory& tempDir()                    // this replaces the tempDir object; it
{                                        // could be static in the Directory class
    static Directory td;                // define/initialize local static object
    return td;                            // return reference to it
}

請記住:

(1)為內建型別物件進行手工初始化,因為C++不保證初始化它們。

(2)建構函式最好使用初始化列表,而不要在建構函式體內使用賦值操作。初始化列表中列出的成員變數排列次序與class中宣告次序相同。

(3)為免除“跨編譯單元之初始化次序”問題,請以local static物件替換non-local static物件。

-------------------------------------------------------------------------------------------------------------------

相關文章