【Cpp】《Effective C++》第一章-讓自己習慣C++

  這是我在學習《Effective C++》中總結得出的心得與體會,完全是以我自己的理解所作的筆記,是對個人經驗的積累。基於第三版我以每個章節進行總結,全書一共九個章節。

第一章-讓自己習慣C++

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

  View C++ as a federation of languages.
  在初期Cpp被開發之初,Cpp並沒有如今這樣豐富的功能,它更應該被稱爲僅是一個帶有面向對象特性的C,即C with Classes。但如今的Cpp已經在其基礎紙上新增了諸多特性例如模板更好的支持了泛型編程,STL給我們提供了更多的工具,加上其完全兼容C使其既可以面向對象編程也可以面向過程編程,這就使Cpp成爲了一個歷史上獨一無二絕無僅有的強大存在,其也可以被理解爲多個語言的組合,我們這些語言稱爲次語言(sublanguage)
  也正因Cpp可以被看爲多個次語言的組合,使它的語法更爲複雜,標準更加難以捉摸,但是當我們在使用某個次語言的時候,守則與通例都會變得更加簡單易懂。然而當我們從一個次語言切換到另一個時,守則往往也會改變。
  在《Effective C++》中將Cpp分爲了四種次語言,是我們可以更加直觀的正視這門複雜的語言。
  4種次語言:
  1、C:Cpp是以C爲基礎,Cpp的編程是更爲高級的C編程,其可以是爲是對C的封裝及升級,並且Cpp中的諸多語法都是來自於C,例如指針、數組、預處理等等。但當我們僅僅使用C這一次語言的時候的我們則要考慮守則與規範在這一次語言中的極限,因爲在這裏沒有模板,沒有異常,沒有重載。因此當我們在Cpp內使用純C進行編程時要注意讓自己遵守的規範從C的角度出發,考慮更爲底層更爲細節的方面。
  2、Object-Oriented C++:這一部分可以看作是Cpp開發之初C with Classes所實現的部分,是帶面向對象版本的C。在這一次語言中凸顯了Cpp面向對象的特點例如封裝、繼承、多態、動態綁定等。在使用這部分的次語言時我們則要遵守面向對象在Cpp上的守則。
  3、Template C++:這是Cpp的泛型編程部分,在這一部分有着模板這一概念,而模板的書寫往往是在給高級編程搭輪子。模板編程瀰漫了整個Cpp,並且帶來了嶄新的編程範型模板元編程(template meteprogramming/TMP)。唯獨模板才適用的規範也並不在少數,但是這些與Cpp主流編程並不衝突。
  4、STL:STL是Cpp中標準的模板庫,也是最常用最常見的工具庫,其中六大組件互相配合,當然如果你使用STL那麼也要遵守STL獨有的規範。
  在使用不同的次語言時就有着不同的規範守則,當你從一個次語言切換到另一個時高效編程守則有可能需要你改變策略。例如在對內置類型傳參時用值傳參(pass-by-value)往往比傳引用(pass-by-reference)更加高效,但當你如果從C part of C++這一次語言移往Object-Oriented C++,對於用戶自定義的類型在傳參時往往是傳引用(pass-by-reference)更加高效,因爲省略掉了一次拷貝構造,避免了不必要的開銷,對於Template C++ 來說也是這樣。
  由以上可以看出當我們在使用Cpp進行編程時,使用不同部分的次語言爲了更加高效的進行編程往往就需要遵守不同的守則,這些守則有可能在不同的次語言間是相互違背的,所以在不同的次語言見進行轉換時我們的策略可能也需要進行改變。這也是爲什麼要將Cpp分爲這些次語言的原因這點很重要。
  牢記一點:Cpp的高效編程視情況而變化,取決於使用Cpp的哪一部分。

儘量以const,enum,inline替換#define

  Prefer consts,enums,and inlines to #defines.
  在這一條款中,要求程序員最好可以用編譯器來代替預編譯器,因爲預編譯器並不可以視爲語言的一部分。

