Effective C++ 讀書(shū)筆記

手頭上有一本 Scott Meyes 的 Effective C++(3rd Edition),雖然中文的出版時(shí)間是感人的2011年(也就是說(shuō)C++11的那些新特性都沒(méi)討論了),但看網(wǎng)上的一些評(píng)論,此書(shū)還是值得一讀的。(PS:作者針對(duì)C++11和14的新特性有本新書(shū) Effective Modern C++。。

Day 1:

Item 1: View c++ as a federation of languages

我果然還是喜歡在開(kāi)頭說(shuō)一點(diǎn)總結(jié)性的東西。這里對(duì)條款1做一點(diǎn)記錄。。

C++是一個(gè)多重范型編程語(yǔ)言,一個(gè)簡(jiǎn)單理解C++的方法是把C++視為多個(gè)次語(yǔ)言(sublanguage)組成的聯(lián)邦,次語(yǔ)言之間的守則可能會(huì)不一致。次語(yǔ)言共有4個(gè):C,面向?qū)ο螅∣bject-Oriented C++),模版 (Template C++),STL。

一個(gè)例子:對(duì)內(nèi)置類(lèi)型(C-like)而言,pass-by-value通常比pass-by-reference高效;對(duì)用戶自定義的對(duì)象,由于構(gòu)造和析構(gòu)函數(shù)的存在,pass-by-reference-to-const往往更好;而在傳遞STL迭代器(iterator)和函數(shù)對(duì)象(function object)時(shí),pass-by-value則更好,因?yàn)樗鼈兌际腔贑指針實(shí)現(xiàn)的。

Item 2: Prefer consts, enums and inlines to #define

#define只是預(yù)處理階段的文本替換,我們要盡量利用編譯器提供更多信息。比較常用的就是const代替#define的常量,用inline函數(shù)代替#define的宏函數(shù)。這兩個(gè)平時(shí)都用的很多了。enum只是在下面很特定的例子中有用:

// 1.h
class A {
public:
    static const int x = 5;
    int y[x];
};

這里希望聲明一個(gè)常量x,它以類(lèi)成員形式出現(xiàn),而且所有對(duì)象只保存它的一個(gè)副本。于是就有了這么一個(gè)static const的成員變量,但是舊編譯器并不允許你在static變量聲明時(shí)賦值,#define又無(wú)法體現(xiàn)x的作用域,這時(shí)就可以使用enum:

// new_1.h
class A {
public:
    enum { x = 5 };
    int y[x];
};

很無(wú)語(yǔ)是吧。。是個(gè)挺過(guò)時(shí)的故事。值得一說(shuō)的是,你如果想要取1.h中x的地址(其實(shí)也是挺奇怪的,相當(dāng)于取常量地址),是需要重新聲明一下這個(gè)成員變量的:

// 1.cpp
#include "1.h"
#include<iostream>

const int A::x;  // already set value to 5, if not, assign a value here

int main() {
    A a;
    std::cout << a.x << std::endl;  // ok with or without the redeclaration
    std::cout << &(a.x) << std::endl;  // must redeclare A::x
    return 0;
}

Day 2:

Item 3: Use const whenever possible

const是一個(gè)神奇的關(guān)鍵字,它是C++語(yǔ)法的一部分,卻允許你對(duì)程序的語(yǔ)義做出限制。應(yīng)該盡可能在程序中使用const關(guān)鍵字,這樣編譯器能夠幫你做更多。

class Rational {
    const Rational operator * (const Rational &lhs, const Rational &rhs) {...}
};

int main() {
    Rational a, b, c;
    // should be "==", but will pass compilation if "const" missed
    if((a * b) = c) {...}  
}

const在指針上有兩種語(yǔ)義:1)指針本身不能指向其他地址;2)指針?biāo)傅刂返闹挡荒茏儎?dòng)。分別對(duì)應(yīng):

char greeting[] = "hello";
char* const p1 = greeting;
const char* p2 = greeting;

Item 1中提到了,STL的迭代器是基于指針的。在STL中這兩類(lèi)迭代器分別對(duì)應(yīng):

