--- title: 'C++ 物件 & 類別' disqus: kyleAlien --- C++ 物件 & 類別 === ## OverView of Content 如有引用參考請詳註出處,感謝 :smile: C++ 以類別為中心 [TOC] ## 程序式 & 物件導向 程序式 : **先思考程序** (演算法為重),再思考如何表達 物件導向設計(OOP) : **先思考資料** (資料為重),再思考如何表達 :::success * **OOP 特性** 1. 抽象化 2. 封裝 3. 同名宣告,不同定義 4. 繼承 5. 程式在利用 ::: ## 抽象化 & 類別 * **抽象化** : 抽象化是以 **使用者介面定義**,它是表示介面的資訊,**取出基本操作的特性,並以特性表示方法 (Function)**,可看作 **==方法的宣告==** * **類別 Class** : C++ 將**抽象化轉為使用者自定義的工具**,它結合 **==資料成員、型態==** & 處理資料的 **==成員函數==** ```cpp= #include <string> class PersonInfo { private : // 資料成員 std::string name; int age; int idNum; public : // 抽象 void setInfo(std::string name, int age, int id); std::string getInfo(); std::string getName(); int getAge(); int getIdNum(); }; ``` ### 物件型態 * 型態可以以 `struct`、`enum`、`union`、`class` ... 表示出來,其**主要作用在於三點** 1. **多少的記憶體** : 該資料需要記憶體的總量 2. **如何去解釋** : 每個記憶體讀取出來後要如何解釋 3. **如何操作** : 取出的資料可以如何操作,就像是指標不能拿來做乘法 下面範例新增一個 **struct MyStruct 型態**,內部放置資料、指標、short、short、int ```cpp= #include <iostream> using namespace std; union MyUnion { short Word; struct { char Low : 8; char Height : 8; } Byte ; }; enum MyEnum { Hi, Alien, Welcom }; struct MyStruct { char *name; short number; short age; int id; }; int main() { union MyUnion *myUnion = new union MyUnion; myUnion->Byte.Height = 0x01; myUnion->Byte.Low = 0x01; cout << "MyUnion: " << myUnion->Word << endl; enum MyEnum *myEnum = new enum MyEnum; cout << "MyEnum: " << myEnum << endl; struct MyStruct *myStruct = new struct MyStruct; myStruct->age = 100; cout << "MyStruct: " << myStruct->age << endl; return 0; } ``` > ![](https://i.imgur.com/YlpSFcf.png) MyStruct 總共需要空間為,`4 (ptr)` + `2 (short)` + `2 (short)` + `4 (int)` = `12 Byte` > 解釋、操作則按照 MyStruct 型態 > ![](https://i.imgur.com/8OUZdAz.png) ### 類別 class 封裝 * C++ 的關鍵字 `class` 標示該程式碼定義為類別,將 **==資料成員==、==成員函數聯繫 (binding)==**,成立一個獨立單元 **就可稱為類別 class** * **將資料放置在一起併與抽象化 (不定義函數體) 分離稱為封裝** ```cpp= #include <iostream> #include <string> using namespace std; class PersonInfo { private : // 資料成員 string name; int age; int idNumber; public : // 抽象方法 "3. " void setInfo(string name, int age, int id); string getInfo(); string getName(); int getAge(); int getIdNum(); }; ``` 1. 可把 `PersonInfo` 稱為 **物件 object** or **實體 instance** 2. **可直接宣告後定義方法**,C++ 會轉換回內嵌函數 (inline method),會面會在提到 inline 函數 3. **也可只宣告函數原型**,定義由其他檔案去完成 **--封裝--** > **資料 & 宣告分離** > > ![](https://i.imgur.com/OSl05np.png) :::success * `struct` & `class` 差異 | 關鍵字 | 存取權限 | 使用 | | -------- | -------- | -------- | | struct | **public** | 純粹作為資料 | | class | **private** (可控) | 作為抽象宣告函數、並可包含資料成員 | ::: ### 存取控制權 * 存取控制權 : 可以看到上面程式碼中有 `public`、`private`、`protect` 關鍵字,**class 類別用於==隔絕==使用者直接控制資料成員** (符合 OOP 原則),避免使用者直接存取的**功能 private 又稱為 資料隱藏 (data hiding) ==作用在於保持資料完整性==** * **預設控制權為 private**,大部分會寫出來,寫出來可以**強調資料特性**,**存取 private 資料的==唯一方式==是透過函數** ```cpp= class PersonInfo { std::string name; // default is private int age; // default is private int idNum; // default is private public : // 抽象 void setInfo(std::string name, int age, int id); std::string getInfo(); std::string getName(); int getAge(); int getIdNum(); }; ``` :::info class 後面記得要加 `;` 符號 ::: ### 實作 class 抽象函數 * 抽象函數可存取 private 的成員變量,**若非成員函數要存取私有變量,編譯器會 Error,但除了 ==friend 函數例外==** * 實作函數時要使用 ==**範疇運算子 `::`**==,**標示函數所屬的類別**,類似於 namespace 指定變數 1. **Class 類定義**:定義抽象方法、成員,並定義一個私有方法 ```cpp= #ifndef CLASS__TESTCLASS_H_ #define CLASS__TESTCLASS_H_ #include <string> #include <iostream> using namespace std; class PersonInfo { private : // 資料內容 string name; int age; int idNum; string info() { string str; str = "name: " + name + ", age: " + to_string(age) + ", id:" + to_string(idNum); return str; } void show() { cout << info() << endl; } public : void setInfo(string name, int age, int id); string getInfo(); string getName(); int getAge(); int getIdNum(); }; #endif /* CLASS__TESTCLASS_H_ */ ``` 2. **Class 類定義**: ```cpp= #include "TestClass.h" void PersonInfo::setInfo(string name, int age, int id) { this->name = name; this->age = age; this->idNum = id; } //"1. " string PersonInfo::getInfo() { return this->info(); //"2. " Occur error } string PersonInfo::getName() { return this->name; } int PersonInfo::getAge() { return this->age; } int PersonInfo::getIdNum() { return this->idNum; } ``` 1. 有 **使用範疇運算子** 的稱為 **標示名稱**、而沒有使用的稱為 **未標示名稱**,Ex: public 函數會使用 private 函數 2. **`this` 成員函數可以使用 `private` 的成員變量、函式,而 ++外部呼叫者不可以使用++,保護資料成員的安全性** **--實作--** > ![](https://i.imgur.com/7V3svNj.png) ### inline 成員函數 * 在 class 內有 **共同使用到的函數**,就可直接在 class **宣告時就定義出來,這會被編譯器==自動轉為內嵌 inline 函數==** * 當然也可以不再宣告時就定義,可分開做,但是要遵照 inline **特別規則** > 定義在使用它們的每個檔案中,也就是說**多少檔案 include,就要實作多少次** 1. **Class 抽象定義** ```cpp= /** * TestClass.h 封裝 PersonInfo */ class PersonInfo { private : // 資料內容 ... void show() { //"1. " std::cout << info() << std::endl; } public : // 抽象 ... void hello(); // 新增 private 方法 }; ``` * 自動翻譯成 inline 函數,inline 可有可無 (限於功能來說,但 inline 不等於一般函數) :::success * inline 更像是把函數複製到指定呼叫位置,不會出入 Stack ::: 2. **Class 實作**:其實這樣很不方便,所以大部分共用程式都會在宣告時就定義 ```cpp= /** * TestClass.cpp 實作 PersonInfo */ inline void PersonInfo::hello() { //"2. " std::cout << "Hello " << std::endl; } ``` :::warning 如果定義在 private 中,則不能在不同檔案中定義 ::: 3. main 直接內聯函數 ```cpp= int main() { PersonInfo info; info.setInfo("Alien", 20, 1234); cout << info.getInfo() << endl; info.hello(); return 0; } ``` > ![](https://i.imgur.com/HfXxYQR.png) ### class 函數 & 資料關係 * **每一個新建的物件都有==自己==的內部變數** (各自的變數儲存空間 in memory),但是**宣告實作函數是共用的** :::info * 在 OOP 中呼叫共用函數的動作稱為 `傳遞訊息 (sending Message)` ::: 1. 原型 Header ```cpp= /** * 原型 P.h */ #include <iostream> class P { public: int test = 0; void show(); } ``` 2. 實做類 Cpp source ```cpp= /** * 實作 */ #include "P.h" void P::show() { std::cout << "value: " << test << std::endl; } /** * 使用者 */ #include "P.h" int main() { P p1, p2; std::cout << &p1.test << "\n"; std::cout << &p2.test << "\n"; return 0; } ``` 物件創建概念圖 > ![](https://i.imgur.com/uM1JXwE.png) **--實作--** > 可看出創建兩個物件後,其參數是各自放置不同位子的 > > ![](https://i.imgur.com/AnDzTAE.png) ## 建構函數、解構函數 * **建構函數** : 與類別同名且無型態,並無回傳值,**在宣告類別時==自動==呼叫建構函數**,不管是靜態、動態創建 > 宣告方式:`MyClass()` * **解構 (析構) 函數** : Func 前有一個反向符號,與類別同名且無型態,也無回傳值,**在類別離開範疇、或是 delete 時==自動==呼叫析構函數** > 宣告方式:`~MyClass()` :::success * **Java & C++ 解構** Java 中就沒有解構函數,不過它有個類似的函數 `finalize`,不過由於 Java 會自動回收內存,所以不用考慮太多解構 ::: 1. Class 抽象宣告 ```cpp= /** * MyClass.h */ #include <iostream> class MyClass { public: MyClass(); ~MyClass(); }; ``` 2. Class 定義建構、解構函數 ```cpp= /** * MyClass.cpp */ #include "MyClass.h" MyClass::MyClass() { std::cout << "MyClass 建構函數" << std::endl; } MyClass::~MyClass() { std::cout << "MyClass 解構(析購)函數" << std::endl; } ``` 3. 靜態對象建構 ```cpp= /** * main.c */ #include <iostream> #include "./MyClass/MyClass.h" int main() { MyClass m1; return 0; } ``` **--實做--** > ![](https://i.imgur.com/bR9t58v.png) ### 建構函數 - 初始化 (預設參數) * 可 **使用預設引數,減少類別的初始化**;在宣告時定義預設引數,**定義時不可使用預設引數**,定義函數內部還是 **要指定到內部成員** 1. Class 抽象宣告 ```cpp= /** * MyClass.h */ #include <iostream> #include <string> using namespace std; class MyClass { private: int age; string name; public: MyClass(); MyClass(string name, int age); ~MyClass(); }; ``` 2. Class 定義預設有引數的建構 ```cpp= /** * MyClass.cpp */ #include "MyClass.h" MyClass::MyClass() { cout << "MyClass 建構函數" << endl; } MyClass::MyClass(string name, int age) { // 1. 定義時不用再寫出預設函數 cout << "MyClass 建構函數" << endl; //2. 定義時要把預設引數只定到內部成員 this->name = name; this->age = age; cout << "name: " << this->name << ", age = " << this->age << endl; } MyClass::~MyClass() { cout << "MyClass 解構(析購)函數" << endl; } int main() { MyClass m1; MyClass m2("Alien", 12); return 0; } ``` > ![](https://i.imgur.com/h1HYQ8Z.png) ### 預設建構函數 * 如果一個 Class 類沒有建構函數,系統會用預設建構函數 ```cpp= MyClass::MyClass() { } ``` * 假設有自己的建構函數並且有要求的引數,但在使用時沒有給予,編譯器會錯誤,有兩種辦法可以解決 ```cpp= // 宣告 MyClass(string name, int age); //定義 MyClass::MyClass(string name, int age) { } // 顯式使用 MyClass MyClass(); // 因沒有引數 name 編譯器會報錯 ``` 1. **函數重載**,多個建構函數 > MyClass(); // 重載另一個建構函數 > > MyClass(string name = "Alien", int age = 10); 2. 每個建構函數的**引數都有預設值** > MyClass(string name = "Alien", int age = 10); ### 建構函數 - 類初始化 (建構) 方法 * **靜態建構**有兩種方法,**顯式、隱式**,兩種方法都有相同效果 ```cpp= // 顯式 MyClass m1 = MyClass(str); // 隱式 MyClass m2(str, 12345, 67890); ``` * 動態建構是使用關鍵字 `new`,並使用 **回傳型態指標** ```cpp= // 動態 MyClass *m3 = new MyClass(str); ``` * 隱式使用 **如果沒有擴號,就一定是使用預設的建構函數** ```cpp= // 隱式 MyClass m4; // 沒括號 MyClass *m5 = new MyClass; // 沒括號 ``` * 物件陣列,也**可指定建構函數** ```cpp= // 預設 建構函數 MyClass infos_1[4]; // 指定 建構函數 MyClass infos_2[] = { MyClass(str, 60), MyClass(str2, 30) }; ``` * **C++11 可以使用 ==初始化列表 `{}`==** ```cpp= // 初始化列表 {} std::string str2("Pan"); MyClass m4{str2}; MyClass m5{str2, 5432, 1}; MyClass m6 = {str2, 333, 444}; MyClass *m7 = new MyClass{str2, 111, 222}; ``` **--實做--** > ![reference link](https://i.imgur.com/eta6iLh.png) ### const 成員函數 * 我們知道 `const` 可拿來規定該物件不可該改,但是如果**只規定物件 ++部分不可修改++ 則應該在被呼叫的 function 後加關鍵字 const** * **const 用來確保呼叫該 function 時,這個物件不會被更改** ==(++表示該函式保證不會修改呼叫的物件++)== > 下面會宣告兩個物件,該物件內部有一個 const_show() 被 const 修飾,show 並沒有被 const 修飾 1. **Class 抽象宣告** ```cpp= /** * MyClass.h 宣告物件 */ #include <iostream> #include <string> class MyClass { private: std::string name; int id; int phone; public: MyClass(); MyClass(std::string name, int id = 999, int phone = 999); void show(); void const_show() const; //"3. " ~MyClass(); }; ``` * 這個宣告整個被 const 包覆,導致 show 這個普通 function 也無法修改 name 變數;**const 修飾 `const_show` 方法,表示調用該方法時,這個物件的成員變數不會被該改**,變成了 **唯讀(`read-only`)** 2. **Class 定義** ```cpp= /** * MyClass.cpp 定義物件 */ #include "MyClass.h" MyClass::MyClass() { std::cout << "隱式預設,MyClass 建構函數" << std::endl; } MyClass::MyClass(std::string name, int id, int phone) { std::cout << "MyClass 建構函數" << std::endl; this->name = name; this->id = id; this->phone = phone; } MyClass::~MyClass() { std::cout << "MyClass 解構(析購)函數" << std::endl; } void MyClass::show() { name = "Hello, " + name; std::cout << "name: " << this->name << ", id = " << this->id << ", phone = " << this->phone << std::endl; } void MyClass::const_show() const { //name = "Hello const, " + name; // Occur Error std::cout << "name: " << this->name << ", id = " << this->id << ", phone = " << this->phone << std::endl; } ``` * `show` & `const_show` 內部都有修改成員變數 name,差別在 **const_show 有被 const 修飾,所以 ++無法修改 name 變數++** 3. 測試呼叫 ```cpp= /** * main.cpp 客戶端使用物件 */ int main() { std::string str("Alien"); MyClass m1 = MyClass(str); m1.show(); m1.const_show(); const MyClass m2 = MyClass(str); //m2.show(); return 0; } ``` **--實做--** > ![reference link](https://i.imgur.com/RcRnx59.png) ## this 指標 **this 指標指向呼叫成員函數** 的物件本身,並且 **==this 做為 隱藏 的引數,傳入成員函數中==**,這也就是為何上面我們在定義方法時,可以使用 `this` ```cpp= MyClass::MyClass(string name, int age) { cout << "MyClass 建構函數" << endl; this->name = name; // this 使用 this->age = age; } ``` * **this 的指標是被 const 指示**,所以不能被修改指向其它地方,**使用 間接運算子 `*` 可以取得 const Object 物件** ```cpp= // 概念程式 (this 等同 `MyClass * const this`) MyClass::MyClass(MyClass * const this, string name, int age) { cout << "MyClass 建構函數" << endl; this = new MyClass("Bob", 1234); this->name = name; this->age = age; } ``` > 如果要對 this 改變指向,則會錯誤 > > ![](https://i.imgur.com/8cB2wm3.png) ### this、ref 差異 * **引用 ref & this 理解**: ref 原型是不可修改內容的指標,其實也就是透過 const 修飾內容 & 指標指向的物件 (read-onlt) > ![](https://i.imgur.com/filTB6Q.png) :::info * **`ref` 應該思考為進一步的抽象** **它將指標給隱藏,讓使用者直接使用「符號」來訪問參數**,許多更高階的語言都使用引用來讓開發者使用 > 這也就是為啥有人說 C++ 不算完全的高級語言,因為它仍保有底層(assemble)的操作概念(底層需要知道 **記憶體位置、數值**,才可以操作參數) ::: ```cpp= /** * MyClass.h 宣告物件 */ class MyClass { private: std::string name; int score; public: MyClass(std::string name, int score); const MyClass& getHighScore(const MyClass & m) const; void show(); void const_show() const; ~MyClass(); }; /** * MyClass.cpp 定義物件 */ #include "MyClass.h" MyClass::MyClass(std::string name, int score) { //std::cout << "MyClass 建構函數" << std::endl; this->name = name; this->score = score; } MyClass::~MyClass() { //std::cout << "MyClass 解構(析購)函數" << std::endl; } void MyClass::show() { std::cout << "name: " << name << ", " <<"score: " << score << std::endl; } void MyClass::const_show() const { std::cout << "name: " << name << ", " <<"score: " << score << std::endl; } const MyClass& MyClass::getHighScore(const MyClass & m) const { //this->score += 10; //"4. " **`this` is read-only** if(m.score > score) { return m; } else { return *this; } } /** * main.cpp 客戶端使用物件 */ int main() { std::string str("Alien"); std::string str2("Pan"); MyClass m1 = MyClass(str, 60); MyClass m2 = MyClass(str2, 30); MyClass result1 = m1.getHighScore(m2); result1.show(); //m2.getHighScore(m1).show(); // "2. " fail //MyClass result2 = m2.getHighScore(m1); // "3. " ok //result2.show(); m2.getHighScore(m1).const_show(); // "1. " ok return 0; } ``` **--實做--** > ![](https://i.imgur.com/oOI0JdR.png) ## 類別範疇 - Class Scope * **屬於類別範疇的項目 (資料成員、成員函數) ==只能在類別內使用==** > **類別範疇** : **不能直接從外部呼叫類別內的成員**,儘管是 public 的成員仍然 **必須透過==類別名==才能訪問** * **++class 函數名稱是全域範疇++,不可轉為區域範疇 (不可 static)** > ![](https://i.imgur.com/51Pid3R.png) ```cpp= // 宣告 class P { private: int a; public: int b; void show(); } // ------------------------------------------------------------------ // 定義 必須透過範疇運算子 :: void P::show() { // TODO: } // ------------------------------------------------------------------ // 客戶訪問 P.a = 10; // Err : private 不可訪問 b = 10; // Err : 不可直接訪問,必須透過 class P P.b = 10; // ok show(); // Err : 不可直接訪問,必須透過 class P P.show(); // ok ``` 可透過以下幾種方法訪問 | 訪問符號 | 使用 | 範例 | | -------- | -------- | - | | **==.==** | class 宣告後呼叫 | P.b | | **==->==** | 方法中指標使用 | this->b | | **==::==** | **class 定義函數**、**靜態變數** | P::show() | ### 類別內常數 - Static Field * **類別宣告是描述物件的樣子 (也就是 Class)**,不是產生物件,所以 **const 常數無法做為初始化成員變數**,也就是說 const 描述的成員變量,**要在產生物件時才有記憶體空間** * 但仍有以下兩種方法可以達到目的 1. 使用 ==**static const**== 關鍵字,**將物件存在靜態區塊記憶體** 2. 使用 ==**enum**== 關鍵字,宣告列舉,**enum 具有類別範疇** ```cpp= class P { private: static const int Months = 12; // 所有物件共用 int Birthday_1[Months]; enum { Monthd = 12 // 所有物件共用 (類似 Static) }; int Birthday_2[Months]; }; ``` ### 範圍列舉 enum * 過去 enum 是靜態全域儲存,所有常常有名稱衝突的問題 ```cpp= enum Dinner { One, Two, Three }; enum Lunch { // 衝突 ~~~~~~ ! One, Two, Three // (內容物相同) }; ``` 1. **enum 可以透過 struct、class 包裝後重新使用** ```cpp= #include <iostream> using namespace std; enum Dinner { One, Two, Three }; struct MyStruct{ enum Lunch { One, Two, Three }; }; class PP { public: enum Breakfast { One, Two, Three }; }; namespace ABC { enum Hello { One, Two, Three }; } int main() { enum Dinner d = One; // ok (原來的宣告) cout << d << endl; MyStruct::Lunch l = MyStruct::One; // ok (宣告在 Struct 內部) cout << l << endl; MyStruct::Lunch l2 = MyStruct::Two; // ok cout << "l: " << l << ", l2: " << l2 << endl; PP::Breakfast b = PP::One; // ok (宣告在 Class 內部) cout << b << endl; ABC::Hello h2 = ABC::Hello::Two; // ok cout << "h: " << h << ", h2: " << h2 << endl; return 0; } ``` > 2. **enum 包裝 class、struct,稱為==範圍列舉==** ```cpp= enum class Food_1 { Rice, Noodle, Friut }; enum struct Food_2 { Rice, Noodle, Friut }; int main() { Food_1 f1 = Food_1::Rice; Food_2 f2 = Food_2::Friut; return 0; } ``` > ![](https://i.imgur.com/qxQUOBZ.png) * **C++11 對範圍列舉 的安全性更加嚴警**,**傳統 enum 會被自動轉型為 int,但範圍 enum 並不會轉型為 int** (除了強制轉型) ```cpp= #include <iostream> enum Dinner { One, Two, Three }; struct MyStruct{ enum Lunch { One, Two, Three }; }; class PP { public: enum Breakfast { One, Two, Three }; }; enum class Food_1 { Rice, Noodle, Friut }; int main() { enum Dinner d = One; // ok MyStruct::Lunch l = MyStruct::Two; // ok PP::Breakfast b = PP::Three; // ok Food_1 f = Food_1::Rice; if(l > d) { std::cout<< "Dinner < Lunch" << std::endl; } // not allow // // "1. " 對於範圍列舉來說是不可以的,但 可以透過強制轉型 int(f),轉換就可以比對 if(l < f) { std::cout<< "Food_1 < Lunch" << std::endl; } return 0; } ``` **--實做--** > ![](https://i.imgur.com/hQtzCBT.png) * enum 預設使用的型態是 int,**可使用 ==:== 轉換不同型態 (必須整數型態)** ```cpp= // 基礎單位轉為 short 更省空間 enum class : short Size {Small, Medium, Large}; ``` ## Appendix & FAQ :::info enum 轉換預設單位好像不行 (:short)??? ::: ###### tags: `C++` `this` `enum`