用const,enum替代宏定義常量

  預編譯器中你定義的宏的名稱並不會計入符號表內,從而使得調試及排錯十分困難。因此我們儘量使用常量定義來代替宏定義,首當其衝就是爲了避免宏定義使得其名稱無法計入符號表。
  但是對於定義常量來代替宏有兩點特殊情況需要討論:
  1、定義常量指針(constant pointers)。關於常量指針和指向常量的指針在C語言章節就有解釋。在本書中這裏指的是一個完全是常量的指針,即是一個既是本身無法改變並且所指內容也無法改變的指針,因此我們爲了定義這樣一個常量需要將const寫兩遍才行。例:const char* const authorName = "Misaki";,但這種情況下利用Cpp中的STL中的string類反倒更好定義。
  2、類的成員專屬常量。這種常量爲了讓其只有一份實體,因此要讓其成爲static的。一般情況下我們在定義一個靜態的成員變量時都需要在類外進行定義,然而如果這個靜態成員變量具有常性,即是const的,並且它爲整數類型(integral type),再並且只要不取它們的地址則它無需在類外進行定義。

#include <iostream>
using namespace std;
class GamePlayer
{
public:
    static const int NumTurns = 5;
    int scores[NumTurns];
};
//const int GamePlayer::NumTurns; //不給予數值的定義式
int main()
{
    cout << GamePlayer::NumTurns << endl;
}


5

  當然如果你要取他們的地址或者你的編譯器堅持要看到定義式則也是可以加上定義式的,但是 不可以再次給予數值,因爲其載聲明時已經獲得初值了。
  但是我們無法使用宏定義的方式來定義一個成員常量,宏並不在乎作用域,受#define#undef等宏的控制,因此宏並不具備封裝性。
  但是有的編譯器也許不支持在聲明中就獲得初值的in-class初值設定,呢麼我們只好在定義中加入初值。

#include <iostream>
using namespace std;
class GamePlayer
{
public:
    static const int NumTurns;
    //int scores[NumTurns];//編譯器要求在編譯時就能確定數組的大小
};
const int GamePlayer::NumTurns = 5; 
int main()
{
    cout << GamePlayer::NumTurns << endl;
}


5

  但這種情況下我們並無法在編譯期間獲得一個常量值,因此無法用其定義數組,但是也有其他解決方法,改用the enum hack的補償做法。一個枚舉類型的數值可權充int被使用。

#include <iostream>
using namespace std;
class GamePlayer
{
public:
    //static const int NumTurns;
    enum 
    {
        NumTurns = 5
    };
    int scores[NumTurns];//編譯器要求在編譯時就能確定數組的大小
};
//const int GamePlayer::NumTurns = 5; 
int main()
{
    cout << GamePlayer::NumTurns << endl;
}


5

  這樣也可以在編譯時就確定常量的值。通過這個enum hack我們也可以直接在編譯期間使用常量,然而enum hack在某些方面的表現更接近於#define宏定義而不是const常量,例如取一個const常量的地址是合法的,然而取#defineenum的地址是不合法的。enum hack在很多方面都很實用,並且是模板元編程的而基礎技術。

用inline替代宏函數

  另一個#define很坑的地方就是用它去實現宏函數。因爲宏只是單純的文本替換因此我們經常要給所有實參都加上括號來防止意外發生,但這樣會大大降低代碼可讀性。因此書中極大程度建議使用inline函數來代替宏函數,它同樣可以節省函數調用的額外開銷,但是有着更好的可讀性。並且在類中還可以定義專屬於類內的private inline函數,而宏定義做不到此事。
  我們總結一下宏的缺點
  1、定義常量不會計入記號表(symbol table),因此不便於調試。
  2、無法定義成員常量,宏並不重視作用域。
  3、宏的使用,尤其是宏函數會使代碼可讀性變差,不便於理解。
  因此對於常量,最好以constenum替代#define;對於宏函數最好用inline函數替代#define。在用const定義常量的時候也有兩點特殊情況需要注意,一個是常量指針一個是類專屬常量。
  我們基本可以用enum,const,inline替代#define所有使用場景,降低了對預處理器的使用,但是也並非完全消除,預處理器依然有舉足輕重的作用,我們只能儘可能減少對其的使用。比如說當我們在寫日誌檢錯時就需要用到宏,需要知道具體是哪一行發生了錯誤,而如果使用函數則會跳轉到函數中。

