Try   HackMD
tags: 大一程設-下 東華大學 東華大學資管系 基本程式概念 資管經驗分享

Copy Constructor、Overloading Assignment Operator(中)

Overloading Assignment Operator

Assignment Operator (指派運算子,= )

還記得在上篇,一開始我們有講下面這個東西。

Animal a1("bob"); Animal a2(a1); // line 4 and line 5 are the same Animal a2 = a1; a2 = a1; // line 6 not equal to line 4 and line 5

前三行都已經被說完了,你可能會說,但明明第四行的寫法,平常在寫的時候也是把值 assign 過去阿,他跟第二行還有第三行差在哪裡?

在 C++ 底層就已經寫好第二行跟第三行會產生等價的效果,都會呼叫 copy constructor,然而第四行這樣的寫法並沒有,所以我們先來試試看,如果今天直接寫 a2 = a1 會發甚麼事。

一樣我們看兩種例子,一種是類別內沒有指標變數的屬性,跟有指標變數的屬性,結果有甚麼不同。

class Animal{ public: Animal(){ this->name = ""; } void set_name(string n){ this->name = n; } string get_name(){ cout << &(this->name) << endl; return this->name; } Animal(string n){ this->name = n; } private: string name; }; int main(){ Animal a1("bob"); // call user-defined constructor Animal a2; // call default constructor a2 = a1; // 其餘 main 皆相同,自己對照之前的例子複製 }

輸出結果如下 :

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

你會發現結果跟原本一模一樣,好像沒有不同阿。但程式碼的差異是差非常多的。

// declaration and initialization Animal a1("bob"); Animal a2 = a1; // or Animal a2(a1);

這樣寫的意思是,a1 呼叫自定義的建構子,a2 呼叫 copy constructor,把 a1 當作參數丟進來,根據函式內容進行實體化。

Animal a1("bob"); // call user-defined constructor Animal a2; // call default constructor a2 = a1; // assign

上面這樣的寫法是,a2 呼叫預設建構子,接著把 a1 的內容複製一份「值」給 a2,所以你可以發現為甚麼上面那個例子可以產生跟 copy constructor 一樣的結果,因為我們是複製值出去,採用的記憶體仍舊是不同塊的。

但同樣的情況如果套用到有指標變數屬性的類別裡面,結果會長怎麼樣呢?

class Animal{ public: Animal(){ // default constructor this->age = NULL; } Animal(int a){ // user-defined constructor this->age = new int; *(this->age) = a; } Animal(const Animal& a){ // copy constructor this->age = new int; *(this->age)= *(a.age); } void set_age(int a){ this->age = new int; *(this->age) = a; } int get_age(){ cout << "address of age: " << &(this->age) << ", "; cout << "value of age: " << this->age << ", "; return *(this->age); } ~Animal(){ // destructor cout << "Destructor:" << endl; cout << &(this->age) << endl; cout << this->age << endl; cout << *(this->age) << endl; cout << "-----------" << endl; delete this->age; } private: int* age; }; int main(){ Animal a1(20); Animal a2; a2 = a1; // 其餘相同 }

結果如下 :

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

你可以發現 a1 跟 a2 的指標變數 age 存的記憶體是同一塊,如同前面所說的,a2 = a1 是複製一份「值」出來,所以 a1 是複製指標變數 age 存的位址給 a2,結果兩個人運用到同樣一塊記憶體空間。

這樣的結果跟我們還沒有親手寫 copy constructor 時一樣。

再往下看,根據 Destructor 的呼叫順序,可以知道是 a2 先被刪除,而因為 0xcb1510 這塊記憶體是被 new 出來的,a2 的解構子就會先把這塊記憶體回收,所以當輪到 a1 執行 destructor 的時候,會發現最後想刪卻刪不到東西,因此出現亂碼。

也因為這樣,最後下面寫 return value 3221226356,代表他發生了錯誤,這篇筆記的最後會介紹這個錯誤。

所以問題回到指標變數上的時候,又發生了一樣的問題,「操弄同樣一塊記憶體」,因此我們要來解決這樣的問題。我們必須親手來撰寫 Assignment Operator Overloading。

親手撰寫 Assignment Operator Overloading

還記得 Operator Overloading 會根據是幾元運算子而有幾個參數,所以很簡單,像下面這樣。(忘記的這邊支援你,請多注意有沒有加 friend 的不同)

Operator Overloading 能夠讓我們自定義運算子的行為,所以這邊要做的事情就是,自定義 = 運算子,讓他在複製的時候,不要讓 a1 跟 a2 的指標變數去操弄同樣的記憶體。

// 語法公式: return_type operator operator_you_want_overloading(arguments); // EX: void operator = (Animal& right_side){ // statements }

實際寫起來會像這樣:

// when call a2 = a1 void operator = (Animal& right_side){ this->age = new int; *(this->age) = *(right_side.age); }

當我們自定義 = 的行為後,a2 = a1 會是 a2 來呼叫這個 function,而 a1 會被當作參數傳進來。這樣寫就能避免掉操弄同樣記憶體的情況了。

那這邊你可能會問,不是說 operator overloading 是 overloading 幾元運算子就會有幾個參數嗎,這邊的 = 是二元運算子,怎麼只有一個參數?

跟我們在 overloading 的時候是不是 friend 有關,忘記了你只好回去看前面的筆記摟
Orange

完整的類別如下,歡迎複製下來跑跑看 :

class Animal{ public: Animal(){ // default constructor this->age = NULL; } Animal(int a){ // default constructor this->age = new int; *(this->age) = a; } Animal(const Animal& a){ // copy constructor this->age = new int; *(this->age)= *(a.age); } void operator = (Animal& right_side){ // operator overloading this->age = new int; *(this->age) = *(right_side.age); } void set_age(int a){ *(this->age) = a; } int get_age(){ cout << "address of age: " << &(this->age) << ", "; cout << "value of age: " << this->age << ", "; return *(this->age); } ~Animal(){ cout << "Destructor:" << endl; cout << &(this->age) << endl; cout << this->age << endl; cout << *(this->age) << endl; cout << "-----------" << endl; delete this->age; } private: int* age; }; int main(int argc, char** argv) { Animal a1(20); Animal a2; a2 = a1; cout << "A1s'" << endl; cout << a1.get_age() << endl; cout << "-------------" << endl; cout << "A2s'" << endl; cout << a2.get_age() << endl; cout << "-------------" << endl; a1.set_age(25); cout << "A1's age:" << endl; cout << a1.get_age() << endl; cout << "A2's age:" << endl; cout << a2.get_age() << endl; return 0; }

解釋一下

void operator = (Animal& right_side){ // operator overloading this->age = new int; *(this->age) = *(right_side.age); }

因為知道我們想要讓 = 能夠有跟 copy constructor 一樣的行為,所以你會發現撰寫的內容跟 copy constructor 一樣。至於更詳細的緣由你可以繼續往下看。

多看一點

我想只有一個等號說明可能不清楚,我們試試看 overloading 其他的 operator。

overloading 二元的 + 運算子

int operator + (Animal& right_side){ return *(this->age) + right_side.get_age(); } // when call a1+a2 // we will trigger this function, and plus age of two animals

還可以 oveloading 非常多的運算子,剩下的就交給大家自己去摸索了。

Big Three 三法則

Big Three:

  • copy constructor
  • assignment operator overloading
  • destructor

稍微總結一下 copy constructor 跟 Assignment Operator Overloading,可以發現都是為了解決類別內的指標變數屬性造成的操弄記憶體相同之問題,而我們常寫的 Assign = 也會產生這樣的問題,因此 copy constructor 跟 Assignment Operator Overloading 都必須要在類別定義內同時被撰寫。

也因為需要動態的來配置記憶體,針對物件的記憶體回收變得更為重要,因此需要撰寫 Destructor。

上述提到的這三項東西就是 C++ 內俗稱的 Big Three。

所以看到這你可以了解,這三項東西,一旦要撰寫,就必須要全部都撰寫,缺一不可。雖然這三項東西如果不寫都有預設的,但透過上面的講解已經可以知道預設的會產生的行為不符合我們的需求,所以必須要自己來定義函式的內容。

[補充(Optional)]你該知道的 error 訊息

接下來來跟大家討論一個錯誤訊息,針對這個錯誤雖然你的程式可能可以跑(多數是 run 不起來的),但結果通常是錯誤的,而且是非常不安全的。

The return value 3221225477 in hex is 0xC0000005 or STATUS_ACCESS_VIOLATION
STATUS_ACCESS_VIOLATION : 讀取或寫入無法存取的記憶體位置。

return value 3221226356,也是針對操弄記憶體的錯誤而產生的錯誤。

所以你在操弄指標變數的時候,其實必須非常的小心,那以講到這邊來看,你在何處需要特別注意呢?

  • 是否需要 new 記憶體給指標變數,會不會操弄到同一塊
  • delete 有沒有可能刪到同樣一塊記憶體

針對刪到同樣一塊記憶體這件事,我們來看看下面這個例子。

這個例子我們假設你的類別裡有指標變數,而且你少寫了 = 的 operator overloading,卻還是做了 a2 = a1 這件事。

class Animal{ public: Animal(){ this->age = NULL; } void set_age(int a){ *(this->age) = a; } int get_age(){ cout << "address of age: " << &(this->age) << ", "; cout << "value of age: " << this->age << ", "; return *(this->age); } Animal(int a){ this->age = new int; *(this->age) = a; } Animal(const Animal& a){ this->age = new int; *(this->age)= *(a.age); } ~Animal(){ cout << "Destructor:" << endl; cout << &(this->age) << endl; cout << this->age << endl; cout << *(this->age) << endl; cout << "-----------" << endl; delete this->age; } // void operator = (Animal& right_side){ //// this->age = new int; // *(this->age) = *(right_side.age); // } private: int* age; }; int main(int argc, char** argv) { Animal a1(20); Animal a2; a2 = a1; cout << "A1s'" << endl; cout << a1.get_age() << endl; cout << "-------------" << endl; cout << "A2s'" << endl; cout << a2.get_age() << endl; cout << "-------------" << endl; a1.set_age(25); cout << "A1's age:" << endl; cout << a1.get_age() << endl; cout << "A2's age:" << endl; cout << a2.get_age() << endl; cout << "-------------" << endl; return 0; }

根據前面的講解,你已經可以知道 a2 = a1 只會複製值,所以現在兩個指標變數會操弄同樣一塊記憶體,我們來看看輸出會產生甚麼東西。

你可以看到,根據輸出 destructor 的呼叫順序是 a2 再 a1,所以 a1 刪不到東西了,因為 a2 已經把那個 new 的內容給刪掉了,他操弄到了一塊空的記憶體,因此出現了下面的 1645072 這個亂碼,也因此產生了最下面的 return value 3221226356

所以總的來說,針對 new 出來的記憶體,你需要很小心,並且知道是誰在操縱他。

小結

這篇筆記準備的例子只是可能發生的情況,實際還是根據你們的需求去撰寫相對應的程式碼,但是思維跟做法都跑不掉這篇筆記要表達的。

Reference

https://stackoverflow.com/questions/5973427/error-passing-xxx-as-this-argument-of-xxx-discards-qualifiers