--- 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
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.