--- title: '類別 & 動態記憶體 & 拷貝' disqus: kyleAlien --- 類別 & 動態記憶體 & 拷貝 === ## OverView of Content 如有引用參考請詳註出處,感謝 :smile: > 在類別中配置動態記憶體,要十分注意記憶體配置釋放,以及它載入時使用的順序 [TOC] ## 類別 - 靜態成員 ### 靜態元素 - static * static 成員特性:static 是 **[指示字](https://hackmd.io/uf-tHfltRVCV45D1h6ZiGA#指示字、修飾字)**,**不管產生多少的物件,每個物件都是使用同一個靜態成員** * **==不能在宣告中定義 static==**,會編譯期間出錯,這樣其實滿合理的,否則每 include 一次就會導致 static 變數重算 (guess) ```cpp= // 抽象 Class #ifndef CLASS___NEW_STRINGBAD_H_ #define CLASS___NEW_STRINGBAD_H_ class stringbad { private: static int objectCount; // static int objectCount = 0; // Error public: stringbad(); virtual ~stringbad(); }; #endif /* CLASS___NEW_STRINGBAD_H_ */ // ----------------------------------------------------------- // 定義 Class #include "stringbad.h" int stringbad::objectCount = 0; ``` > 靜態元素定義在 Class 中會編譯失敗 > > ![](https://i.imgur.com/KqhgJEX.png) :::warning * class 內它只能存在於宣告中,**在==定義中必須移除 static==不能使用指示字**,否則會在編譯期出錯 > ![](https://i.imgur.com/2mxmvVH.png) ::: ### 常量元素 - static & const * **const 可以讓 static 變量定義在 class 中**,但因為 **const 只能定義一次不能更改**,針對需求做出不同的使用方案 ```cpp= #ifndef CLASS___NEW_STRINGBAD_H_ #define CLASS___NEW_STRINGBAD_H_ class stringbad { ... const static int constant = 20; public: stringbad(); virtual ~stringbad(); }; #endif /* CLASS___NEW_STRINGBAD_H_ */ ``` ## 類別 & 動態記憶體 動態類別申請的記憶體在 **堆** 中,它並不會自動釋放,所以要 **配合類別的 ++析構函數++ 使用** ```cpp= // Class 抽象宣告 #ifndef CLASS___NEW_STRINGBAD_H_ #define CLASS___NEW_STRINGBAD_H_ #include <iostream> #include <cstring> using namespace std; class stringbad { char *str; // 字串內容指標 int strLen; // 子串長度 static int objectCount; const static int constant = 20; public: stringbad(); stringbad(char* str); virtual ~stringbad(); friend ostream& operator << (ostream& o, const stringbad& str); }; #endif /* CLASS___NEW_STRINGBAD_H_ */ // ------------------------------------------------------------------ // Class 定義 #include "stringbad.h" int stringbad::objectCount = 0; // 定義靜態成員初始化 (不能使用 static) stringbad::stringbad() : stringbad((char*)"C++") { // 呼叫指定建構函數 cout << "default construct" << endl; } stringbad::stringbad(char* str) { cout << "setting construct: " << str << endl; this->strLen = strlen(str); // 使用動態創建 new char[] (就是 new[]),+ 1 是為了加入字串的最後一碼 '\0' this->str = new char[this->strLen + 1]; strcpy(this->str, str); this->str[strLen] = '\0'; objectCount++; } stringbad::~stringbad() { cout << "deconstruct: " << str << endl; // 對等使用,在建構函數中建立 new[],在析構函數內 delete[] delete[] str; objectCount--; } ostream& operator << (ostream& o, const stringbad& str) { o << "-----> " << str.str << endl; return o; } int main() { return 0; } ``` ### 函數引數 & 動態規劃 - 複製物件 * 這裡要結合之前 [**函數引數**](https://hackmd.io/7Csy-1LVTouQFl2e2sBvtA#%E5%87%BD%E6%95%B8%E5%BC%95%E6%95%B8) 概念 & 新的動態規劃的概念一起思考 ```cpp= void callMethod_Ref(stringbad & s) { // 使用原物件 cout << "In Reference: " << s << endl; } void callMethod_Val(stringbad s) { //"2. " 會產生形式變數 cout << "In Value: " << s << endl; } int main() { // create 3 object of stringbad stringbad st1((char*)"Hey Pan, how's going on?"); stringbad st2((char*)"Today is Saturday"); stringbad st3((char*)"I read book at friend's coffee shop for prepare find new job"); cout << "\n" << st1; operator<<(cout, st2); cout << st3 << endl; // call reference function callMethod_Ref(st1); //"1. " cout << "After call reference function: " << st1 << endl; // call value function callMethod_Val(st2); //"3. " cout << "After call value function: " << st2 << endl; cout << "Implicit copy" << endl; stringbad st4 = st3; // "4. " cout << st3 << endl;; cout << st4 << endl; return 0; } ``` 1. 使用 reference 由於是使用原本的物件,不會導致析構 > ![](https://i.imgur.com/1bRTWvj.png) 2. **產生暫時物件**:**暫時物件會複製原來物件 str 的成員函數,但++不包括 static 成員++**,而 **暫時物件的產生,又是透過==複製建構函數== 建構出來** :::info 複製建構函數之後說明 ::: 3. 在 `2` 中建構的暫時物件,會在 function 結束後自動釋放,而 **==它的釋放又會呼叫到 st2 的析構函數==,++導致 st2 new[] 出的空間被刪除++,在後面要求要顯示該物件的內容錯誤,無法正常顯示** (空字串) > ![](https://i.imgur.com/4pfb1Nt.png) 4. 這個方式又是另一個呼叫複製建構函數的方式 :::info 複製建構函數不使用一般的 construct ::: > ![](https://i.imgur.com/RTdAUmX.png) ### GPF (General Protection Fault) * GPF 代表,當程式 **試圖想要存取不屬於它的記憶體空見範圍的資料** > 上面範例可看到,**delete[] 超過的物件原本的數量**,有些變異程式 or 作業系統會**在 -1 之前阻止該程式繼續運行** ### 建構函數中 new - 注意事項 * 在建構函數中使用 new,就必須在解構函數中使用 delete (對稱使用);像是 `new` & `delete`、`new[]` & `delete[]` 必須 **成對使用** :::info **delete 可操作在空指標上**,不會異常 ::: ```cpp= class P { private: char * str; public: P(); ~P(); }; P::P() { str = nullptr; //C++11 使用 // str = 0; 同上 } P::~P() { delete[] str; // ok, delete 可操作在空指標上 } int main() { P p; return 0; } ``` * **讓一個物件初始化另一個物件時必須要==覆寫建構函數==**,並做深層拷貝 (下面會詳細說) * **讓一個==暫時物件==(轉換函數) 初始化另一個物件時必須要 ==覆寫建構函數==**,並做深層拷貝 (下面會詳細說) ## 特殊成員函數 若程式設計師沒有去定義以下成員函數,**C++ 將會 ==自動提供==** :::success 1. 預設 **建構函數** -> `Object()` 2. **複製建構函數** -> `Object(const Object &)` 3. **指定運算子** -> `operater =` (等號) 4. **解構函數** -> `~Object()` 5. **位置運算子** -> `this` (每個 class 自帶) ::: ### 預設建構函數 * 無引數 & 不做任何事情,當然也可以去自己定義,但是 **當有預設函數時,兩者不能相互存在** :::info 預設建構函數的存在是因為 **物件都需要建構函數** ::: ```cpp= stringbad::stringbad() { // 預設建構函數 } stringbad::stringbad(int len = 0) { // 預設建構函數 + 預設函數,在函數簽名解釋起來會相互衝突的 } ``` ### 複製建構函數 複製建構函數,原型如下,**默認會複製成員變數** ```cpp= class stringbad { ... public: ... // 固定格式 stringbad(const stringbad &); } // ------------------------------------------------------------------- // 實現 stringbad::stringbad(const stringbad &s) { this->str = s.str; this->strLen = s.strLen; cout << "copy construct: " << this->str << endl; } ``` :::info 複製函數又做了哪些事情 ? 複製了成員變量,**但 ==靜態成員不複製==** (畢竟它是共用的),這種行為又稱為 **==淺拷貝==** ::: * 複製的建構函數,它的觸發方式 ==++**分為 2 個,觸發關鍵**++==,隱式、顯式 1. **顯式觸發**:直接賦予值,最常見以下幾種狀況,**將新物件透過 ++已存在物件++ 初始化,++不會產生暫時物件++** ```cpp= void explicitCopy() { stringbad st1((char*) "Hello World"); cout << "\nst2(st1), "; stringbad st2(st1); cout << "\nst3 = st1, "; stringbad st3 = st1; cout << "\nst4 = stringbad(st1), "; stringbad st4 = stringbad(st1); cout << "\n*st5 = new stringbad(st1), "; stringbad *st5 = new stringbad(st1); // 動態 } ``` > ![](https://i.imgur.com/vYKRzlH.png) 2. **隱式觸發**:Function 呼叫,在 Function 產生新物件,並要返回時,**會 ++產生暫時物件++** + 複製物件的行為 ```cpp= const stringbad testObject() { stringbad s("123 站著穿"); return s; // 產生暫時物件! 返回 } int main() { cout << "Main Start" << endl; testObject(); cout << "Main finish" << endl; return 0; } ``` > ![](https://i.imgur.com/9koBHR7.png) :::warning * 返回時,物件複製到哪裡,又在何時被解構 ? ^1.^ 返回前呼叫複製建構函數,將資料到某個記憶體位置、^2.^ 解構暫時物件、^3.^ 回來原來函數呼叫的地方,到指定記憶體為只取值 ::: * 以下使用複寫++複製建構函數++,**觀察 ==複製建構函數何時會被呼叫==** ```cpp= /* * 宣告 */ #include <iostream> class stringbad { ... public: ... stringbad(const stringbad & s); // 新增建構函數宣告 ... }; /** * 定義複製建構函數 */ stringbad::stringbad(const stringbad & s) { std::cout << ++objCount << "------call copy construct" << std::endl; } // ---------------------------------------------------------------------- /** * 客戶端 使用 */ #include "stringbad.h" using std::cout; using std::endl; void callMethod_Ref(stringbad & s) { cout << "In Reference:" << s << endl; } void callMethod_Val(stringbad s) { cout << "In Value:" << s << endl; } int main() { stringbad st1; // 呼叫建構函數 cout << st1 << ", addr: " << &st1 << "\n"; st1 = "Hello World 123"; // "1. " 建立暫時物件 cout << st1 << ", addr: " << &st1 << "\n"; stringbad st2("!!!AAAA"); // 呼叫建構函數 cout << st2 << ", addr: " << &st2 << "\n"; stringbad st3 = st2; // "2. " 呼叫建構函數 cout << st3 << ", addr: " << &st3 << "\n"; callMethod_Val("HHRRRDDD"); // "3. " stringbad st4("!!!BBBB"); callMethod_Val(st4); // "4. " return 0; } ``` 1. 字串會建構暫時物件,**++自動搜尋引數++ (符合一個引數的建構函數),此暫時物件會立刻解析**,之後也不會透過複製建構函數 2. **顯式把原有的物件 `st2`,複製到新物件 `st3`**,所以會呼叫 **複製建構函數** > ![](https://i.imgur.com/8KAZCi4.png) 3. 傳入 ++**引數**++,**建立暫時物件** (因為引數不匹配),不呼叫複製函數 4. 傳入 ++**引數**++ 再建立 ++**暫時物件**++,**隱式把 `st4` 複製到 `callMethod_Val` 函數中的引數 s**,會呼叫複製函數 > ![](https://i.imgur.com/lPCUWjr.png) | 方法 | 複製建構函數 | 一般建構函數 | | -------- | -------- | -------- | | st1 = "123" (隱式匹配、暫時變數) | No | Yes | | st1 = st2 | Yes 顯式 | No | | 呼叫方法(傳入 物件) | Yes 隱式 | No | | 呼叫方法(傳入 變數) (隱式匹配、暫時變數) | No | Yes | * **結論 : ==建構函數 & 複製建構函數不會同時使用==,==++有暫時物件不代表就會使用複製函數++==** ### 淺拷貝 & 深拷貝 * **預設的`複製建構函數`、`指定運算子` 都是淺拷貝,++==不拷貝 static 成員==++** 深拷貝都必須自己覆寫 :::success * 寫 ^1.^ `operator=` 指定運算子、^2.^ 複製建構函數,**才能確保有完整的深度拷貝** ::: * 另外還要注意的一點是,**拷貝函數的觸發情景** > ![](https://hackmd.io/_uploads/SyDtOZQD2.png) 1. **建構函數被賦予相同對象**,符合拷貝建構語法(`構造函數(const 類& other)`)所以會觸發: ```cpp= // 概念程式 Apple apple; Apple apple2(apple); ``` 2. **使用 `=` 賦予值**:複寫操作符 `=` 的意義,包含了拷貝、構造兩個概念 ```cpp= // 概念程式 Banana banana; Banana banana2 = banana; ``` 3. **function 傳入參數是實體類,也會觸發拷貝函數**: ```cpp= // 概念程式 void myFunc(Car car) { // 觸發 } void myFuncRef(Car& car) { // 引用不會觸發 } ``` 4. **返回的值事實體類時**,也會觸發拷貝函數 ```cpp= // 概念程式 Car myFunc() { // 觸發 return Car(); } Car& myFuncRef() { // 引用不會觸發 return & car; } ``` ### 重寫複製函數 - 深層拷貝 * **重寫複製函數** 深層拷貝 ```cpp= /** * 重寫 複製運算子 */ stringbad::stringbad(const stringbad & s) { //"1. " this->len = s.len; this->str = new char[this->len + 1]; // + 1 for '\0' std::strcpy(this->str, s.str); std::cout << ++objCount << "------call copy construct" << std::endl; } /** * 客戶端 使用 */ #include "stringbad.h" using std::cout; using std::endl; void callMethod_Val(stringbad s); int main() { stringbad st1("Hey, it's Show time"); cout << st1 << ", addr: " << &st1 << "\n"; stringbad st2 = st1; cout << st2 << ", addr: " << &st2 << "\n"; cout << "-----分隔線-----" << "\n\n"; callMethod_Val(st1); cout << "-----分隔線-----" << "\n\n"; return 0; } void callMethod_Val(stringbad s) { cout << "In Value:" << s << endl; } ``` * 這裡的重點在 1. **動態創建 棧 空間,並儲存 棧 的指標** (淺拷貝只會拷貝相同的地址) 2. **`std::strcpy` 複製內容** > 成功複製所有自串內容,並且 obj 數量也正確 > > ![](https://i.imgur.com/NnLoNSj.png) ### 指定運算子 - 顯式深層拷貝 :::info 指定運算子 就是 `=` (等號) ::: * 使用 指定運算子 除了是顯式,它也 **==避免暫時物件的產生 & 刪除==**;要 **==重載等號必須是函數成員==,[= (等號) 不能用 friend 加載](https://hackmd.io/Roh5BF4MRN2YPgmZZ7c3Sw?view#多載限制-operator)** > **物件 ++初始化++ 時不一定會使用到指定運算子** ```cpp= stringbad s = s1; // 使用複製建構函數 ``` :::warning * `operator=` 加載的實作要注意三點 1. 避免不要加載自己,==**比對指標,不是物件!**== 2. 釋放原來申請 堆 的動態空間 3. **返回 reference**,方便於++串接指定++ ::: ```cpp= /** * 宣告 */ #include <iostream> class stringbad { private: static int objCount; char *str; int len; public: stringbad(); stringbad(const char *str); stringbad(const stringbad & s); // 複製函數 ~stringbad(); // 重載 = const stringbad & operator=(const stringbad &s); // operator << friend std::ostream & operator<<(std::ostream & o, const stringbad & s); }; // -------------------------------------------------------------------------- /** * 實作 */ const stringbad & stringbad::operator =(const stringbad & s) { cout << "operator =, new address: " << &s << ", this address: " << this; if(&s == this) { return *this; // 相同物件則忽略 } delete[] this->str; // 刪除原本申請的空間 this->len = s.len; this->str = new char(this->len + 1); // 創建新空間 strcpy(this->str, s.str); // 複製新數據 return *this; // 返回對象 } /** * 客戶端使用 */ void overrideOperator() { stringbad st1; // 呼叫預設建構函數 st1 = "1111111"; // 呼叫指定建構函數 (轉換函數) cout << st1 << ", addr: " << &st1 << "\n"; cout << "-----分隔線-----" << "\n\n"; } ``` * 這分為三個部分 (並且依照順序) 1. 使用單一引數的建構函數,可以做為 [**轉換函數**](https://hackmd.io/Roh5BF4MRN2YPgmZZ7c3Sw?view#類別型態建構-amp-單一引數-amp-轉換函數),自動找尋符合的建構函數 2. 轉換函數找到對應的建構函數後,建構出暫時對象,再使用 `operator=` 重載指定運算子 3. 刪除暫時物件 ("11111" 創建出的暫時物件) * **結論 : ==如果沒重寫 operator= 的話,++轉換函數++ 創建出來的對象 就不會使用複製建構函數==,導致沒辦法深度拷貝** **--實作--** > ![](https://i.imgur.com/Mh2tyAy.png) ### 類別成員拷貝 * 若類別成員有其他類別 A,當你複製參數時,要**選擇 A 的 ++拷貝建構子++,這在類別繼承會說到** ```cpp= /** * 宣告 */ class stringbad { private: static int objCount; char *str; int len; // 新增 string 元素 std::string str2; public: stringbad(); stringbad(const char *str); stringbad(const string str2); stringbad(const stringbad & s); // 複製建構函數 ~stringbad(); const stringbad & operator=(const stringbad &s); // operator << friend std::ostream & operator<<(std::ostream & o, const stringbad & s); }; /** * 定義 */ stringbad::stringbad(const std::string str2) { this->str2 = str2; len = this->str2.length(); str = 0; } stringbad::stringbad(const stringbad & s) { std::cout << "重載 copy construct" << std::endl; this->len = s.len; this->str = new char[this->len + 1]; // + 1 for '\0' std::strcpy(this->str, s.str); // string 並沒有自己複製該物件,必須顯是呼叫複製建構函數 ! this->str2 = s.str2; std::cout << ++objCount << "------call copy construct" << std::endl; } std::ostream & operator<<(std::ostream & o, const stringbad & s) { //o << s.str; o << s.str2; return o; } /** * 使用 */ #include "stringbad.h" using std::cout; using std::endl; int main() { std::string str("Hey Boy~"); cout << "string: "<< str << endl; stringbad strbad_1(str); cout << "strbad_1: "<< strbad_1 << endl; stringbad strbad_2 = strbad_1; cout << "strbad_2: "<< strbad_2 << endl; return 0; } ``` **--實做--** > 可以看到 console 就沒有辦法輸出 strbad_2 的值,因為物件沒有拷貝成功 > ![reference link](https://i.imgur.com/4BvayuC.png) ## 回傳物件 > 回傳物件有分為 4 種,要看狀況而使用 1. `const object&` 2. `object&` 3. `object` 4. `const object` ### 回傳 const 物件的 reference * 回傳 const reference 大部分的**理由是考慮到效率,避免物件的複製**,**==回傳一個物件會呼叫到++複製建構函數++==,而回傳一個 ref 就不會** ```cpp= #include "stringbad.h" using std::cout; using std::endl; const stringbad & callMethod_Ref(const stringbad & s); const stringbad callMethod_Val(const stringbad & s); int main() { stringbad str("123 站著穿"); cout << callMethod_Ref(str) << endl; cout << callMethod_Val(str) << endl; return 0; } const stringbad & callMethod_Ref(const stringbad & s) { cout << "In Reference:" << s << endl; return s; } const stringbad callMethod_Val(const stringbad & s) { cout << "In Value:" << s << endl; return s; 回傳物件,會呼叫複製建構函數產生暫時物件 ! 導致效率下降 } ``` **--實作--** > ![](https://i.imgur.com/shLJIh3.png) ### 回傳非 const 物件的 reference * 回傳 ref 主要是**看有沒有這個需要**;可拿 cout 為例子,**cout 使用 << 運算子就必須回傳 ostream ref** (因為可能會修改內容) ```cpp= // ostream 成員函數 operator<< friend const ostream & operator<<(ostream &, const char *); ``` * 如果是需要改動到 ref 就需要使用這種方式 ```cpp= int main() { stringbad s1("123 站著穿"); stringbad s2, s3; s3 = s2 = s1; cout << "s1: " << s1 << endl; cout << "s2: " << s2 << endl; cout << "s3: " << s3 << endl; return 0; } ``` **--實作--** > ![](https://i.imgur.com/VrSguGi.png) ### 回傳 object * **一般來說多載運算子 ==operator 屬於這種範疇==** * 若回傳物件屬於區域變數時,就必須使用回傳物件,因為當**函數結束時該物件就會被釋放**,**==++區域變數回傳++不會使用 copy construct==** ```cpp= stringbad getString() { stringbad s1("123 站著穿"); return s1; // no call copy construct } int main() { stringbad s2 = getString(); stringbad s3 = s2; // call copy construct cout << "s2: " << s2 << endl; cout << "s3: " << s3 << endl; return 0; } ``` ### 回傳 const object * 防止 operator 的特性被濫用 ```cpp= net = f1 + f2; //"1. " f1 + f2 = net; // 濫用 "2. " ``` 1. `f1.operator+(f2)`,並返回新物件,賦予給 net 2. **`f1.operator+(f2)` 後產生暫時物件,再由 `暫時物件.operator=(net)`** * 如果使用 const 修飾回傳物件,代表回傳物件不可被更改,就可防止濫用的情況,上面的例子來說就是 f1.operator+(f2) 產生的暫時物件 ## Appendix & FAQ :::info ::: ###### tags: `C++`