std::vector<int> vec;
const std::vector<int>::iterator iter1 = vec.begin();
std::vector<int>::const_iterator iter2 = vec.begin();

const在C++中如此重要的另一個(gè)因素是在用戶自定義類(lèi)型上,pass-by-reference-to-const比pass-by-value高效。因此,應(yīng)當(dāng)盡可能多用const關(guān)鍵詞修飾成員函數(shù),已方便在const對(duì)象上的操作。
關(guān)于一個(gè)成員函數(shù)是否能用const關(guān)鍵字,編譯器關(guān)心的是bitwise constness,即函數(shù)內(nèi)部沒(méi)有對(duì)成員變量的賦值;而程序員更應(yīng)當(dāng)關(guān)心程序事實(shí)運(yùn)行中的logical constness,即有些變量可能會(huì)被修改,但需要保持const的變量事實(shí)上是不會(huì)變的,兩個(gè)例子:

// a program to "beat" bitwise constness
// we only apply const function on const object, but the string value changed

#include<iostream>
#include<cstring>

class Text {
    char *content;
    public:
    // ...
    char& operator [] (std::size_t idx) const {
        return content[idx];
    }
};

int main() {
    char greeting[] = "hello";
    const Text text = Text(greeting);
    char *p = &text[0];
    *p = 'j';
    std::cout << text[0] << std::endl;
    return 0;
}

注意,這個(gè)例子并不是說(shuō)編譯器錯(cuò)了,事實(shí)上它的確滿足bitwise constness原則,content指針的值并沒(méi)有發(fā)生變化。但我們明顯是希望content所指向的字符串也保持不變。事實(shí)上,這是使用指針的問(wèn)題,在重載[]時(shí)返回const char&或者用string類(lèi)型作為成員變量,編譯器都能發(fā)現(xiàn)這段程序的錯(cuò)誤行為。

// a snippet explaining "logical constness"
// the const function getLength() updates two variables
// and they are declared as mutable

class Text {
    char *content;
    mutable std::size_t length;
    mutable bool lengthIsValid;
    public:
    // ...
    std::size_t getLength() const {
        if(!lengthIsValid) {
            length = strlen(content);
            lengthIsValid = true;
        }
        return length;
    }
};

最后,很多時(shí)候我們會(huì)為const對(duì)象實(shí)現(xiàn)一個(gè)const成員函數(shù),為非const對(duì)象實(shí)現(xiàn)一個(gè)非const成員函數(shù)。他們行為幾乎完全一樣,如上述重載[]前要做一系列合法性檢查。那么為了減少代碼冗余,一個(gè)常見(jiàn)的操作是用非const函數(shù)調(diào)用const函數(shù),這里展示一下C++風(fēng)格的類(lèi)型轉(zhuǎn)換寫(xiě)法:

const char& operator [] (std::size_t idx) const {
    // many lines of code
    return content[idx];
}
char& operator [] (std::size_t idx) {
    return const_cast<char&>(static_cast<const Text&>(*this)[idx]);
}

Day 3:

Item 4: Make sure objects are initialized before they're used

C++中來(lái)自C語(yǔ)言的數(shù)據(jù)類(lèi)型是不保證初始化的,因此請(qǐng)手動(dòng)初始化所有的內(nèi)置類(lèi)型。

對(duì)于用戶定義類(lèi)型,請(qǐng)使用成員初值列(member initialization list)代替賦值操作,這是出于效率上的考慮:

// constructor in assignment style
A::A(const string &s, const std::list<int> l) {
    str = s; lst = l;
}
// a more efficient constructor
A::A(const string &s, const std::list<int> l): str(s), lst(l) {}

具體來(lái)說(shuō),對(duì)于A類(lèi)的成員變量str和lst(它們都不是內(nèi)置類(lèi)型),它們的初始化時(shí)間在調(diào)用A構(gòu)造函數(shù)之前,也就是說(shuō)在上面的寫(xiě)法中,str和lst先用各自的默認(rèn)構(gòu)造函數(shù)初始化后,又進(jìn)行了一次賦值;而在下面的寫(xiě)法中,它們就是用參數(shù)值初始化的。

