rule of three

因?yàn)镃++語(yǔ)言沒(méi)有內(nèi)嵌的GC機(jī)制,C++程序員不得不操心動(dòng)態(tài)內(nèi)存管理的問(wèn)題。而工程中很多內(nèi)存管理的問(wèn)題都是由于違背了rule of three的原則惜互。
按照rule of three的要求,如果一個(gè)類(lèi)顯式地定義下列其中一個(gè)成員函數(shù),那么其他兩個(gè)成員函數(shù)也應(yīng)該一起被定義橄维。也就是說(shuō)這三個(gè)函數(shù)要么都不定義,要么都要定義拴曲。

  • 析構(gòu)函數(shù)(destructor
  • 復(fù)制構(gòu)造函數(shù)(copy constructor
  • 復(fù)制賦值運(yùn)算符(copy assignment operator

下面通過(guò)一個(gè)簡(jiǎn)單的例子說(shuō)明違反這個(gè)原則所帶來(lái)的潛在的內(nèi)存問(wèn)題争舞。

學(xué)生管理系統(tǒng)

學(xué)生類(lèi)的實(shí)現(xiàn)

Student類(lèi),用來(lái)存儲(chǔ)學(xué)生的姓名及年齡澈灼。在構(gòu)造函數(shù)中根據(jù)name所指向的字符串的長(zhǎng)度按需分配內(nèi)存竞川,在析構(gòu)中釋放分配的內(nèi)存以防止內(nèi)存泄露。

struct Student {
    Student(const char * theName, int theAge)
        : name(createName(theName))
        , age(theAge) {
        std::cout << "construct Student" << std::endl;
    }
    ~Student() {
        std::cout << "destruct Student" << std::endl;
        if (name != NULL) {
            delete[] name;
            name = NULL;
        }
    }
private:
    static char * createName(const char * theName) {
        if (theName == NULL) {
            return NULL;
        }
        char * ptr = new(std::nothrow) char[strlen(theName) + 1];
        if (ptr == NULL) {
            return NULL;
        }
        strcpy(ptr, theName);
        return ptr;
    }
private:
    char * name;
    int age;
};

學(xué)校類(lèi)的實(shí)現(xiàn)

學(xué)校類(lèi)用來(lái)存儲(chǔ)學(xué)校里所有的學(xué)生信息

struct School {
    void addStudent(const Student & student) {
        students.push_back(student);
    }
private:
    std::vector<Student> students;
};

main函數(shù)中我們構(gòu)造alicetom兩名學(xué)生叁熔,并把他們加入到學(xué)校中委乌。

int main() {
    Student tom("Tom", 17);
    Student alice("Alice", 16);
    School school;
    school.addStudent(tom);
    school.addStudent(alice);
    return 0;
}

結(jié)果分析

編譯運(yùn)行這段代碼,發(fā)現(xiàn)程序運(yùn)行出現(xiàn)了內(nèi)存錯(cuò)誤:

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

從運(yùn)行結(jié)果上看荣回,Student的構(gòu)造函數(shù)被執(zhí)行了兩次遭贸,析構(gòu)函數(shù)里的log輸出了三次。我們顯式構(gòu)造了tomalice兩個(gè)實(shí)例心软,這兩個(gè)對(duì)象在程序退出時(shí)會(huì)分別分別調(diào)用一次析構(gòu)函數(shù)革砸。那么,多出來(lái)的哪次析構(gòu)函數(shù)調(diào)用是哪兒觸發(fā)的呢糯累?答案就在School類(lèi)中算利。
School類(lèi)的成員變量std::vector<Student> students;用來(lái)存儲(chǔ)所有學(xué)生的信息,在students.push_back函數(shù)中泳姐,會(huì)調(diào)用Student類(lèi)的拷貝構(gòu)造函數(shù)來(lái)構(gòu)造一個(gè)Student對(duì)象效拭,插入到vector的尾部,程序退出時(shí),變量school被析構(gòu)缎患,school的成員變量students以及students里的每一個(gè)Student實(shí)例都會(huì)會(huì)被析構(gòu)慕的。
可是Students類(lèi)中并沒(méi)有實(shí)現(xiàn)拷貝構(gòu)造函數(shù),為什么能夠編譯通過(guò)呢挤渔?原來(lái)是編譯器自動(dòng)幫我們實(shí)現(xiàn)了它肮街。

special member functions

C++標(biāo)準(zhǔn)中規(guī)定(參考c++98的標(biāo)準(zhǔn)中Special member functions一節(jié))
如果一個(gè)類(lèi)沒(méi)有顯式地聲明以下四個(gè)函數(shù),編譯器將自動(dòng)生成默認(rèn)版本判导。

  • 構(gòu)造函數(shù)(constructor)
  • 拷貝構(gòu)造函數(shù)(copy constructor)
  • 拷貝賦值操作符(copy assignment)
  • 析構(gòu)函數(shù)(destructor)
    由于Student的實(shí)現(xiàn)中并沒(méi)有顯式的聲明拷貝構(gòu)造函數(shù)嫉父,編譯器將會(huì)自動(dòng)為Student類(lèi)創(chuàng)建一個(gè)拷貝構(gòu)造函數(shù)。而編譯器創(chuàng)建的版本只是單純地將源對(duì)象的每個(gè)非static的成員變量拷貝到目標(biāo)對(duì)象眼刃。對(duì)于Student類(lèi)绕辖,編譯器自動(dòng)生成的拷貝構(gòu)造函數(shù)實(shí)現(xiàn)如下:
Student::Student(const Student & rhs) {
    this->name = rhs.name;
    this->age = rhs.age;
}

可以看到,在編譯器自動(dòng)生成的這個(gè)版本中擂红,rhs.name所指向的這段內(nèi)存的指針被賦給了this->name仪际,在這兩個(gè)對(duì)象的析構(gòu)函數(shù)里,都會(huì)嘗試刪除這段內(nèi)存昵骤,從而導(dǎo)致了double free的內(nèi)存錯(cuò)誤树碱。

copy assignment

    Student tom("Tom", 17);
    Student clone("clone", 10);
    clone = tom;

閱讀上面的代碼,思考在函數(shù)退出時(shí)會(huì)發(fā)生啥变秦?
在這段代碼中成榜,我們通過(guò)copy assignment來(lái)clone了一個(gè)tom。由于Student類(lèi)中沒(méi)有顯式地聲明copy assignment伴栓,編譯器為我們自動(dòng)生成了copy assignment的默認(rèn)實(shí)現(xiàn)伦连。同拷貝構(gòu)造函數(shù)一樣雨饺,這個(gè)函數(shù)也只是簡(jiǎn)單地把源對(duì)象中的每一個(gè)non-static的成員變量拷貝到目標(biāo)對(duì)象钳垮。對(duì)于Student類(lèi),編譯器自動(dòng)生成的copy assignment函數(shù)的如下:

Student & Student::operator = (const Student & rhs) {
    this->name = rhs.name;
    this->age = rhs.age;
    return *this;
}

在執(zhí)行clone = tom時(shí)额港,clone對(duì)象的copy assignment被調(diào)用饺窿。clone對(duì)象里的name指針指向了tom對(duì)象的name所指向的指針。在程序退出時(shí)移斩,保存字符原先clone.name所指向的內(nèi)存不會(huì)被釋放肚医,而tom.name所指向的內(nèi)存卻被釋放了兩次。(一次是在tom的析構(gòu)函數(shù)中向瓷,一次是在clone的析構(gòu)函數(shù)中)

rule of three

上面例子中出現(xiàn)的兩個(gè)內(nèi)存問(wèn)題肠套,都是因?yàn)闆](méi)有遵循role of threeStudent類(lèi)聲明并實(shí)現(xiàn)了析構(gòu)函數(shù)猖任,卻沒(méi)有同時(shí)實(shí)現(xiàn)copy constructorcopy assignment operator你稚。
這三個(gè)成員函數(shù)屬于Special member functions,如果沒(méi)有被顯式的定義或禁止,編譯器會(huì)自動(dòng)創(chuàng)建它們刁赖。如果一個(gè)類(lèi)顯式地定義了其中的一個(gè)函數(shù)搁痛,最可能的原因是這個(gè)類(lèi)牽扯到資源的管理,編譯器自動(dòng)生成的版本滿足不了類(lèi)資源管理的需求宇弛,因此需要重新實(shí)現(xiàn)鸡典。那么另外兩個(gè)函數(shù),也理應(yīng)做相應(yīng)的資源相關(guān)的操作枪芒。
而如果一個(gè)類(lèi)管理的資源不支持copy與共享彻况,就應(yīng)該明確地拒絕,顯式的禁止copy constructorcopy assignment operator病苗,以防止因?qū)ο罂截惗鴰?lái)的資源使用錯(cuò)誤疗垛。