總結

  1、對於單純常量,最好以const對象或enums替換#define。
  2、對於宏函數,最好以inline函數替代。

儘可能使用const

  Use const whenever possible.
  const多才多藝,可以用來修飾相當多的變量或函數,一旦被修飾則該對象會被編譯器強制約束來保持它自身不會被改變。

const對於指針和迭代器

  對於指針而言 const出現在*左邊表示被指示物是常量,如果出現在*右邊則表示指針本身是常量,當然其也可以兩邊都出現表示被指示物和指針本身都是常量。
  通過對STL中容器的模擬實現我們也可以很清楚的知道迭代器底層的實現其實就是一個指針,因此迭代器對const來說也有着和指針一樣的應用。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> vec = {1, 2, 3, 4};
    //我們給接下來這個迭代器加上const
    //其類似於T* const指針,注意不是const T*雖然const寫在前面,但這裏const強調的是迭代器自身的不可變
    //迭代器也不過是一個typedef類型而已
    const vector<int>::iterator iter = vec.begin();
    *iter = 5;//合法,迭代器所指對象可變
    //iter++;//不合法,迭代器自身不可變
    cout << vec[0] << endl;
    //這裏相當於一個const T*指針,指向內容不可改,自身可改
    vector<int>::const_iterator const_iter = vec.begin();
    //*const_iter = 1;//不合法,指向內容不可改
    const_iter++;//合法,自身可變
    cout << *const_iter << endl;
}


5
2

const對於函數的應用

  const可以與一個函數的返回值、參數、自身產生關聯。在這裏我們先討論對返回值以及參數的關聯。
  對於函數的返回值,我們用const進行修飾來避免產生一些意外情況。這裏我用書中舉得例子,來重載一個有理數類的operator*運算符。

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

  這裏參數設置爲引用加const修飾的原因是爲了減少拷貝構造加快效率同時避免對操作數的修改,很好理解,但是返回值加const或許比較難以想到,這是因爲總可能有人會寫出這樣違法的代碼。

Rational a, b, c;
(a * b) = c;//在a * b結果上調用operator=

  這段代碼如果我們對operator*重載的返回值並未加上const編譯器是允許編的過這樣違法的操作的,但這明顯是不合常理的是我們想命令禁止的,因此對於用const來修飾函數的返回值也有着莫大的必要。
  對於const修飾參數已經再熟悉不過了,只要你不想修改它都建議將其定義爲const,這是很好的習慣。

const成員函數

  接下來我們着重討論如何const對於函數自身的修飾,而這些修飾都與類成員函數有關。
  const修飾成員函數自身就是將其修飾爲常成員函數,在這種函數中無法修改調用對象自身的數據,因爲傳入的是一個constthis指針。同時我們還要注意一點很重要的語法,常成員函數與普通成員函數間會構成重載,即使兩個成員函數的參數列表完全相同,但只要他們在同一個類中並且函數名相同,僅由常量性constness不同也是可以構成重載的。但是注意此時其構成重載的原因並非是this指針這個參數被const修飾,因爲const修飾參數是構不成函數重載的,這裏的重載原因是因爲函數常量性的不同,與參數無關
  下面這個例子就展現了利用成員函數常量性的不同所構成重載。當普通成員函數與常成員函數發生重載時,普通對象會優先自動調用普通成員函數,常對象會優先自動調用常成員函數。