對(duì)于跨文件的static變量,請(qǐng)用local-static對(duì)象替換non-local static對(duì)象。因?yàn)榭缥募膕tatic變量初始化順序是不確定的,而有時(shí)我們必須指定特定順序,舉例而言:

// fs.h
class FileSystem {
public:
    // lines of code
    void foo() {}
};

FileSystem& fs() {
    static FileSystem fsObj;
    return fsObj;
}
// fs.cpp
tfs().foo();

調(diào)用全局變量就不太好了:

// fs.cpp
extern FileSystem fs;
fs.foo();

當(dāng)有多個(gè)跨文件的全局變量時(shí),上面寫(xiě)法中FileSystem對(duì)象的創(chuàng)建時(shí)間是可控的,下面則可能造成多個(gè)對(duì)象創(chuàng)建順序的混亂。

Day 4:

Item 5: Know what functions C++ silently writes and calls

你寫(xiě)了一個(gè)空的類(lèi)(只聲明了一些成員變量),C++編譯器會(huì)按需為你生成default構(gòu)造函數(shù),copy構(gòu)造函數(shù),析構(gòu)函數(shù)以及重載賦值符號(hào)。而一旦你聲明了對(duì)應(yīng)的函數(shù),C++就不會(huì)再默默為你做這些事。

class Empty {};
// the above code equals below
class Empty {
    Empty() { ... }
    Empty(const Empty &rhs) { ... }
    ~Empty() { ... }
    Empty& operator = (const Empty &rhs) { ... }
};

這些默認(rèn)函數(shù)的行為都是naive的,對(duì)于基礎(chǔ)類(lèi)型,copy構(gòu)造函數(shù)和賦值只做簡(jiǎn)單的bits拷貝。對(duì)于string這樣定義了copy構(gòu)造函數(shù)和賦值的類(lèi)型,則會(huì)調(diào)用對(duì)應(yīng)函數(shù)。
這種naive方法當(dāng)然有失效的時(shí)候,比如你定義了一個(gè)const成員變量,或者你的成員變量是一個(gè)引用,那么很明顯bits拷貝的方法是不行的,你必須手動(dòng)初始化這兩類(lèi)變量。編譯器會(huì)提示你顯式地編寫(xiě)構(gòu)造函數(shù),copy賦值函數(shù),重載賦值符。

Day 5:

Item 6: Explicitly disallow the use of compiler-generated functions you do not want

有些類(lèi)不應(yīng)當(dāng)存在拷貝操作,讓編譯器幫助你的方法是將這個(gè)類(lèi)的copy構(gòu)造函數(shù)和賦值重載聲明為private

class UnCopyable {
    UnCopyable(const UnCopyable&);  // parameter name can be skipped
    UnCopyable& operator = (const UnCopyable&);
    public:
    // lines of code
};

為了進(jìn)一步防止成員函數(shù)和友元函數(shù)調(diào)用賦值,你需要使這兩個(gè)private函數(shù)的函數(shù)體為空。這樣如果你在成員函數(shù)或友元函數(shù)中使用了賦值,連接器(linker)會(huì)報(bào)錯(cuò)。

Item 7: Declare destructors virtual in polymorphic base classes

先看一份代碼:

#include <iostream>

using namespace std;

class A {
    public:
    int x;
    virtual ~A() {
        cout << "Destructor A is called" << endl;
    }
};

class B: public A {
    public:
    int y;
    ~B() {
        cout << "Destructor B is called" << endl;
    }
};

class C {
    public:
    int x;
    ~C() {
        cout << "Destructor C is called" << endl;
    }
};

int main() {
    A *ptr = new B();
    delete ptr;
    cout << "size A: " << sizeof(A) << endl;
    cout << "size C: " << sizeof(C) << endl;
    return 0;
}

運(yùn)行結(jié)果:

Compiler: GNU G++14 6.4.0
Ouput:
Destructor B is called
Destructor A is called
size A: 8
size C: 4