Student的參考實(shí)現(xiàn)1

struct Student {
    Student(const char * theName, int theAge)
        : name(createName(theName))
        , age(theAge) {
        std::cout << "construct Student" << std::endl;
    }
    ~Student() {
        std::cout << "destruct Student" << std::endl;
        if (name != NULL) {
            delete[] name;
            name = NULL;
        }
    }

    Student(const Student & rhs)
        : name(createName(rhs.name))
        , age(rhs.age) {
    }

    Student & operator = (const Student & rhs) {
        if (this->name != NULL) {
            delete [] this->name;
            this->name = NULL;
        }
        if (rhs.name != NULL) {
            this->name = createName(rhs.name);
        }
        this->age = rhs.age;
        return *this;
    }
private:
    static char * createName(const char * theName) {
        if (theName == NULL) {
            return NULL;
        }
        char * ptr = new(std::nothrow) char[strlen(theName) + 1];
        if (ptr == NULL) {
            return NULL;
        }
        strcpy(ptr, theName);
        return ptr;
    }
private:
    char * name;
    int age;
};

Student的參考實(shí)現(xiàn)2

在克隆人技術(shù)實(shí)現(xiàn)之前,copy一個(gè)人沒(méi)有意義硫朦,因此需要顯式地禁止copy constructorcopy assignment operator(將這兩個(gè)函數(shù)聲明為私有而不提供實(shí)現(xiàn))贷腕。

