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

Copy Constructor、Overloading Assignment Operator(上)

前言

複製建構子可謂一下最大大魔王,我覺得很會打這個不太重要,但他想傳遞的概念非常重要,是很重要的思維!

引導

今天透過宣告類別實體化物件的時候,根據需求我們可能會要複製某個物件的資料。
我們先不管實例,直接先用簡單的類別說明。

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; }

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 而言,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 也是一種建構子,所以他其實也是在做物件實體化的動作。
Orange

Copy Constructor 與類別同名,參數只有一個,必須要是同類別的物件,是一種 function。

C++ 預設每一個類別其實都有一個預設的 copy constructor,如果我們沒定義他,他會自己運作,所以我們可以自己實作他。

預設的 copy constructor 被呼叫的時候,會把資料都複製一份出來。

大概會長像下面這樣

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 →

class Animal{ public: Animal(); // constructor Animal(string n); // user-defined constructor Animal(const Animal& copyObject); // copy constructor };

語法說明

// 類別名稱(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 來確保安全。

實際運作

// 上方省略 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 用來複製內容,那既然預設的已經會幫我們做複製了,我們還有必要來覆寫(親自定義)他嗎?

答案是有的,我們先來說總結。

  • 當今天 class 的屬性有指標絕對需要 copy constructor
    • 因為預設的複製建構子進行複製的時候,僅複製指標變數的儲存的值(位址),而不是儲存其位址所指向的值。

所以會造成刪除同一塊記憶體的情況。

想必無法理解,在真的講到上面的情況前,我們先看一個小例子,再回頭講這個比較難的。

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; }

上上個例子我們示範了非指標的變數複製的結果,會發現名字的改變並不會相互影響,但如果今天變數換成指標變數的時候,結果就完全大不相同了哦,我們看一下輸出。

如果覺得圖片不清楚,可以右鍵圖片「在新分頁中開啟圖片」看完整大小。

按照上上個例子的做法,不是應該年齡也不會互相影響嗎? 怎麼我改 a1 的 age 結果兩個人的 age 都變成 25 了?

這就跟指標變數本身的特性有關,「指標變數存的是位址」,在做Animal a2 = a1;的時候,我們複製的是值,因此我們是把上圖的 0xbd1530 這個位址丟進去 a2 的指標變數 age 裡面,所以現在兩個物件的 age 都指向同一個地方,進而我只要修改一個人的 age 最後指向的地方的值,另一個人就會跟著被改變。

這是非常糟糕的,也就是因為這個緣故,如果類別的屬性有指標變數,我們就必須親手撰寫 copy constructor,因為預設的 copy constructor 會發生上面這樣的情況。

親手撰寫 copy constructor

為了避免上面的情況,我們親自來撰寫 copy constructor 來處理這個問題吧!

首先我們知道 Animal 的 age 屬性是指標變數,所以我們必須要給一塊記憶體放到這個指標變數裡面(不是指標變數本身的記憶體哦,是他要指向的那個整數所在的地方),但這個記憶體要從哪來呢? 目前並沒有配置的記憶體阿,所以我們要自己動態的配置出來。

先貼上完整的程式碼,細部的說明就往下看吧。

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:

Animal(int a){ // user-defined constructor this->age = new int; *(this->age) = a; }

如同上面說的,因為 age 現在是指標變數,因此我們需要分配一塊記憶體給他來存(切記不是 age 變數本身的記憶體位址!!),我們 new 出來的是紅色的這塊。要先 new 出記憶體,才能把參數 a 的數字拿來存。

copy constructor:

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 語法

解構子與類別同名,要在最前面加上 ~,且沒有參數,不用呼叫他,系統會根據物件的存活週期的最後來呼叫執行裡面的內容,釋放掉記憶體。

~類別名稱(){ // 回收你動態配出來的東西 } ~Animal(){ delete this->age; // 這邊的例子要把 age 給回收掉。 }

以我們上面寫的例子來說,Animal 物件都是在 main 裡面,所以他們的存活週期就是 main 要結束之前,才會呼叫 destructor,至於其他的存活週期以前已經教過摟。

記憶體的回收在 C++ 內一直都是很重要的,雖然大多新的語言不用我們去擔心這些,但是保有這邊理論的思維帶去開發,你會變得更謹慎,更成熟穩重。

但 Destructor 如果因為寫程式的不小心,其實會引發刪掉同樣一塊記憶體的問題,我們放到下一篇筆記的最後跟大家說。

小結

接下來要來講與 copy constructor 一定要搭配在一起的東西 - Assignment Operator Overloading。

這邊的思維是非常重要的,可能要花點心思理解。

Reference