#include <iostream>
#include <string>
using namespace std;
class TextBook
{
public:
    //重載operator[]
    //這裏返回值加const是爲了讓const對象自身不被修改不造成錯誤
    const char& operator[](size_t position) const
    {
        return text[position];
    }
    char& operator[](size_t position)
    {
        return text[position];
    }
private:
    string text = "123456";
};
void Print(const TextBook& book1, TextBook& book2)
{
    cout << book1[0] << endl;//合法
    cout << book2[0] << endl;//合法
    book2[0] = '5';//合法,普通對象調用普通成員函數,返回值可改
    cout << static_cast<const TextBook&>(book2)[0] << endl;
    //static_cast<const TextBook&>(book2)[0] = '1';//非法這裏我們將其轉換爲const類型對象發現可以主動調用常成員函數
    //book1[0] = '5';//非法,常量對象調用常成員函數而其返回值是const修飾的因此不可改
}
int main()
{
    TextBook book;
    Print(book, book);
}


1
1
5

  並且通過以上的代碼我還發現雖然普通對象可以調用常成員函數,但是當其與普通成員函數構成重載時我們想要顯示調用常成員函數必須將對象轉換爲const的,這完全沒有必要,屬於脫褲子放屁的一種手段。

bitwise constness和logical constness

  如果一個成員函數是常成員函數,則就會引申出這樣兩個流行概念。
  bitwise const陣營的人相信只有成員函數不改變成員的任何變量的時候纔可以說其爲const的,這也是Cpp中對常量性的定義,const成員函數不可以更改對象內任何的non-static成員變量。
  但是不幸的是在Cpp中很容易對這一觀點進行反駁,很多const儘管已經對對象的成員進行了更改依然可以編譯通過。比如說成員中有指針的存在。比如以下這個例子。

#include <iostream>
#include <string.h>
using namespace std;
class CTextBook
{
friend ostream& operator<<(ostream &cout, const CTextBook& text);
public:
    CTextBook(const char* str)
        :_text(nullptr)
    {
        int capacity = strlen(str) + 1;
        _text = new char[capacity];
        strcpy(_text, str);
    }
    ~CTextBook()
    {
        if(_text != nullptr)
        {
            delete[] _text;
        }
    }
    char& operator[](size_t position) const
    {
        return _text[position];
    }
private:
    char* _text;
};
ostream& operator<<(ostream &cout, const CTextBook& text)
{
    cout << text._text << endl;
    return cout;
}
int main()
{
    CTextBook text("Hello");
    text[0] = 'J';
    cout << text;
}


Jello

  以上這個例子中我們在const成員函數中並沒有改變成員變量,因此編譯器並沒有報錯,以此欺騙了編譯器,但是並不代表我們不能通過const成員函數改變成員內的對象。由此即產生了第二個陣營對const成員函數的理解即logical constness
  這個陣營中的人主張const成員函數可以改變其所處理的對象中的某些成員變量,但是只有在客戶端偵測不出的情況下纔可以這樣。但是還有一些情況下我們要修改一些本身可以修改的成員變量,他們的修改從邏輯角度考慮是合理的,是必須的,但是此時編譯器由於語法的關係判定其不符合bitwise constness,因此判定其報錯不通過編譯。例如以下這種情況。

#include <iostream>
#include <string.h>
using namespace std;
class CTextBook
{
friend ostream& operator<<(ostream &cout, const CTextBook& text);
public:
    CTextBook(const char* str)
        :_text(nullptr)
    {
        int capacity = strlen(str) + 1;
        _text = new char[capacity];
        strcpy(_text, str);
    }
    ~CTextBook()
    {
        if(_text != nullptr)
        {
            delete[] _text;
        }
    }
    char& operator[](size_t position) const
    {
        return _text[position];
    }
    size_t length() const
    {
        if(!_lengthIsValid)
        {
            //這裏明顯是編譯不過的,因爲其不是bitwiss constness的
            _len = strlen(_text);
            _lengthIsValid = true;
        }
        return _len;
    }
private:
    char* _text;
    int _len;
    bool _lengthIsValid;
};
ostream& operator<<(ostream &cout, CTextBook& text)
{
    cout << text._text << endl;
    return cout;
}
int main()
{
    CTextBook text("Hello");
    text[0] = 'J';
    cout << text;
}

  以上這個例子肯定是編譯不過的,其不符合編譯器的編譯標準,但是從邏輯角度來看這些賦值和改變我們是可以接受了,但是編譯器不同意怎麼辦呢?解決這個問題需要用到mutable釋放掉non-static成員變量的bitwiss constness約束。