struct Student {
    Student(const char * theName, int theAge)
        : name(createName(theName))
        , age(theAge) {
        std::cout << "construct Student" << std::endl;
    }
    ~Student() {
        std::cout << "destruct Student" << std::endl;
        if (name != NULL) {
            delete[] name;
            name = NULL;
        }
    }

private:
    static char * createName(const char * theName) {
        if (theName == NULL) {
            return NULL;
        }
        char * ptr = new(std::nothrow) char[strlen(theName) + 1];
        if (ptr == NULL) {
            return NULL;
        }
        strcpy(ptr, theName);
        return ptr;
    }
private:
    Student(const Student & rhs);
    Student & operator = (const Student & rhs);
private:
    char * name;
    int age;
};

School的實(shí)現(xiàn)

由于Student類(lèi)禁止了拷貝構(gòu)造函數(shù),而vectorpush_back的時(shí)候需要調(diào)用到對(duì)象的拷貝構(gòu)造函數(shù)咬展,因此先前的代碼會(huì)編譯不過(guò)泽裳,為了解決這個(gè)問(wèn)題,可以在vector中存儲(chǔ)Student的指針破婆。在容器中存儲(chǔ)指針涮总,消除了對(duì)象的構(gòu)造拷貝,提高了運(yùn)行效率祷舀,但引入了另外一個(gè)復(fù)雜度瀑梗,即對(duì)象創(chuàng)建與刪除職責(zé)的管理。

rule of five(C++11)

如果一個(gè)類(lèi)顯式定義了destructor裳扯、copy constructor抛丽、copy assignment operator三個(gè)函數(shù)中的任意一個(gè)函數(shù),編譯器不再自動(dòng)生成move constructormove assignment operator的默認(rèn)實(shí)現(xiàn)饰豺。如果一個(gè)類(lèi)支持移動(dòng)語(yǔ)義(move semantics)亿鲜,需要顯式地聲明并實(shí)現(xiàn)這兩個(gè)函數(shù)。這樣冤吨,對(duì)于支持移動(dòng)語(yǔ)義的類(lèi)蒿柳,這五個(gè)函數(shù)應(yīng)該同時(shí)出現(xiàn)。

