這是一篇對什么是C++的The Rule of Three的錯誤更正和詳細說明。
閱讀時間7分鐘。難度???

雖然上一篇文章的閱讀量只有凄慘的兩位數,但是懷著對小伙伴負責的目的,必須保證代碼的正確性。這是大廚做技術自媒體的態度。
前文最后一段代碼是這樣的:
class?Dog?{
?private:
???char* name;
???int?age;
?public:
???'...省略構造和拷貝構造函數...'
????//拷貝賦值函數
???Dog& operator=(const?Dog& that) {
?????name = new?char[strlen(that.name)+1];
?????strcpy(name, that.name);
?????age = that.age;
???}
???'...省略析構函數...'
};
先不談異常安全,這段拷貝賦值函數的代碼本身有什么問題?
有3個問題:
沒有釋放原對象指針成員指向的內容
沒有返回值
沒有自賦值檢查
下面我們一個一個分析。
?1???沒有釋放原對象指針
這個問題很嚴重,因為一定會造成內存泄露。
原因是指針所指的內存未被釋放,而指針又指向了別處。
例子如下,我們寫了一個main函數,長這樣:
int?main(int?argc, char* argv[])?{
????Dog D1("Bobby",?2);
????Dog D2("Teddy",?3);
????Dog D2 = D1;
}
D1和D2分別是是Dog的對象。根據構造函數的定義,D2中的name指針指向了字符數組“Teddy”。而當進行D2 = D1操作時,name =?new?char[strlen(that.name)+1]這一步會在D2中重新創建一個名字為name且指向“Bobby”的指針。
這么做也許編譯器不會報錯,但是會有問題。
因為在new一個name指針之前,原本的name指針指向的內存并沒有被釋放。而新的name指針只對新創建的內存負責,老的內存已經變成無主之地。看來內存泄露是逃不掉了。
這個問題看著復雜,解決的辦法倒是簡單,只需要在拷貝賦值函數體第一行加上 delete[] name就可以了。
class?Dog?{
?private:
???char* name;
???int?age;
?public:
???'...省略構造和拷貝構造函數...'
????//拷貝賦值函數
???Dog& operator=(const?Dog& that) {
?????delete[] name; //釋放原對象指針成員指向的內容
?????name = new?char[strlen(that.name)+1];
?????strcpy(name, that.name);
?????age = that.age;
???}
???'...省略析構函數...'
};
2???沒有返回值第二個問題犯的錯很低級,拷貝賦值函數的行為和普通函數一樣需要一個返回值。而返回值的類型通常是類的對象的引用。
參照常用的寫法,這里返回*this(this是C++類的隱藏成員,表示對象本身)。
class?Dog?{
?private:
???char* name;
???int?age;
?public:
???'...省略構造和拷貝構造函數...'
????//拷貝賦值函數
???Dog& operator=(const?Dog& that) {
?????delete[] name; //釋放原對象指針成員指向的內容
?????name = new?char[strlen(that.name)+1];
?????strcpy(name, that.name);
?????age = that.age;
?????return?*this; //返回對象引用
???}
???'...省略析構函數...'
};
另外大家可能有疑問為什么返回值是一個引用而不是一個值呢?
答案是只有引用才能進行連續賦值。
假設有3個Dog對象:D1、D2、D3,如果返回值不是引用,那么類似D1 = D2 = D3將不能通過編譯。
?3???沒有自賦值檢查
什么叫做自賦值?
就是兩個相同對象之間用等號連接,比如:
int?main(int?argc, char* argv[])?{
????Dog D1("Bobby", 2);
????Dog D1 = D1; //同一個D1相互賦值
}
當然,一般不會有人寫出這樣的代碼來。這里只是舉個簡單的例子,但是如果在大型項目中不同開發者對同一對象取了不同的別名,那么自賦值的情況是有可能發生的。
對于上面的Dog類而言,如果執行D1 = D1,那么會發生下面的事情:
首先,對象D1中的name指針被析構,name指向的內存被釋放;
然后,下一行中的strlen(that.name)又用到了D1的name所指向的內存。
重點來了:這時你會驚訝地發現編譯器提示你name已經不存在了!!!
因為在編譯器看來,你在做對同一對象先釋放了內存再使用的非法事情!
就好比你是拆遷大隊的,你沒有確認拆的是不是自己的房子就不管三七二十一直接拆了,然而你晚上還要回家住......

C++真的燒腦,僅僅是不小心把自己賦值給了自己就把自己的一部分給搞丟了,這在其他語言中似乎是天方夜譚。但是C++似乎很情愿把事情搞復雜。
幸好,自賦值問題也很容易修復,只需要在delete指針之前做一個自賦值的判斷。
完整代碼如下:
class?Dog?{
?private:
???char* name;
???int?age;
?public:
???'...省略構造和拷貝構造函數...'
????//拷貝賦值函數
???Dog& operator=(const?Dog& that) {
?????if(this?!= &that) { //判斷是否自賦值
?????????delete[] name; //釋放原對象的指針指向的內容
?????????name = new?char[strlen(that.name)+1];
?????????strcpy(name, that.name);
?????????age = that.age;
?????}
?????return?*this;
???}
???'...省略析構函數...'
};
this?!= &that這個判斷的寫法看上去莫名其妙,大廚來給大家分析一下:
this代表D1=D1中等號左邊的D1,&that代表等號右邊的D1的引用(本質上還是D1)。this和&that二者如果相等就說明是同一個對象,那么拷貝賦值函數就直接返回對象的引用。
至此,三個問題終于都解決了
?4???總結時刻
通過以上問題的剖析可以發現,C++一大半奇奇怪怪行為的背后都有一個處理不當的指針。
另外,寫一個正確的類真的一點都不簡單,需要考慮內存泄露,返回值類型,自賦值等等情況。
打住,再說下去大廚真的轉行成C++專業勸退師了。