--- tags: books, note --- # Class 如果今天在直角座標平面上有幾個點需要表示 ![](https://i.imgur.com/FJfy1er.png) 學`class`之前可能會馬上想到用兩個陣列 e.g. ```cpp= #include<iostream> #include<iomanip> using namespace std; int main() { //{ 2, 5},{-5, 3},{ 1, -1},{-4, -1},{ 5, 1} int x[5] = {2, -5, 1, -4, 5}; int y[5] = {5, 3, -1, -1, 1}; for (int i = 0; i != 5; i++) cout << "(" << setw(2) << x[i] << ", " << setw(2) << y[i] << ")" << endl; return 0; } ``` ``` result: ( 2, 5) (-5, 3) ( 1, -1) (-4, -1) ( 5, 1) ``` 這樣寫固然輕鬆,但如果操作複雜一點呢? 假設需要對`x`進行排序 i.e. ```cpp= #include<iostream> #include<iomanip> using namespace std; void sort(int key[], int val[], int size) { for (int i = 0; i < size; i++) for (int j = i; j < size; j++) if (key[j] < key[i]) swap(key[i], key[j]), swap(val[i], val[j]); } int main() { //{ 2, 5},{-5, 3},{ 1, -1},{-4, -1},{ 5, 1} int x[5] = {2, -5, 1, -4, 5}; int y[5] = {5, 3, -1, -1, 1}; sort(x, y, 5); for (int i = 0; i != 5; i++) cout << "(" << setw(2) << x[i] << ", " << setw(2) << y[i] << ")" << endl; return 0; } ``` ``` result: (-5, 3) (-4, -1) ( 1, -1) ( 2, 5) ( 5, 1) ``` 因為內建`sort`沒有辦法在判斷其中一個值的同時改變與其下標相同的另一個值 所以必須要自己寫一個`sort` 現在是因為我刻的`sort`簡單,看起來才會沒這麼複雜 但如果對於`sort`需要到$O(nlogn)$的速度的話,自己寫勢必就沒效率又容易錯 而這個問題的其中一個解決方法就是使用**class** --- ## Declaration 我們可以把`class`想成是自己定義的型態 在這個問題中,我們需要的是一個能分別儲存`x`和`y`值的型態 先姑且叫做`position` 那在C++我們可以這樣宣告 ```cpp= class position{ }; ``` 與函數不同 記得在大括號的最後打上分號 --- ## Member variable 有了`position`這個`class` 接下來我們希望能在每個`position`都擁有`int`型態的`x`跟`y` i.e. ```cpp= class position { int x; int y; }; ``` `int`型態的`x`和`y`就是position這個`class`的成員變數 這一段的意思是 1. 定義一個`class`叫`position` 2. 每一個`position`包含兩個`int`變數`x`跟`y` 那要如何存取呢 ```cpp= int main(){ position pos; pos.x = 5, pos.y = 10; cout << pos.y << endl; } ``` 像這樣使用 `.`+`(變數名稱)`就可以存取那個變數 一開始提到的直角座標就可以改成像這樣的形式 i.e. ```cpp= #include<iostream> #include<iomanip> using namespace std; class position { int x; int y; }; int main() { //{ 2, 5},{-5, 3},{ 1, -1},{-4, -1},{ 5, 1} position pos[5]; pos[0].x = 2, pos[0].y = 5; pos[1].x = -5, pos[1].y = 3; pos[2].x = 1, pos[2].y = -1; pos[3].x = -4, pos[3].y = -1; pos[4].x = 5, pos[4].y = 1; for (int i = 0; i != 5; i++) cout << "(" << setw(2) << pos[i].x << ", " << setw(2) << pos[i].y << ")" << endl; return 0; } ``` :::danger error: could not convert '{2, 5}' from '\<brace-enclosed initializer list\>' to 'position' error: 'int position::x' is private within this context ::: 改完之後會出現這樣子的錯誤訊息 那這是正常的 因為我們還沒加上十分重要的一行 ⸺ **public:** --- ## Private/Public ```cpp= class position { public: int x; int y; }; ``` `public:`的作用是讓`public`以下的內容在`class`以外也能使用 原本上面那個`error`寫說`'int position::x' is private`是因為 `class`會預設所有東西為`private`,也就是在`class`外不能使用 相反的,如果想要保護`class`裡的資料,用`private`就是個不錯的選擇 在加上`public:`之後,上面的程式碼可以順利執行了 輸出如下 i.e. ``` result: ( 2, 5) (-5, 3) ( 1, -1) (-4, -1) ( 5, 1) ``` --- ## Constructor 在上一節,我用了這個又難看又長的方法來初始化`position` ```cpp= position pos[5]; pos[0].x = 2, pos[0].y = 5; pos[1].x = -5, pos[1].y = 3; pos[2].x = 1, pos[2].y = -1; pos[3].x = -4, pos[3].y = -1; pos[4].x = 5, pos[4].y = 1; ``` 想當然一般來說不可能會這樣用,既慢又沒有效率 那有什麼解決方法呢? 其中一個就是建構元(Constructor) 建構元正如其名,是在一個class要**建構**的時候呼叫的函式 同樣以直角坐標的點來舉例 i.e. ```cpp= class position{ public: int x; int y; position(int a, int b){ x = a, y = b; } }; ``` 其中 ```cpp position(int a, int b){ x = a, y = b; } ``` 的部分就是建構元 建構元並沒有回傳值,且在宣告時的名字要和`class`的名字一致 這樣一來,上述那個很長的初始化就可以縮短 e.g. ```cpp= #include<iostream> #include<iomanip> using namespace std; class position { public: int x; int y; position(int a, int b) { x = a, y = b; } }; int main() { //{ 2, 5},{-5, 3},{ 1, -1},{-4, -1},{ 5, 1} position pos[5] = { position( 2, 5), position(-5, 3), position( 1, -1), position(-4, -1), position( 5, 1) }; for (int i = 0; i != 5; i++) cout << "(" << setw(2) << pos[i].x << ", " << setw(2) << pos[i].y << ")" << endl; return 0; } ``` 或者是在定義的地方可以用建構元專用的初始化敘述 例如上面那個建構元就可以改寫成這種形式 i.e. ```cpp= position(int a, int b): x(a), y(b) {} ``` :::success **隨堂測驗** 有什麼情況是非得用這種初始化敘述的呢? (提示,宣告後不能改變) ::: 以為這樣就結束了嗎 不,顯然否 其實在C++11以後,新增了一個叫做`initializer_list`的東西 這種容器的應用很廣泛,在`class`上,我們可以用它來做非常簡單的初始化 i.e. ```cpp= #include<iostream> #include<iomanip> using namespace std; class position { public: int x; int y; }; int main() { //{ 2, 5},{-5, 3},{ 1, -1},{-4, -1},{ 5, 1} position pos[5] = {{ 2, 5}, { -5, 3}, { 1, -1}, { -4, -1}, { 5, 1}}; for (int i = 0; i != 5; i++) cout << "(" << setw(2) << pos[i].x << ", " << setw(2) << pos[i].y << ")" << endl; return 0; } ``` 像這樣用大括號包住一些值,初始化時就會依序對應到`class`裡的變數 :::success **延伸思考** 如果大括號裡的值比宣告的變數少的話會發生什麼事,如果比較多又會發生什麼事? ::: --- ## Destructor 有好好背4000單的同學應該就會知道 construction <--> destruction 那相對於建構元(Constructor) 當然就會有解構元(Destructor)的存在 接下來介紹幾個解構元的需要注意的點 * 解構元在`class`被解構的時候才會呼叫 * 解構元沒有回傳值也沒有引數 那接下來就是示範解構元如何用 e.g. ```cpp= class position { public: int x; int y; ~position(){} }; ``` 就這樣,沒了💩 看起來好像很沒用 我可以肯定的說,~~真的沒用~~ . . . 在某些地方真的是有用的 主要會有差的地方其實是在`class`裡有指標的時候 為了釋放`class`內指標占用的記憶體 可以在解構元寫`delete` :::success **動動腦** 還有哪些地方用的到解構元呢? ::: --- ## Member function 一樣是那個直角坐標上的五個點 如果今天需要對點做一個操作,要把`x`跟`y`做對換 可以這樣寫 i.e. ```cpp= class position { public: int x; int y; }; void swap(position& pos){ int tmp = pos.x; pos.x = pos.y; pos.y = tmp; } ``` 這種寫法雖然很直覺,不過如果`x`跟`y`兩個變數都是`private`的話就會有些問題 e.g. ```cpp= class position { int x; int y; }; void swap(position& pos){ int tmp = pos.x; pos.x = pos.y; pos.y = tmp; } ``` :::danger error: 'int position::x' is private within this context error: 'int position::y' is private within this context ::: 如果你`x`跟`y`需要設成`private` 但需要對這些變數進行一些自定義的操作時 這時候用`public`的成員函數就可以解決上面的編譯錯誤 同時限定外部只能透過這個函式來操作變數值 會很大部分避免不知道哪裡直接修改到變數值然後debug不出來的問題 成員函數的宣告方法跟一般函數無異 不過在成員函數內可以直接取用`class`的成員變數和其他成員函數 講了這麼多,直接上code i.e. ```cpp= #include<iostream> using namespace std; class position { int x; int y; public: void swap() { int tmp = x; x = y; y = tmp; } void print() { cout << x << ' ' << y << endl; } position(int a, int b): x(a), y(b) {}; }; int main() { position pos{2, 5}; pos.print(); pos.swap(); pos.print(); return 0; } ``` ``` result: 2 5 5 2 ``` :::success **想想看** 明明上面小節用`initializer_list`沒有寫,為什麼還要再寫一個建構元呢 ::: --- ## Difference between Class and Struct 在舊版本的C語言中,`Struct`和`Class`其實有明顯的差別 不過到了C++,兩者最顯著的差別就只剩下 * `Class`預設`private` * `Struct`預設`public` 什麼意思呢 直接上code ```cpp= #include<iostream> using namespace std; class position { int x; int y; }; int main() { position pos{2, 5}; cout << pos.x << ' ' << pos.y << endl; return 0; } ``` :::danger error: 'int position::x' is private within this context error: 'int position::y' is private within this context ::: 由於前幾節已經討論過這個情形 很明顯可以看出問題在於`position`裡沒有加上`public` 不過當我們改用`struct` 不用改任何其他code也不會有編譯錯誤 ```cpp= #include<iostream> using namespace std; struct position { int x; int y; }; int main() { position pos{2, 5}; cout << pos.x << ' ' << pos.y << endl; return 0; } ``` ``` result: 2 5 ``` :::success **調查** 你比較喜歡用class還是struct呢,可以善用HackMD的留言功能跟我們說喔 ::: --- ## Inheritance 這屬於比較進階的用法 學到三角函數之後,極座標就是一個我們會碰到,有別於直角坐標系的另一種表示方法 那如果今天兩種表示方法都要用到 我們當然可以用兩種`class`來裝這兩種不同的表示方法 但是有些不管是直角座標或極座標都會用到的函數 寫兩次就感覺有點太過多餘 這種時候就可以用到`class`的**繼承**(Inheritance) 直接上code i.e. ```cpp= #include<iostream> using namespace std; struct position { void print() { cout << "this is a point" << endl; } }; struct polar: position { float r; float theta; polar(float a, float b): r(a), theta(b) {} }; struct cartesian: position { float x; float y; cartesian(float a, float b): x(a), y(b) {} }; void wow(position pos) { cout << "wow, a point" << endl; } int main() { cartesian pos{2, 5}; pos.print(); wow(pos); wow(polar(1, 2)); return 0; } ``` ``` result: this is a point wow, a point wow, a point ``` 可以發現不管是`cartesian`或是`polar`都屬於`position` 所以`position`內定義的函式在`cartesian`和`polar`都可以用 同樣的使用`position`當作引數的外部函式 `cartesian`和`polar`也都可以用 :::success **腦力激盪** 還有什麼情況很適合用繼承來表示呢? ::: --- > [color=#1f1e33][name=施羿廷][time=Fri, Jun 19, 2020 10:37 PM]