• 當前位置:首頁 > IT技術 > 編程語言 > 正文

    【Effective C++】構造/析構/賦值運算
    2022-04-25 23:11:42

    幾乎你寫的每一個class都會有一個或多個構造函數、一個析構函數,一個copy assignment操作符。如果這些函數犯錯,會導致深遠且令人不愉快的后果,遍及你的整個classes。所以確保它們行為正確時生死攸關的大事。本章提供的引導可讓你把這些函數良好地集結在一起,形成classes的脊柱。

    條款05:了解C++默默編寫并調用哪些函數

    如果你自己沒有聲明,編譯器就會聲明

    • 默認構造函數
    • copy構造函數? ? ? ? ? ? ? ? ? ??//單純地將來源對象的每一個non-static成員變量拷貝到目標對象
    • copy?assignment操作符? ? //同上
    • 析構函數? ? ? ? ? ? ? ? ? ? ? ? ? ? //是個non-virtual

    唯有這些函數被需要(被調用),它們才會被編譯器創建出來,下面代碼造成上述每一個函數被編譯器產出:

    Empty e1;         //默認構造函數
                      //析構函數
    Empty e2(e1);     //拷貝構造函數
    e2=e1;            //copy assignment操作符

    注意:

    如果生成的代碼不合法或者沒有意義時,編譯器會拒絕為class生成operate=:

    (1)不合法

    #include <string>
    #include <iostream>
    using namespace std;
    class Dog {
    public:
        Dog(string& namevalue, int agevalue):name(namevalue),age(agevalue){
            //name = namevalue;  當需要初始化const修飾的類成員/引用成員數據應使用成員初始化列表
            //age = agevalue;
        }
        void show() {
            cout << name << " is " << age << " years old." << endl;
        }
    private:
        string& name;
        const int age;
    };
    
    int main() {
        string s1("Persephone");
        string s2("Satch");    
        Dog d1(s1, 2);
        Dog d2(s2, 36);
        //Dog d1("Persephone", 2);
        //Dog d2("Satch", 36);
        //d1 = d2;//無法引用 函數 "Dog::operator=(const Dog &)" (已隱式聲明) -- 它是已刪除的函數
      d1.show();
      d2.show();
      getchar();
    }

    由于C++不允許“讓引用改指向不同的對象”,編譯器沒有自動生成operate=,d1=d2就會報錯。

    (2)沒有意義

    如果某個base classes將copy assignment操作符聲明為private,編譯器將拒絕為其derived classes生成一個copy assignment操作符。畢竟編譯器為derived classes所生的copy assignment操作想象中可以處理base class成分(條款12)。

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

    有些類,你不想它的對象被拷貝,但如果你不聲明拷貝構造函數,編譯器會自動給你生成。這時候我們可以,

    1)將拷貝構造函數和copy assignment操作符聲明為private(此時member函數和friend函數還是可以調用你的private函數),并且不去定義它。

    2)寫一個base class,它有私有的拷貝構造函數和copy assignment操作符,然后去繼承它,根據上一條,編譯器將拒絕為其生成一個copy assignment操作符。(可能會導致多重繼承

    3)使用Boost提供的版本。(還沒學到

    條款07:為多態基類聲明virtual析構函數

    比如在使用工廠函數時,工廠函數會返回一個base class指針,指向新生成的derived對象。被返回的對象位于heap,因此為了避免泄露內存和其他資源,需要delete該對象。

    BasicCamera* pb = CreateCamera();
    ...
    delete pb;

    指針指向的時子類對象,但卻經由一個base class指針來刪除,而目前的base class有一個non-virtual析構函數(條款05指出,編譯器自動生成的析構函數是non-virtual的)。這會引來災難,因為實際執行時,通常發生的是,對象的derived成分沒被銷毀,base class成分通常會被銷毀,造成一個局部銷毀的對象。

    解決方法:

    給base class一個virtual析構函數

    注意:

    • 任何class只要帶有virtual函數都幾乎確定應該也有一個virtual析構函數。
    • 如果class不含virtual函數,通常表示它不意圖被用作一個base class,此時令其析構函數為virtual,會導致其對象的體積增大,不能傳遞至其他語言的函數。
    • 總而言之,只有當class內含至少一個virtual函數,才為它聲明virtual析構函數
    • 有時候抽象類不想被實體化,比如說BasicCamera創建對象沒有意義,你可以有一個pure virtual函數,這時候可以聲明一個pure virtual的析構函數。
    class BasicCamera {
    public:
        virtual ~BasicCamera() = 0;
    };

    加上virtual,加上“=0”,就成為pure virtual函數了,但是書上說“必須為這個pure virtual析構函數提供一份定義”???由子類繼承后定義可以嗎?

    試了一下,好像確實如此。

    #include<iostream>
    using namespace std;
    class BasicCamera {
    public:
        virtual ~BasicCamera()=0;//如果此處為~BasicCamera(),只會調用父類的析構
    };
    BasicCamera::~BasicCamera() {
        cout << "調用了BasicCamera的析構函數" << endl;
    }//不加會報錯,必須要定義
    class Hik :public BasicCamera {
    public:
        ~Hik() {
            cout << "調用了hik的析構函數" << endl;
        }
    };
    
    class Factory {
    public:
        BasicCamera* CreateCamera() {
            return new Hik();
        }
    };
    int main() {
        Factory fac = Factory();
        BasicCamera* camera = fac.CreateCamera();
        delete camera;
        getchar();
        return 0;
    }

    運行結果:

    ?可以看出,析構函數的運作方式是,最深層派生的那個class(此處為子類hik)的那個析構函數最先被調用,然后是每一個base class的析構函數被調用。編譯器會在子類的析構函數中調用父類的析構函數,所以必須提供一份定義。

    條款08:別讓異常逃離析構函數

    如果析構函數吐出異常程序可能過早結束或出現不明確行為。比如,HikCamera類在析構時拋出異常:

    #include<iostream>
    using namespace std;
    class HikCamera {
    public:
        ~HikCamera() {
            throw 1;
            closed = true;
        }
    private:
        bool closed = false;
    };
    int main() {
        {
            HikCamera cam;
        }
        getchar();
        return 0;
    }

    上圖可以看出,調用了abort函數終止了程序。但如果析構函數必須執行一個動作,而該動作可能會在失敗時拋出異常,該怎么辦?

    1. 如果拋出異常就結束程序(通常通過abort完成):

    ~HikCamera() {
        try {
            throw 1;
            closed = true;
        }
        catch (int i) {
            abort();
        }
    }

    如果程序遭遇一個“于析構期間發生的錯誤”后無法繼續執行,“強迫結束程序”是個合理選項。畢竟它可以阻止異常從析構函數傳播出去(那會導致不明確的行為)。也就是說調用abort可以搶先置“不明確行為”于死地。

    2.吞下因調用close而引發的異常

    ~HikCamera() {
    try {
    throw 1;
            closed = true;}
        catch (int i) {
            cout << "close函數中有異常" << endl;
        }
    }
    

    3. 將異常放在析構函數之外

    提供一個close函數,賦予客戶機會對可能出現的問題作出反應。同時可以在析構函數中追蹤,由析構函數關閉之。

    class HikCamera {
    public:
        void close() {
            throw 1;
            closed = true;
        }
        ~HikCamera() {
            if(!closed){
            try { close(); }
            catch (int i) {
                cout << "close函數中有異常" << endl;
            }}
        }
    private:
        bool closed = false;
    };

    條款09:絕不在構造和析構過程中調用virtual函數

    ?假如在構造函數中調用了virtual函數:

    class BasicCamera {
    public:
        BasicCamera() {
            open();//調用了virtual函數
        }
        virtual void open() {
            cout << "打開了相機" << endl;
        }
    };
    class HikCamera:BasicCamera{
    public:
        HikCamera() {
            cout << "創建了hik相機" << endl;
        }
        void open() {
            cout << "打開了??迪鄼C" << endl;
        }
    };

    運行結果:

    在創建子類對象時,不會創建父類對象,只是初始化子類中屬于父類的成員。父類的構造函數會被調用,運行父類構造函數時,里面的父類版本的virtual函數被調用了,不是子類中的版本。

    原因

    1. 當基類的構造函數執行時,子類的成員變量尚未初始化,如果virtual函數(open函數)調用了子類成員變量,這會導致不明確行為。

    2. 基類構造期間,對象類型為基類。不只是virtual函數被編譯器解析至基類,若使用運行期類型信息,也會把對象視為基類。

    同樣的道理也適用于析構函數。一旦子類的析構函數開始執行,子類成員變量就會呈現未定義值,進入基類析構函數后對象成為基類對象。

    想要確保每一次有子類被建立就調用對應的open(),

    解決方法

    把父類的virtual函數改為不virtual的,然后子類的構造函數傳遞必要信息給父類的構造函數,父類的構造函數就可以調用non-virtual的open()了。像這樣:

    #include<iostream>
    using namespace std;
    class BasicCamera {
    public:
        BasicCamera(const string& info) {
            open(info);
        }
        void open(const string& info) {
            cout << "打開了" << info << "相機" << endl;
        };
    };
    class HikCamera:BasicCamera{
    public:
        HikCamera(string s):BasicCamera(s) {
        }
    };
    int main() {
        HikCamera cam("Hik");
        getchar();
        return 0;
    }

    換句話說,無法使用virtual函數從基類向下調用,在構造期間,你可以“令子類將必要的構造信息向上傳遞至基類構造函數”。

    條款10:令operate=返回一個reference to *this

    關于賦值,可以將它們寫成連鎖的形式:

    int x, y, z;
    x = y = z = 15;     //賦值連鎖形式

    因為賦值所采用的是右結合律,因此,上面的連鎖賦值可以解析為:

    x = (y = (z = 15));

    為了實現這樣的“連鎖賦值”,賦值操作符必須返回一個reference,指向操作符的左側的實參,這也是為classes實現賦值操作符是應該遵守的協議:

    class Widget {
    public:
        ...
    
        Widget& operator=(const Widget& rhs)    //返回類型是一個reference,指向當前的對象
        {
            ...
            return* this;     //返回左側的對象
        }
        ...
    };

    這個協議不僅適用于以上的標準賦值形式,也適用于所有賦值相關運算。例如:+=,*=,-=等。需要注意的是,這只是一個協議,并不是強制的。但是,最好還是這樣做,因為這份協議被所有的內置類型和標準程序庫提供的類型如string,vector,complex,tr1::shared_ptr或即將提供的類型所共同遵守的。

    條款11:在operate=中處理“自我賦值”

    自我賦值

    “自我賦值”發生在對象被賦值給自己時:

    class Widget{ ... };
    Widget w;
    ...
    w = w;  //賦值給自己

    看著有些愚蠢,但它合法,不要以為大家絕對不這么做。以下為某些不明顯“自我賦值”產生的情況:

    (1)指針/引用指向同一個對象

    a[i] = a[j];    //當i = j時
    *px = *py    // 當兩個指針指向同一個東西時

    (2)父類的指針和子類的指針指向同一個對象

    class Base { ... };
    class Derived: public Base { ... };
    void doSomething(const Base& rb, Derived* pd);   //rb和pd可能是同一個對象

    自我賦值的后果

    在我們打算自行管理資源時,自我賦值的情況可能會使我們掉進“在停止使用資源之前意外釋放了它”的陷阱。假設建立一個class,來保存一個指針指向一塊動態分配的位圖(bitmap):

    class Bitmap{ ... };
    class Widget{
        ...
    private:
        Bitmap* pb;   //指針,指向一個從heap分配而得的對象
    };

    下面是operate=的實現代碼,

    Widget& operate=(const Widget& rhs){
        delete pb;                 //停止使用當前的bitmap
        pb = new Bitmap(rhs.pb);   //使用rhs‘s bitmap的副本(不new的話,就指向一個bitmap了
        return *this
    }

    表面合理,但自我賦值出現時不安全:

    Widget rhs;
    Widget w = rhs;  //這是ok的
    
    rhs = rhs;  //第一步,將rhs.pb指向的對象刪掉 

    解決方法

    想要阻止這種做法,有以下方法:

    (1)增加“證同測試”

    Widget& operate=(const Widget& rhs){
        if (this == &rhs) return *this; //如果是自我賦值,不要做任何事
        delete pb;  
        pb = new Bitmap(rhs.pb);  
        return *this
    }

    這樣做行得通,具備了“自我賦值安全性”,但是還不具備“異常安全性”。如果”new Bitmap“導致了異常,Widget最終還是有一個指針指向一塊被刪除的Bitmap。這樣的指針有害,你無法安全地刪除它們,甚至無法安全地讀取它們。

    (2)在復制pb所指東西之前不要刪除pb:

    Widget& operate=(const Widget& rhs){
        Bitmap* pOrig = pb;         //pb和pOrig指向一個對象
        pb = new Bitmap(rhs.pb);  //pb指向rhs.pb的副本
        delete pOrig ;             //利用pOrig刪去舊對象
        return *this
    }

    同時具備了“自我賦值安全性”和“異常安全性”。

    (3)copy and swap技術

    swap(Widget& rhs){ ... } //交換*this和rhs的數據
    Widget& operate=(const Widget& rhs){
        Widget temp(rhs);    //為rhs的數據做一個復件
        swap(temp);            //將*this數據和上述復件的數據交換
        return *this
    }

    如果創建指針的話,需要delete掉,但是創建對象的話,析構函數會delete掉指針。temp拷貝rhs的成功說明new Bitmap沒有異常,然后swap。

    更激進版:

    swap(Widget& rhs){ ... } //交換*this和rhs的數據
    Widget& operate=(Widget rhs){
        swap(rhs);            //將*this數據和上述復件的數據交換
        return *this
    }

    傳入值時,會自動生成復件。此時的rhs就是原rhs的拷貝。

    條款12:復制對象時勿忘其每一個成分

    copy構造函數和copy assignment操作符我們稱之為copying函數,編譯器會自動為我們的classes創建copying函數,將拷貝對象的所有成員變量都做一份拷貝。如果你聲明自己的copying函數,可能會導致:

    1. 如果你漏了一個成員變量沒有復制,大多數編譯器不會告訴你

    2. 繼承時,子類的copying函數只復制了子類的成員,而父類的成員變量會被默認的構造函數初始化

    ?

    PriorityCustomer的copying函數沒有復制Customer成員變量,PriorityCustomer的copy構造函數并沒有指定實參傳給其base class構造函數,因此PriorityCustomer對象的Customer成分會被不帶實參的父類default構造函數初始化(上面的是偽代碼,父類必須要有默認構造函數)。父類的default構造函數將對name和lastTransaction執行缺省的初始化動作。
    base class 的成分往往是private, 所以你無法直接訪問他們,你應該讓derived class的copying函數調用相應的base class函數:

    #include<iostream>
    using namespace std;
    //--------父類--------------------------
    class BasicCamera { public: BasicCamera() {}//父類必須要又默認構造函數,如果子類沒有在拷貝構造的過程拷貝父類成員,那么子類會調用父類的默認構造函數,對父類的成員變量執行缺省的初始化。 BasicCamera(const BasicCamera& rhs) { name = rhs.name; } BasicCamera& operator=(const BasicCamera& rhs) {} void setname(string s) { name = s; } void show() { cout << "name is " << name << endl; } protected: private: std::string name; };
    //---------------子類--------------
    class HikCamera : public BasicCamera { public: HikCamera() {}; HikCamera(const HikCamera& rhs) :BasicCamera(rhs), price(rhs.price) { };//調用基類的copy構造函數 HikCamera& operator=(const HikCamera& rhs) { BasicCamera::operator =(rhs);//對基類成分進行賦值 price = rhs.price; return *this; }; void set(int x, string s) { BasicCamera::setname(s); price = x; } void show() { BasicCamera::show(); cout << "price is " << price << endl; } protected: private: int price; }; int main() { HikCamera cam; string s = "hik"; cam.set(100, s); HikCamera cam1(cam); cam1.show(); getchar(); }

    運行結果

    如果沒有BasicCamera(rhs),運行結果:

    ?

    總結:

    當你編寫一個copying函數,請確保

    1.復制所有的local成員變量,

    2.調用所有的base class內的適當的copying函數。

    當這兩個copying函數有近似相同的實現本體,令一個copying函數調用另一個copying函數無法讓你達到你想要的目標。

    總結:

    05:編譯器可以暗自為class創建default構造函數、copy構造函數、copy assignment操作符以及析構函數。

    08:析構函數絕對不要吐出異常。如果一個析構函數調用的函數可能拋出異常,析構函數應該捕捉任何異常,然后吞下它們(不傳播)或結束程序。

    08:如果客戶需要對某個操作函數運行期間拋出的異常作出反應,那么class應該提供一個普通函數(而非析構函數中)執行該操作。

    10:令賦值操作符返回一個reference to *this。

    確保當對象自我賦值時operate=有良好行為,其中技術包括比較“來源對象”和目標對象“的地址、精心周到的語句順序、以及cpoy and swap。

    確定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象時,其行為仍然正確。

    12:Copying函數應該確保復制“對象內的所有成員變量”及”所有base class成分“。

    12:不要嘗試以某個copying函數實現另一個copying函數。應該將共同機能放進第三個函數中,并由兩個copying函數共同調用。

    ?參考:

    1. 《Effective C++》P34-60

    本文摘自 :https://www.cnblogs.com/

    開通會員,享受整站包年服務
    国产呦精品一区二区三区网站|久久www免费人咸|精品无码人妻一区二区|久99久热只有精品国产15|中文字幕亚洲无线码