#include <iostream>
#include <string.h>
using namespace std;
class CTextBook
{
friend ostream& operator<<(ostream &cout, const CTextBook& text);
public:
    CTextBook(const char* str)
        :_text(nullptr)
        ,_len(0)
        ,_lengthIsValid(false)
    {
        int capacity = strlen(str) + 1;
        _text = new char[capacity];
        strcpy(_text, str);
    }
    ~CTextBook()
    {
        if(_text != nullptr)
        {
            delete[] _text;
        }
    }
    char& operator[](size_t position) const
    {
        return _text[position];
    }
    size_t length() const
    {
        if(!_lengthIsValid)
        {
            //這裏明顯是編譯不過的,因爲其不是bitwiss constness的
            _len = strlen(_text);
            _lengthIsValid = true;
        }
        return _len;
    }
private:
    char* _text;
    mutable int _len;
    mutable bool _lengthIsValid;
};
ostream& operator<<(ostream &cout, const CTextBook& text)
{
    cout << text._text << endl;
    return cout;
}
int main()
{
    CTextBook text("Hello");
    text[0] = 'J';
    cout << text;
    cout << text.length() << endl;
}


Jello
5

  mutable雖然可以解決bitwise constness而不是logical constness的問題。但是不能解決所有的問題,比如constnon-const成員函數中代碼冗餘的問題,我們要實現兩個功能完全相同的函數只是爲了讓常量性不同的對象調用,這樣就會造成代碼冗餘。最好的解決方法就是non-const函數中調用const函數。注意以上這段話不能反過來,我們不能夠在const函數中調用non-const函數。然而要想完成這次調用我們需要有兩次轉型。

#include <iostream>
#include <string.h>
#include <assert.h>
using namespace std;
class CTextBook
{
friend ostream& operator<<(ostream &cout, const CTextBook& text);
public:
    CTextBook(const char* str)
        :_text(nullptr)
        ,_len(0)
        ,_lengthIsValid(false)
    {
        int capacity = strlen(str) + 1;
        _text = new char[capacity];
        strcpy(_text, str);
    }
    ~CTextBook()
    {
        if(_text != nullptr)
        {
            delete[] _text;
        }
    }
    //const函數
    const char& operator[](size_t position) const
    {
        //在這裏加上檢查,我們先少些點代碼,假設這之前有更多的代碼
        //......
        assert(position < length());
        return _text[position];
    }
    //non-const函數
    char& operator[](size_t position)
    {
        //這裏只需要調用const版本的函數即可,但是爲了調用要先進行類型轉換
        //這裏用到了將本身的對象轉換爲常對象來調用常成員函數
        const char& a = static_cast<const CTextBook&>(*this)[position];
        //這裏將返回值的const限定去掉
        return const_cast<char&>(a);
    }
    size_t length() const
    {
        if(!_lengthIsValid)
        {
            //這裏明顯是編譯不過的,因爲其不是bitwiss constness的
            _len = strlen(_text);
            _lengthIsValid = true;
        }
        return _len;
    }
private:
    char* _text;
    mutable int _len;
    mutable bool _lengthIsValid;
};
ostream& operator<<(ostream &cout, const CTextBook& text)
{
    cout << text._text << endl;
    return cout;
}
int main()
{
    CTextBook text("Hello");
    const CTextBook con_text("Hello");
    //con_text[0] = 'J';//常量成員調用常成員函數,無法更改
    text[0] = 'J';
    cout << text;
    cout << text.length() << endl;
}


Jello
5

  但是要注意這其中進行了兩次轉型,這是十分不安全的儘量還是少使用,但是儘管如此我們還是達成了我們避免數據冗餘的問題。