參考資料

  • 《effective C++》條款05漩蟆,了解C++默默編寫(xiě)并調(diào)用哪些函數(shù)
  • 《effective C++》條款06垒探,若不想使用編譯器自動(dòng)生成的函數(shù),就該明確拒絕
  • 《C++編程規(guī)范》第52條怠李,一致地進(jìn)行復(fù)制和銷(xiāo)毀

后記

文中例子的運(yùn)行結(jié)果基于mac os + clang圾叼,clang版本信息如下:

  • Apple LLVM version 7.0.2 (clang-700.1.81)
  • Target: x86_64-apple-darwin14.5.0
  • Thread model: posix

感謝M23指正仔引,在Linux + gcc的環(huán)境下,double free的例子中褐奥,析構(gòu)函數(shù)里的log輸出了四次咖耘。在mac os + clang環(huán)境下log輸出了三次并不因?yàn)槲鰳?gòu)函數(shù)應(yīng)該被調(diào)用三次,而是因?yàn)閮?nèi)存異常終止了log的輸出撬码。
至于Linux + gccmac os + clang異常處理上的不同是另外一個(gè)話題儿倒,在此不做討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末呜笑,一起剝皮案震驚了整個(gè)濱河市夫否,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叫胁,老刑警劉巖凰慈,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異驼鹅,居然都是意外死亡微谓,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)输钩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)豺型,“玉大人,你說(shuō)我怎么就攤上這事买乃∫霭保” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵剪验,是天一觀的道長(zhǎng)肴焊。 經(jīng)常有香客問(wèn)我,道長(zhǎng)功戚,這世上最難降的妖魔是什么娶眷? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮疫铜,結(jié)果婚禮上茂浮,老公的妹妹穿的比我還像新娘双谆。我一直安慰自己壳咕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布顽馋。 她就那樣靜靜地躺著谓厘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪寸谜。 梳的紋絲不亂的頭發(fā)上竟稳,一...
    開(kāi)封第一講書(shū)人閱讀 51,231評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼他爸。 笑死聂宾,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诊笤。 我是一名探鬼主播系谐,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼讨跟!你這毒婦竟也來(lái)了纪他?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤晾匠,失蹤者是張志新(化名)和其女友劉穎茶袒,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體凉馆,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡薪寓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了澜共。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片预愤。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖咳胃,靈堂內(nèi)的尸體忽然破棺而出植康,到底是詐尸還是另有隱情,我是刑警寧澤展懈,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布销睁,位于F島的核電站,受9級(jí)特大地震影響存崖,放射性物質(zhì)發(fā)生泄漏冻记。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一来惧、第九天 我趴在偏房一處隱蔽的房頂上張望冗栗。 院中可真熱鬧,春花似錦供搀、人聲如沸隅居。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)胎源。三九已至,卻和暖如春屿脐,著一層夾襖步出監(jiān)牢的瞬間涕蚤,已是汗流浹背宪卿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留万栅,地道東北人佑钾。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像烦粒,于是被迫代替她去往敵國(guó)和親次绘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1. 讓自己習(xí)慣C++ 條款01:視C++為一個(gè)語(yǔ)言聯(lián)邦 為了更好的理解C++撒遣,我們將C++分解為四個(gè)主要次語(yǔ)言:...
    Mr希靈閱讀 2,802評(píng)論 0 13
  • 以下內(nèi)容轉(zhuǎn)自stack overflow 問(wèn)題What is The Rule of Three? Problem...
    chnmagnus閱讀 954評(píng)論 0 0
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,516評(píng)論 1 51
  • 姐姐义黎,您好我是麗妍國(guó)際美容院的美容師禾进。我們公司開(kāi)業(yè)15年在湖南、深圳廉涕、廣州泻云、花都、番禺狐蜕、都有幾十家分店宠纯。因?yàn)樾碌昙?..
    久伴則暖閱讀 244評(píng)論 0 0
  • 心中的北川就在樣走了一趟北川,心中無(wú)限惆悵层释。坐在所謂的長(zhǎng)途汽車(chē)上婆瓜,我的一顆心忐忑不安,我不知道我會(huì)看到的是怎樣的一...
    蕭蕭Ruth閱讀 265評(píng)論 0 0