--- GA: G-RZYLL0RZGV --- ###### tags: `大一程設-下` `東華大學` `東華大學資管系` `基本程式概念` `資管經驗分享` Copy Constructor、Overloading Assignment Operator(上) === [TOC] ## 前言 複製建構子可謂一下最大大魔王,我覺得很會打這個不太重要,但他想傳遞的概念非常重要,是很重要的思維! ## 引導 今天透過宣告類別實體化物件的時候,根據需求我們可能會要複製某個物件的資料。 我們先不管實例,直接先用簡單的類別說明。 ```cpp= class Animal{ public: Animal(){ this->name = ""; } Animal(string n){ this->name = n; } void set_name(string n){ this->name = n; } string get_name(){ cout << &(this->name) << endl; return this->name; } private: string name; }; int main(int argc, char** argv) { Animal a1("bob"); Animal a2 = a1; cout << a1.get_name() << endl; cout << a2.get_name() << endl; a1.set_name("josh"); cout << a1.get_name() << endl; cout << a2.get_name() << endl; return 0; } ```  可以看到對於 a1 跟 a2 而言,name 這個屬性各自隸屬不同的記憶體區塊,畢竟是兩個不同物件的屬性,在第 22 行我們做了一個宣告即 assign 的動作把 a1 的值 copy 一份給 a2,所以你才能看到上圖的輸出。 而因為是複製值,所以 name 沒有占用同一塊記憶體。當第 22 行修改了 a1 的 name 沒有影響到 a2 的 name。 而像第 22 行這樣一個**宣告即 assign 的 copy 動作**,其實就已經觸發了 copy constructor 了。 ## 什麼是 Copy Constructor copy constructor 又稱複製建構子,顧名思義,他是一種建構子,還記得建構子是物件初始化的天生狀態,我們可以透過自定義的建構子來初始化物件,而既然稱為複製建構子,想必有一個想要複製的東西,而又稱建構子,所以是把某個東西複製過來,成為另一個東西天生的狀態。 > 簡單的說,copy constructor 就是想要做到把 A 物件的內容複製一份給 B 物件這樣的動作。 > 而因為 copy constructor 也是一種建構子,所以他其實也是在做物件實體化的動作。 > [name=Orange] <span style="color:red">**Copy Constructor 與類別同名,參數只有一個,必須要是同類別的物件,是一種 function。**</span> C++ 預設每一個類別其實都有一個預設的 copy constructor,如果我們沒定義他,他會自己運作,所以我們可以自己實作他。 而**預設**的 copy constructor 被呼叫的時候,會把資料都複製一份出來。 大概會長像下面這樣 :point_down: ```cpp= class Animal{ public: Animal(); // constructor Animal(string n); // user-defined constructor Animal(const Animal& copyObject); // copy constructor }; ``` ### 語法說明 ```cpp= // 類別名稱(const 類別物件){...} class_name(const class_name& class_object){...} // 範例 Animal(const Animal& copyObject){...} ``` 為何要 call-by reference? * 因為呼叫 function 參數物件若用 call-by value 非常佔記憶體。 * 這個之前已經說過,就不再贅述,忘記去看前面 為何要 const? * 今天是把 A 複製給 B,所以呼叫 copy constructor 的想必是 B,而 A 會作為參數,把東西全部 copy 給 B,那在複製的過程中絕對不希望 A 的內容被修改,所以要 const 來確保安全。 ### 實際運作 ```cpp= // 上方省略 int main(){ 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 } ``` 請看第四行,根據參數,我們知道是一個 Animal 物件,呼叫 Copy Constructor。 第四行跟第五行的寫法,只要你是在宣告的時候這樣寫,會產生一樣的效果,都會呼叫 copy constructor。 但第六行的寫法,如果今天是單純的 assign,並不等於第四行跟第五行,下面都會說到,但你可以先記起來。 倘若今天沒有在類別內撰寫 copy constructor,你的程式若執行 copy 的動作的時候,會有預設的 copy constructor 被執行,詳細可以看 reference。 ## 何時需要親自來定義 Copy Constructor 根據前面的說明,我們已經知道 Copy Constructor 用來複製內容,那既然預設的已經會幫我們做複製了,我們還有必要來覆寫(親自定義)他嗎? 答案是有的,我們先來說總結。 * <span style="color:red">**當今天 class 的屬性有指標絕對需要 copy constructor**</span> * 因為預設的複製建構子進行複製的時候,僅複製指標變數的儲存的值(位址),而不是儲存其位址所指向的值。 **所以會造成刪除同一塊記憶體的情況。** 想必無法理解,在真的講到上面的情況前,我們先看一個小例子,再回頭講這個比較難的。 ```cpp= class Animal{ public: Animal(){ this->age = NULL; } Animal(int a){ this->age = new int; *(this->age) = a; } 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); } private: int* age; }; int main(int argc, char** argv) { Animal a1(20); Animal 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; } ``` 上上個例子我們示範了非指標的變數複製的結果,會發現名字的改變並不會相互影響,但如果今天變數換成指標變數的時候,結果就完全大不相同了哦,我們看一下輸出。  > <span style="color:red">如果覺得圖片不清楚,可以右鍵圖片「在新分頁中開啟圖片」看完整大小。</span> 按照上上個例子的做法,不是應該年齡也不會互相影響嗎? 怎麼我改 a1 的 age 結果兩個人的 age 都變成 25 了? 這就跟指標變數本身的特性有關,「指標變數存的是位址」,在做`Animal a2 = a1;`的時候,我們**複製的是值**,因此我們是把上圖的 `0xbd1530` 這個位址丟進去 a2 的指標變數 age 裡面,所以現在兩個物件的 age 都指向同一個地方,進而我只要修改一個人的 age 最後指向的地方的值,另一個人就會跟著被改變。 這是非常糟糕的,也就是因為這個緣故,如果類別的屬性有指標變數,我們就必須親手撰寫 copy constructor,因為預設的 copy constructor 會發生上面這樣的情況。 ### 親手撰寫 copy constructor 為了避免上面的情況,我們親自來撰寫 copy constructor 來處理這個問題吧! 首先我們知道 Animal 的 age 屬性是指標變數,所以我們必須要給一塊記憶體放到這個指標變數裡面(不是指標變數本身的記憶體哦,是他要指向的那個整數所在的地方),但這個記憶體要從哪來呢? 目前並沒有配置的記憶體阿,所以我們要自己動態的配置出來。 先貼上完整的程式碼,細部的說明就往下看吧。 ```cpp= class Animal{ public: Animal(){ 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) = a; } int get_age(){ cout << "address of age: " << &(this->age) << ", "; cout << "value of age: " << this->age << ", "; return *(this->age); } private: int* age; }; int main(int argc, char** argv) { // main 跟上個例子一模一樣,自己去上面複製 } ``` 細部說一下每個 function。 **user-defined constructor:** ```cpp= Animal(int a){ // user-defined constructor this->age = new int; *(this->age) = a; } ``` 如同上面說的,因為 age 現在是指標變數,因此我們需要分配一塊記憶體給他來存(切記不是 age 變數本身的記憶體位址!!),我們 new 出來的是紅色的這塊。要先 new 出記憶體,才能把參數 a 的數字拿來存。  **copy constructor:** ```cpp= Animal(const Animal& a){ this->age = new int; *(this->age)= *(a.age); } ``` 這樣當 a2 被 copy constructor 實體化的時候,會把 a1 當作參數丟進來,因為實體化的時候 age 並沒有被指派記憶體,因此我們 new 一個給他,最後再把值賦予 a2 的 age。 ## Destructor Destructor,又稱解構子,負責來把物件的記憶體給回收,跟動態陣列的思維很像,你必須要來把你手動配置的記憶體給回收掉,如果你類別今天都沒有 new 出來的東西,可以不撰寫沒關係。 但很明顯的,上例有一個指標變數,我們也有親自 new 記憶體出來,接下來就來介紹一下解構子吧。 ### Destructor 語法 解構子與類別同名,要在最前面加上 `~`,且沒有參數,不用呼叫他,系統會根據物件的**存活週期的最後**來呼叫執行裡面的內容,釋放掉記憶體。 ```cpp= ~類別名稱(){ // 回收你動態配出來的東西 } ~Animal(){ delete this->age; // 這邊的例子要把 age 給回收掉。 } ``` 以我們上面寫的例子來說,Animal 物件都是在 main 裡面,所以他們的存活週期就是 main 要結束之前,才會呼叫 destructor,至於其他的存活週期以前已經教過摟。 記憶體的回收在 C++ 內一直都是很重要的,雖然大多新的語言不用我們去擔心這些,但是保有這邊理論的思維帶去開發,你會變得更謹慎,更成熟穩重。 但 Destructor 如果因為寫程式的不小心,其實會引發刪掉同樣一塊記憶體的問題,我們放到下一篇筆記的最後跟大家說。 ## 小結 接下來要來講與 copy constructor 一定要搭配在一起的東西 - Assignment Operator Overloading。 這邊的思維是非常重要的,可能要花點心思理解。 ## Reference * [複製建構子 - 維基百科](https://zh.wikipedia.org/wiki/%E8%A4%87%E8%A3%BD%E5%BB%BA%E6%A7%8B%E5%AD%90) * [C++ Copy Constructor (拷貝建構函式,複製建構函式)](https://www.itread01.com/content/1548766986.html)
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up