owned this note
owned this note
Published
Linked with GitHub
---
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 中會編譯失敗
>
> 
:::warning
* class 內它只能存在於宣告中,**在==定義中必須移除 static==不能使用指示字**,否則會在編譯期出錯
> 
:::
### 常量元素 - 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 由於是使用原本的物件,不會導致析構
> 
2. **產生暫時物件**:**暫時物件會複製原來物件 str 的成員函數,但++不包括 static 成員++**,而 **暫時物件的產生,又是透過==複製建構函數== 建構出來**
:::info
複製建構函數之後說明
:::
3. 在 `2` 中建構的暫時物件,會在 function 結束後自動釋放,而 **==它的釋放又會呼叫到 st2 的析構函數==,++導致 st2 new[] 出的空間被刪除++,在後面要求要顯示該物件的內容錯誤,無法正常顯示** (空字串)
> 
4. 這個方式又是另一個呼叫複製建構函數的方式
:::info
複製建構函數不使用一般的 construct
:::
> 
### 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); // 動態
}
```
> 
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;
}
```
> 
:::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`**,所以會呼叫 **複製建構函數**
> 
3. 傳入 ++**引數**++,**建立暫時物件** (因為引數不匹配),不呼叫複製函數
4. 傳入 ++**引數**++ 再建立 ++**暫時物件**++,**隱式把 `st4` 複製到 `callMethod_Val` 函數中的引數 s**,會呼叫複製函數
> 
| 方法 | 複製建構函數 | 一般建構函數 |
| -------- | -------- | -------- |
| st1 = "123" (隱式匹配、暫時變數) | No | Yes |
| st1 = st2 | Yes 顯式 | No |
| 呼叫方法(傳入 物件) | Yes 隱式 | No |
| 呼叫方法(傳入 變數) (隱式匹配、暫時變數) | No | Yes |
* **結論 : ==建構函數 & 複製建構函數不會同時使用==,==++有暫時物件不代表就會使用複製函數++==**
### 淺拷貝 & 深拷貝
* **預設的`複製建構函數`、`指定運算子` 都是淺拷貝,++==不拷貝 static 成員==++**
深拷貝都必須自己覆寫
:::success
* 寫 ^1.^ `operator=` 指定運算子、^2.^ 複製建構函數,**才能確保有完整的深度拷貝**
:::
* 另外還要注意的一點是,**拷貝函數的觸發情景**
> 
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 數量也正確
>
> 
### 指定運算子 - 顯式深層拷貝
:::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= 的話,++轉換函數++ 創建出來的對象 就不會使用複製建構函數==,導致沒辦法深度拷貝**
**--實作--**
> 
### 類別成員拷貝
* 若類別成員有其他類別 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 的值,因為物件沒有拷貝成功
> 
## 回傳物件
> 回傳物件有分為 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; 回傳物件,會呼叫複製建構函數產生暫時物件 ! 導致效率下降
}
```
**--實作--**
> 
### 回傳非 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;
}
```
**--實作--**
> 
### 回傳 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++`