這份代碼告訴我們兩件事:
1)不要無(wú)端的將一個(gè)類(lèi)的某些函數(shù)聲明為virtual,這會(huì)使你的類(lèi)體積變大,其原因是有虛函數(shù)的類(lèi)會(huì)有一個(gè)虛指針(virtual table pointer),用于指向運(yùn)行時(shí)實(shí)際調(diào)用的函數(shù)。
2)如果一個(gè)類(lèi)是作為基類(lèi)使用,并且會(huì)設(shè)計(jì)多態(tài),那么一定要將構(gòu)造函數(shù)聲明為virtual,否則示例代碼中只會(huì)調(diào)用A類(lèi)的析構(gòu)函數(shù),從而B(niǎo)類(lèi)在堆中為y申請(qǐng)的內(nèi)存會(huì)發(fā)生泄漏。

Day 6:

Item 8: Prevent exceptions from leaving destructors

C++中當(dāng)有多個(gè)異常被拋出時(shí),程序不是結(jié)束執(zhí)行就是導(dǎo)致不明確行為。而如果在析構(gòu)函數(shù)中拋出異常,那么這樣的情況非常容易發(fā)生,比如銷(xiāo)毀一個(gè)vector!因此,絕對(duì)不應(yīng)該讓析構(gòu)函數(shù)拋出異常,而是應(yīng)該在析構(gòu)函數(shù)中就對(duì)其捕捉并處理(考慮剛才提到的危害,在析構(gòu)函數(shù)中發(fā)現(xiàn)異常后直接退出程序也是可以接受的)。
另一個(gè)做法是將可能拋出異常的部分代碼獨(dú)立出來(lái)作為一個(gè)成員函數(shù),讓用戶手動(dòng)去調(diào)用并處理可能帶來(lái)的異常。類(lèi)本身也可以設(shè)置一個(gè)變量記錄進(jìn)入析構(gòu)函數(shù)時(shí)這個(gè)可能拋出異常的程序是否已經(jīng)被調(diào)用。如果沒(méi)有,那么捕獲并處理掉這個(gè)異常,如果已經(jīng)被調(diào)用,那么就可以擺脫異常處理這一煩惱了!這相當(dāng)于是將部分風(fēng)險(xiǎn)責(zé)任分擔(dān)給了用戶從而獲得的“雙保險(xiǎn)”做法。

Item 9: Never call virtual functions during constructor or destructor

虛函數(shù)的作用是在多態(tài)環(huán)境下調(diào)用正確類(lèi)的函數(shù)。而在構(gòu)造函數(shù)和析構(gòu)函數(shù)中使用虛函數(shù)則會(huì)無(wú)法保證這一點(diǎn),這可能會(huì)帶來(lái)困惑。究其原因是一個(gè)派生類(lèi)在調(diào)用自己的構(gòu)造函數(shù)前,會(huì)先調(diào)用基類(lèi)的構(gòu)造函數(shù),此時(shí)這個(gè)派生類(lèi)對(duì)象的運(yùn)行時(shí)類(lèi)型(runtime type information)就會(huì)是基類(lèi)!如果基類(lèi)的構(gòu)造函數(shù)中調(diào)用了虛函數(shù),這個(gè)虛函數(shù)是沒(méi)有辦法指向派生類(lèi)中的實(shí)現(xiàn)的!析構(gòu)函數(shù)的道理類(lèi)似??赐赀@段,不如猜一猜下面代碼的輸出吧!

#include <iostream>

class A {
    int x;
    public:
    A() {
        std::cout << "Construct A" << std::endl;
        foo();
    }
    virtual void foo() {
        std::cout << "A::foo() invocation" << std::endl;
    }
    ~A() {
        std::cout << "Destruct A" << std::endl;
        foo();
    }
};

class B: A {
    int y;
    public:
    B() {
        std::cout << "Construct B" << std::endl;
    }
    void foo() {
        std::cout << "B::foo() invocation" << std::endl;
    }
    ~B() {
        std::cout << "Destruct B" << std::endl;
    }
};

int main() {
    B b;
    return 0;
}

正確答案:

Construct A
A::foo() invocation
Construct B
Destruct B
Destruct A
A::foo() invocation
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容