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

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

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

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

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

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

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

Empty e1;         //默認構造函數(shù)
                  //析構函數(shù)
Empty e2(e1);     //拷貝構造函數(shù)
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修飾的類成員/引用成員數(shù)據(jù)應使用成員初始化列表
        //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;//無法引用 函數(shù) "Dog::operator=(const Dog &)" (已隱式聲明) -- 它是已刪除的函數(shù)
  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:若不想使用編譯器自動生成的函數(shù),就應該明確拒絕

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

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

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

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

條款07:為多態(tài)基類聲明virtual析構函數(shù)

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

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

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

解決方法:

給base class一個virtual析構函數(shù)

注意:

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

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

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

#include<iostream>
using namespace std;
class BasicCamera {
public:
    virtual ~BasicCamera()=0;//如果此處為~BasicCamera(),只會調用父類的析構
};
BasicCamera::~BasicCamera() {
    cout << "調用了BasicCamera的析構函數(shù)" << endl;
}//不加會報錯,必須要定義
class Hik :public BasicCamera {
public:
    ~Hik() {
        cout << "調用了hik的析構函數(shù)" << endl;
    }
};

class Factory {
public:
    BasicCamera* CreateCamera() {
        return new Hik();
    }
};
int main() {
    Factory fac = Factory();
    BasicCamera* camera = fac.CreateCamera();
    delete camera;
    getchar();
    return 0;
}

運行結果:

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

條款08:別讓異常逃離析構函數(shù)

如果析構函數(shù)吐出異常程序可能過早結束或出現(xiàn)不明確行為。比如,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函數(shù)終止了程序。但如果析構函數(shù)必須執(zhí)行一個動作,而該動作可能會在失敗時拋出異常,該怎么辦?

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

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

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

2.吞下因調用close而引發(fā)的異常

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

3. 將異常放在析構函數(shù)之外

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

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

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

?假如在構造函數(shù)中調用了virtual函數(shù):

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

運行結果:

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

原因

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

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

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

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

解決方法

把父類的virtual函數(shù)改為不virtual的,然后子類的構造函數(shù)傳遞必要信息給父類的構造函數(shù),父類的構造函數(shù)就可以調用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函數(shù)從基類向下調用,在構造期間,你可以“令子類將必要的構造信息向上傳遞至基類構造函數(shù)”。

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

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

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

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

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

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

class Widget {
public:
    ...

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

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

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

自我賦值

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

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

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

(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,來保存一個指針指向一塊動態(tài)分配的位圖(bitmap):

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

下面是operate=的實現(xiàn)代碼,

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

表面合理,但自我賦值出現(xiàn)時不安全:

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的數(shù)據(jù)
Widget& operate=(const Widget& rhs){
    Widget temp(rhs);    //為rhs的數(shù)據(jù)做一個復件
    swap(temp);            //將*this數(shù)據(jù)和上述復件的數(shù)據(jù)交換
    return *this
}

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

更激進版:

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

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

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

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

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

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

?

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

#include<iostream>
using namespace std;
//--------父類--------------------------
class BasicCamera { public: BasicCamera() {}//父類必須要又默認構造函數(shù),如果子類沒有在拷貝構造的過程拷貝父類成員,那么子類會調用父類的默認構造函數(shù),對父類的成員變量執(zhí)行缺省的初始化。 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構造函數(shù) 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函數(shù),請確保

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

2.調用所有的base class內(nèi)的適當?shù)腸opying函數(shù)。

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

總結:

05:編譯器可以暗自為class創(chuàng)建default構造函數(shù)、copy構造函數(shù)、copy assignment操作符以及析構函數(shù)。

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

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

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

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

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

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

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

?參考:

1. 《Effective C++》P34-60

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

開通會員,享受整站包年服務立即開通 >