總結

  1、將某些東西聲明爲const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數本體。
  2、扁你其前置實施bitwise constness,但你編寫程序時應該使用logical constness
  3、constnon-const成員函數有着實質等價時,令non-const版本調用const版本可避免代碼重複。

確定對象被使用前已先被初始化

  Make sure that objects are initialized before they`re used.

C part of C++

  在Cpp中是存在不會被初始化的變量的,無論這些變量是成員變量來自類中,還是內置類型的變量都有可能編譯器不會幫助我們初始化,這是十分危險的,因爲會導致不明確的行爲。如果我們在使用C part of C++時變量往往並不會進行初始化例如數組往往需要我們手動給一個初值,而我們在使用Cpp中的vector時編譯器卻可以保證即使我們不給初值這個數組也會被初始化,如果熟悉STL庫的話,你會知道vecotr此時會將內部的三個成員變量全部初始化爲空,長度和容量都爲0。所以在使用C part of C++時我們最好手動爲每一個變量賦予初值,這是很好的編程習慣。

自定義類型

  對於自定義類型以外的所有類型,他們的初始化都落在了構造函數的身上,我們所要作的就是確保每一個構造函數都將對象的每一個成員初始化。
  但是這裏要搞清楚賦值和初始化的概念。這裏就牽扯到了構造函數的使用。我們都知道我們可以在構造函數中對成員賦值,而是用初始化列表纔可以對成員進行初始化。所以經常可以看到有些對Cpp不是很瞭解的程序員寫出這樣的構造函數。

#include <iostream>
using namespace std;
class PhoneNumber
{
public:
    PhoneNumber()
    {
        cout << "PhoneNumber()" << endl;
    }
    PhoneNumber(string theNumber)
    {
        cout << "PhoneNumber(string theNumber)" << endl;
        string _theNumber;
    }
    PhoneNumber operator=(const PhoneNumber& phoneNumber)
    {
        _theNumber = phoneNumber._theNumber;
        cout << "PhoneNumber operator=(const PhoneNumber& phoneNumber))" << endl;
    }
private:
    string _theNumber;
};
class ABEntry
{
public:
    ABEntry(const string& theName, const string& theAddress, const PhoneNumber& theNumber)
    {
        //這裏的構造函數寫法並不是直接對成員進行初始化,更應該說是在初始化後進行了一次賦值
        _theName = theName;
        _theAddress = theAddress;
        //所以這裏會調用賦值函數
        _theNumber = theNumber;
    }
private:
    string _theName;
    string _theAddress;
    PhoneNumber _theNumber;
};
int main()
{
    ABEntry abEntry("Misaki", "China", PhoneNumber("181********"));
}


PhoneNumber(string theNumber)
PhoneNumber()
PhoneNumber operator=(const PhoneNumber& phoneNumber))

  這種構造函數的寫法並不能爲成員變量進行初始化,而是將變量先初始化後再進行賦值,在過程中產生了一次無參構造,一次有參構造,以及一次賦值,這會消耗更多的性能。而最佳的處理手段是將成員變量的初始化放進初始化列表中。

#include <iostream>
using namespace std;
class PhoneNumber
{
public:
    PhoneNumber()
    {
        cout << "PhoneNumber()" << endl;
    }
    PhoneNumber(string theNumber)
    {
        cout << "PhoneNumber(string theNumber)" << endl;
        string _theNumber;
    }
    PhoneNumber(const PhoneNumber& phoneNumber)
    {
        _theNumber = phoneNumber._theNumber;
        cout << "PhoneNumber(const PhoneNumber& phoneNumber)" << endl;
    }
    PhoneNumber operator=(const PhoneNumber& phoneNumber)
    {
        _theNumber = phoneNumber._theNumber;
        cout << "PhoneNumber operator=(const PhoneNumber& phoneNumber))" << endl;
    }
private:
    string _theNumber;
};
class ABEntry
{
public:
    //這樣才能對成員進行初始化,從結果也可以看出這裏少了一次無參的默認構造
    ABEntry(const string& theName, const string& theAddress, const PhoneNumber& theNumber)
        :_theName(theName)
        ,_theAddress(theAddress)
        ,_theNumber(theNumber)
    {
    }
private:
    string _theName;
    string _theAddress;
    PhoneNumber _theNumber;
};
int main()
{
    ABEntry abEntry("Misaki", "China", PhoneNumber("181********"));
}


PhoneNumber(string theNumber)
PhoneNumber(const PhoneNumber& phoneNumber)

  建議再初始化列表中對成員變量進行初始化,並且建議在初始化列表中對所有的成員包括基類都能進行手動初始化,無論是自定義類型還是內置類型,這樣就不用考慮哪些成員是可以不初始化的成員,這樣的編程習慣也是極好的。
  對於成員變量的初始化順序,其順序並不由初始化列表而決定,基類總是優先於派生類進行初始化,而成員變量是以其聲明次序進行初始化的,因此如果你如果定義了一個變量並決定把它作爲數組的大小來初始化數組,那麼你要保證在聲明數組前那個代表大小的變量應該現有值。因此爲了方便理解,增強可讀性我們在構造函數中書寫初始化列表時最好是按照聲明順序對其進行初始化。

不同編譯單元內定義之non-local static對象的初始化

  這一句話很長,很難理解,我們大概可以理解爲在不同文件中的靜態(static)變量的初始化順序。我們想要在一個源文件中使用另一個源文件中定義的全局變量,但是這連個源文件中的變量初始化的先後順序是並沒有明確規定的,因此可能會出現問題。

FileSystem.h

#include <iostream>
using namespace std;
class FileSystem
{
public:
    size_t numDisks() const
    {
        return _disks;
    }
private:
    size_t _disks = 5;
};

第一個.cpp
#include <iostream>
#include "FileSystem.h"
using namespace std;
FileSystem tfs;//其中定義一個全局變量

第二個.cpp
#include <iostream>
#include "FileSystem.h"
using namespace std;
extern FileSystem tfs;
class Directory
{
public:
    Directory()
    {
        _disks = tfs.numDisks();
    }
    size_t _disks;
};
int main()
{
    Directory dir;
    cout << dir._disks << endl;
}

  以上這段代碼中我們在一個源碼文件中定義了一個成員變量,而在另一個源文件中聲明外部變量並且使用了這個變量對我們的成員變量進行了初始化,但是我們並無法確定這個外部變量編譯器在處理時是否會先對其進行初始化,如果沒有初始化而我們又用它對我們的成員進行初始化此時就會無法進行指定的初始化。因此我們要使用一種間接的方式設計它,來保證外部成員一定會先進行初始化。

FileSystem.h

#include <iostream>
using namespace std;
class FileSystem
{
public:
    size_t numDisks() const
    {
        return _disks;
    }
private:
    size_t _disks = 5;
};
FileSystem& tfs();

第一個.cpp
#include <iostream>
#include "FileSystem.h"
using namespace std;
//這裏的設計模式十分類似於單例模式
//在函數內部定義靜態變量,這樣保證在調用函數時一定會先對變量進行初始化
//保證初始化順序
FileSystem& tfs()
{
    static FileSystem fs;
    return fs;
}

第二個.cpp
#include <iostream>
#include "FileSystem.h"
using namespace std;
class Directory
{
public:
    Directory()
    {
        _disks = tfs().numDisks();
    }
    size_t _disks;
};
int main()
{
    Directory dir;
    cout << dir._disks << endl;
}

  這樣的設計模式更加安全可靠。

總結

  1、爲內置類型進行手動初始化,Cpp並不保證會對其進行初始化。
  2、構造函數最好使用初始化列表對成員進行初始化,而不是使用賦值操作,並且初始化時最好按照聲明次序進行初始化,增強可讀性。
  3、爲免除“跨編譯單元之初始化次序”問題,最好書寫接口,利用local static對象代替non-local static對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章