--- tags: 大學筆記系列 --- # 物件導向程式設計 作者: 410410054 王宥愷 ## 第零章 導論 物件導向程式設計的好處: 1. 可以**直接對應**到想設計的物件(ex:賽車),不須透過函式間接達成 2. 可以將**與物件有關的函式寫在一起**,方便管理 3. 透過**公開的互動函式**存取物件,並可將不希望直接更動的變數或函式設定成禁止存取 ## 第一章 ### 觀念釐清 1. cout<<"Hallo"<<endl; 中的 * << : 是函數 * cout : 是物件 * endl : 是函數 2. int a,b; float c,d; a+b 與 c+d 的"+"都是函數,但底層不一樣 3. switch語法 **沒寫break**須注意: ``` C++ #include <bits/stdc++.h> using namespace std; int main() { int x=0; switch(x) { case 0: cout<<0<<endl; case 2: cout<<1<<endl; default: cout<<"default"<<endl; } } ``` 以上程式碼會同時輸出0 1 default ## 第二章 ### C++ 配置記憶體 * 後面會提 ### C++ 讀檔 * 後面會提 ### "," 運算子 * 回傳最後一個expression所被賦予的值 > result = (first = 2, second = first + 1); //result=3 ## 第三章 ### 函式的概念 * 函式算是一種Precedural Abstraction(抽象化程序),把許多指令集結起來形成**黑盒子**,只要懂得使用即可 ### parameters vs. arguments * (Formal)parameters: 函示定義時的參數 * (Actual)argument: 函式呼叫時的參數 ## 第四章 ### Call by value vs. Call by reference * Call by value 例子 ``` C++ #include <bits/stdc++.h> using namespace std; void f(int b) { b+=1; } int main() { int a=0; f(a); cout<<a<<endl; //輸出0 return 0; } ``` * 假的 Call by reference 例子 ``` C++ #include <bits/stdc++.h> using namespace std; void f(int *b) { *b+=1; } int main() { //雖然利用函式成功修改a的值,但還是沒有直接修改到傳入的參數(指向a的指標) int a=0; f(&a); cout<<a<<endl; //輸出1 return 0; } ``` * 真正的 Call by reference 例子 ``` C++ #include <bits/stdc++.h> using namespace std; void f(int &b) { //利用&符號,表示此參數是以Call by reference的方式傳入 b+=1; } int main() { int a=0; f(a); cout<<a<<endl; //輸出1 return 0; } ``` ### Overloading * **函式名**、**函式參數組合**只要有其中一個不一樣便可以給過;在函式同名時,系統會自動依據不同的參數組合呼叫對應函式 ->換句話說,C++ 允許函式名設定成相同的 * 函式同名時的篩選**規則** 1. 檢查傳入的**參數數量**,數量相同者優先選擇 2. 檢查各個**參數的型態**,型態完全匹配者優先選擇 3. 檢查可行的**型態變換** * 先檢查具兼容性的型態變換(ex: int->double) * 再檢查會損失部分資訊的型態變換(ex: double->int) 若以上規則還分不出兩個函式的優先權,則會跳錯 舉例: * 各函式定義如下 1. f(int a,double b); 2. f(double a,int b); 3. f(int a,int b); 4. f(int a,int b,int c); * 則函式呼叫時 * f(2,3,5.2) 經過規則1篩選後,只有**第4個函式**滿足條件,故會呼叫該函式 * f(2.2,3) 經過規則1,2篩選後,只有**第2個函式**滿足條件,故會呼叫該函式 * f(2,3) 經過規則1,2篩選後,只有**第3個函式**滿足條件,故會呼叫該函式 * f(2.2,3.3) 經過規則1,2,3篩選後,第1,2個函式滿足條件,故**跳錯** ### Default argument * 可以給參數初始值,這樣就不用傳全部的參數了 ``` C++ #include <bits/stdc++.h> using namespace std; int f(int a,int b=10,int c=100) { return a+b+c; } int main() { cout<<f(1)<<endl; //輸出 111 cout<<f(1,2)<<endl; //輸出 103 cout<<f(1,2,3)<<endl; //輸出 6 return 0; } ``` * 可以結合Overloading的觀念,不衝突 ``` C++ #include <bits/stdc++.h> using namespace std; void f(int a) { cout<<"1"<<endl; } void f(int a,int b=1) { cout<<"2"<<endl; } int main() { f(1); //跳錯,因為兩個function都可以選 return 0; } ``` ### debug 工具 * 直接用 cout 印出來 * 使用編譯器自帶的debugger * assert(條件): 引入\<cassert\>標頭檔即可使用,當條件不滿足時,結束程式 * drivers: debug技巧,分別測試各個函式以找出可能的bug * Stubs: debug技巧,當函式還沒完成時,可以先回傳一個合理的值,便可先測試後續功能是否正常 ## 第五章 ### 觀念釐清 1. cout<<a[5]<<endl; 中的 "[" 與 "]" 也是函式 ### 連續的記憶體空間 ```C++ int a,b,c; ``` 此時a,b,c的記憶體空間是連續的,就如陣列一樣 ### for(auto x:a) 用法 該語法可以用來遍歷所有元素,須注意以下特性 * 這樣寫不會改到a中的元素 ```C++ int a[]={1,2,3}; for(auto x:a) { x++; } ``` * 要這樣寫才行 ```C++ int a[]={1,2,3}; for(auto &x:a) { x++; } ``` ### array 當參數傳入 ```C++ void f(int a[],int Size) { } int main() { int a[]={1,2,3}; f(a,3); } ``` 順帶一提,array在函式間的傳遞是假的call by reference ## 第六章 ### structure * 可以在宣告時初始化 ```C++ struct Date { int y; int m; int d; }; int main() { Date a={2023,3,7} } ``` * C++ struct中可以放function * C++ struct**預設元素為public**,但可以設成private ### class 簡介 * 新增了許多物件有關概念後,class 從struct中獨立出來自成一種型態 * C++ class**預設元素為private**,但可以設成public * 外部無法存取private的變數與函式,只能存取public的內容 * 使用 "::" 在class外定義被宣告在class內的function * 使用 "." 使用被宣告在class內的變數與函式(class型態的變數與函式被稱為物件) ```C++ class Date { private: int y=2023; int m=3; int d=7; public: void print(); }; void Date::print() { cout<<y<<" "<<m<<" "<<d<<endl; } int main() { Date a; a.print(); return 0; } ``` * 是一種abstract data type(抽象資料型態),使用者看不到細節 * encapsulation(封裝)概念: 例如int型態便是一個class,並且含有"+\-*/"等等function Accessor v.s. Mutator * 兩者通常都是public的函式 * 不同之處: * Accessor: 允許使用者讀資料,又稱做get member * Mutator: 允許使用者透過此函式適當的修改資料 ## 第七章 ### class constructor * 目的是在宣告class時自動初始化 * 必須與class同名 * 會回傳anonymous object(不是void) * 大部分放在public中 * 支援Overloading ``` C++ #include <bits/stdc++.h> using namespace std; class Date { private: int month; int date; public: Date(int input1,int input2) //這就是constructor,注意沒有回傳型態 { month=input1; date=input2; } /* Date(int input1,int input2) //寫成這樣可達成完全相同的事情 :month(input1),date(input2) { } */ Print() { cout<<month<<" "<<date<<endl; } }; int main() { Date birthday(9,4); //Date new_year; //跳錯,自訂義有參數的constructor後,一定要傳參數 birthday.Print(); birthday=Date(1,1); //可以像這樣使用constructor來重新初始化物件(不可使用".") //其中Date(1,1)是anonymous(匿名)的物件 birthday.Print(); return 0; } ``` * default constructor * 若沒有自訂constructor,則會自動創建 * 若有自訂constructor,則不創建 ### class destructor * 在該物件結束的時候會自動執行 * 性質與class constructor 類似,使用時在函式名前加上"~"即可 ``` C++ ~Date() { cout<<"end"<<endl; } ``` ### const的使用 * 結論: 不要改到const本身都沒事 ``` C++ #include <bits/stdc++.h> using namespace std; class Date { private: const int month; const int date; int year=2023; public: Date(int input1,int input2) //注意沒有回傳型態 :month(input1),date(input2) { } void Print() const //設定Print為const function { //year=2020; //跳錯,Print已經是const function了 //不可以改變class內的任何變數(class 外的不再此限) cout<<month<<" "<<date<<endl; } void input1(const int& a) { cout<<a<<endl; } void input2(int a) { cout<<a<<endl; } }; int main() { const int a=10; Date birthday(9,4);//第一次初始化const 變數是可行的 birthday.Print(); //birthday=Date(1,1); //跳錯,嘗試第二次初始化const 變數會錯 birthday.input1(a); //如果以call by reference的方式傳入,對應的函數參數必須也要是const birthday.input2(a); //如果不以call by reference的方式傳入,因為不會動到const的值,就沒差 return 0; } ``` ### Preprocessor Pitfalls * 類似函式,但本質上並不是,會做出不符合function的行為 * 可以想像成直接將傳進去的東西**原封不動置換** * 不可存取class中的private內容,即使在class中定義也不行 ``` C++ #include <bits/stdc++.h> using namespace std; #define judge(x) ((x)>5 ? (x):0) //#define judge (x) ((x)>5 ? (x):0) //跳錯,不可有空格 int main() { int a=6; cout<<judge(++a)<<endl; //輸出8 cout<<a<<endl; //輸出8 return 0; } ``` * 這個例子中,\++a會被原封不動的傳進去#define中,接著在(x)>5及(x)中各被+1後被回傳(main中的++並不起作用) ### inline function * 保留了Preprocessor Pitfalls的置換特性 * 使用方式與行為與函式一樣 * 適合取代很短的函式 * 使用起來更有效率 ``` C++ inline int judge(int x) { return (x) > 5 ? (x) : 0; //回傳7 } ``` ### Static 語法 * 以Static宣告的變數在離開該區塊後不會被釋放,且多個物件間共享記憶體 * 計算obj物件總數範例 ``` C++ #include <iostream> using namespace std; class obj { private: static int counter; //所有型態為obj的物件會共用同一個counter public: obj() { counter++; } obj(const obj& ini) //copy constrastor,沒寫的話將物件當作函式的參數時counter不會自動++,函式回傳的物件counter也不會自動++,但結束時counter還是會--,便會出現counter不正確的情況 { counter++; } ~obj() { counter--; } static void print() //在函式前加上static便可以在物件還沒創建之前便使用此函式 { cout<<counter<<endl; } }; int obj::counter=0; //一定要初始化,不然會出錯 int main() { obj::print(); //輸出0 obj a,b; a.print(); //輸出2 return 0; } ``` ## 第八章 ### operator overloading * 目的是讓使用者自定義運算符號,例如兩個class相加 * 使用non-member function的方式 ``` C++ #include <iostream> using namespace std; class A { private: int val; public: A(int input) :val(input) {} //constructor int get_val() const { return val; } }; //注意是寫在class外面,屬於non-member function const A operator - (const A &left,const A &right) //第一個const是為了確保回傳的anonymous物件不被使用者誤修改; //simple type(預設就有的型態)才要加 //第二跟第三個const是為了確保傳進去運算的物件不被修改到 { return A(left.get_val()-right.get_val()); } bool operator - (const A &temp) //單一參數的自定義operator { return -temp.get_val(); } int main() { A temp1(5),temp2(3); cout<<(temp1-temp2).get_val()<<endl; //(temp1+temp2)是"+"這個自訂的operator回傳的物件; //因為是anonymous object,這行結束後就會消失 cout<<-temp1.get_val()<<endl; return 0; } ``` * 使用member function的方式(感覺像是a.-b) ``` C++ #include <iostream> using namespace std; class A { private: int val; public: A(int input) :val(input) {} //constructor int get_val() const { return val; } //也可以搬到class中來實作operator const A operator - (const A &right) const //第一個const是為了確保回傳的anonymous物件不被使用者誤修改; //simple type(預設就有的型態)才要加 { //第二個const是為了確保傳進去運算的物件不被修改到(保護a-b中的b) //第三個const是為了確保本身運算的物件不被修改到(保護a-b中的a) return A(val-right.get_val()); } bool operator - () const //單一參數的自定義operator { return -val; } }; int main() { A temp1(5),temp2(3); cout<<(temp1-temp2).get_val()<<endl; //(temp1+temp2)是"+"這個自訂的operator回傳的物件; //因為是anonymous object,這行結束後就會消失 cout<<-temp1.get_val()<<endl; return 0; } ``` * member function 的困境 * 處理class跟simple type的operator是單向的,反過來寫會跳錯 ``` C++ #include <iostream> using namespace std; class A { private: int val; public: A(int input) :val(input) {} //constructor int get_val() const { return val; } //也可以搬到class中來實作operator const A operator - (const A &right) const { return A(val-right.get_val()); } }; int main() { A temp1(5); int temp2=3; cout<<(temp1-temp2).get_val()<<endl; //int型態的temp2會自動因為自定義的constructor先轉成A型態 //再透過自定義的operator運算 //cout<<(temp2-temp1).get_val()<<endl; //這樣寫會跳錯,因為在int型態的operator中 //沒有定義能處理A型態的operator return 0; } ``` * 其他operator * "="、"[]"、"()" :只能使用member function的方式自定義 * "++" :分成prefix(\++a)跟postfix(a\++),沒傳參數的視為prefix * "()" :可以讓呼叫物件的動作看起來像呼叫函式 ### friend * 設計用來從外部直接存取class private的成員 * 違反物件導向程式設計精神 * 是單向的 * 將friend作用在class與函式之間,便可以使用non-member function的方式模擬member function的效果(不需另外使用Accessor函式存取private成員) ```C++ #include <iostream> using namespace std; class A { private: int val; public: A(int input) :val(input) {} //constructor int get_val() const { return val; } friend const A operator - (const A &left,const A &right); //讓此函式有權限存取private成員 }; //注意是寫在class外面,屬於non-member function const A operator - (const A &left,const A &right) { return A(left.val-right.val); //可以直接存取private成員 } int main() { A temp1(5); int temp2=3; cout<<(temp1-temp2).get_val()<<endl; cout<<(temp2-temp1).get_val()<<endl; //反過來也能存取了 return 0; } ``` * friend也可以作用在class之間 ### Reference 進階應用 * 除了可以用Reference當函式參數傳入以外,也可以當函式返回值 * 這個技巧可以應用在 * cout<<a<<b<<endl; * a[0]=3; ```C++ #include <iostream> using namespace std; int& f(int &a) { return a; } int main() { int a=10; f(a)=5; cout<<a<<endl; //輸出5 return 0; } ``` ## 第九章 ### C++ C-string * 沿用C語言的字串處理系統 * 字串結尾是'\0',不小心改掉的話便不是C-string了 * 由字元陣列實作 * 支援用cin的方式輸入(以空白當作切割點),cout的方式輸出 * 可以使用cin.getline(字元陣列,最大讀的量) 來一次讀一行(會讀入'\n',並替換成'\0'儲存) ```C++ char a[10]; cin.getline(a,10); ``` * 可以使用cin.get(字元) 來一次讀一個字元 * 可以使用cout.put(字元) 來一次輸出一個字元 * 觀念釐清: 1. 只有宣告時可以直接使用"=",之後須使用strcpy或strncpy來重新賦值 > ex: char str[]="abc"; 3. strncpy比strcpy好,比較不會有複製超過範圍的問題 4. 沒有"\0"作為字串結尾的話便不是C-string,只是單純的字元陣列 > ex: char[3]={'a','b','c'}; ### C++ string * 想要一次讀一行可以使用getline語法(非member function),同樣會讀掉換行,但不會把換行存進去 ``` C++ #include <iostream> using namespace std; int main() { string a; getline(cin,a); cout<<a<<endl; } ``` * 也可以指定斷點 ``` C++ #include <iostream> using namespace std; int main() { string a; getline(cin,a,'!'); //指定斷點為'!' cout<<a<<endl; } ``` * getline會回傳cin,故可以進行以下操作 ```C++ #include <iostream> using namespace std; int main() { string a; int b; getline(cin,a,'!')>>b; cout<<a<<" "<<b<<endl; } ``` * getline陷阱 ```C++ #include <iostream> using namespace std; int main() { //輸入: //54 //apple int a; string b; cin>>a; //讀到54 getline(cin,b); //讀到\n,然後丟掉 cout<<a<<" "<<b<<endl; //b.size()=0 } ``` ## 第十章 ### C++ pointer * 相較於不嚴謹的C,C++將指標視為獨立的型態,不可任意轉換 ```C++ #include <iostream> using namespace std; int main() { //以下操作在C中被允許,但被C++擋下來 int* a; //long int b=a; //錯 //double* b=a; //錯 double* b=(double*)a; //可以過 } ``` ### C++ new 與delete * 取代C中的malloc() * new 比 malloc() 多做了constructor的部分 * delete 比free() 多做了desstructor的部分 * 觀念釐清: * 區域變數會從stack開始往下占用 * 動態配置的變數(new,delele)會從Heap開始往上占用 ![](https://i.imgur.com/WZftC9c.png =40%x) ```C++ #include <iostream> using namespace std; int main() { int* a=new int; //配置一塊整數的記憶體 *a=10; int* b=new int(20); //也可以用類似constructor的方式初始化 int* c=new int[10]; //配置連續的10個int大小的空間 //動態配置10x5的二維陣列 int** d=new int*[10]; for(int i=0;i<10;i++) { d[i]=new int[5]; } cout<<*a<<" "<<*b<<endl; //輸出10 20 delete a; //刪除該指標所指向的記憶體空間 a=nullptr; //好習慣,重新設成空指標 } ``` ### C++ nullptr * 為了解決無法區分NULL是0還是空指標的問題 * 本質上不是0,但能表現出0的性質 ```C++ #include <iostream> using namespace std; void f(int a) { cout<<"f1"<<endl; } void f(int* a) { cout<<"f2"<<endl; } int main() { //f(NULL); //跳錯,因為NULL=0,這樣會導致不知道要呼叫哪個function f(nullptr); //丟nullptr時,compiler會明確的呼叫第二個函式 if(nullptr==0) cout<<"Yes"<<endl; //會輸出YES } ``` ### "this" 應用 * this就是指向當前物件的指標 ```C++ #include <iostream> using namespace std; class A { private: int val; public: A(int input) :val(input) {} //constructor int get_val() const { return val; } //也可以搬到class中來實作operator const A operator = (const A &right) { this->val=right.get_val(); //val=right.get_val(); //與上面那行等價 return *this; //取值並回傳 } }; int main() { A temp1(3),temp2(4),temp3(5); temp1=temp2=temp3; //會先執行temp2=temp3,並回傳temp2 //接著執行temp1=temp2,並回傳temp1 cout<<temp1.get_val()<<" "<<temp2.get_val()<<" "<<temp3.get_val()<<endl; //輸出5 5 5 return 0; } ``` ## 第十一章 ### Class separation * 將程式寫在不同的檔案裡,修改時只需重新compile某部分即可,且可以對使用者隱藏不需知道的細節 * 分成兩類檔案: * interface files (.h): 給使用者使用方式介紹 * inplementation files (.cpp): 函式的底層定義與實現 * 為了避免.h檔多重定義的問題,可以使用#ifndef()解決,邏輯為"若未定義" ![](https://i.imgur.com/SI6eirM.png =30%x) ### namespace * 多人開發的程式可能會有需多重名的class,function等等,為了區分,可以將同名的class,function等等放入不同的namespace下 * 需要用到時則啟用(using)這個namespace(可以只在一個block中生效) * 或是使用"::"來指定使用哪個namespace中的class,function * 未寫在其中一個namespace中的class,function會自動的被定義在global namespace 中 * 支援巢狀 ```C++ #include <bits/stdc++.h> using namespace std; namespace n1 { void f() { cout<<"n1"<<endl; } } namespace n2 { void f() { cout<<"n2"<<endl; } } namespace //無名,只能在這個檔案中使用(local) { void debug() { cout<<"You have a bug"<<endl; } } int main() //此函式在global namespace 中 { { using namespace n1; //使用n1這個namespace f(); //輸出n1 } { using n2::f; //使用n2這個namespace中的f() function f(); //輸出n2 } n1::f(); //也可以用"::"指定使用哪個namespace中的fuction (輸出n1) { using namespace n1; using namespace n2; //f(); //跳錯,f() is ambiguous(使用到此函式時才會從namespace中找) } debug(); //可以直接使用無名的namespace return 0; } ``` ## 第十二章 ### stream * a flow of characters * 會先將file與stream做連結 * 分成instream,outstream ```cpp #include <iostream> #include <fstream> using namespace std; int main() { ifstream file_In; //創建ifstream物件 //ifstream file_In("test1_input.txt"); //ifstream、ofstream物件內建constructor,可以直接初始化要連結的檔案 ofstream file_out; //創建ofstream物件 file_In.open("test1_input.txt"); //連結file與stream物件,之後程式只要使用此物件即可 file_out.open("test1_output.txt"); int a,b,c,d; file_In>>a>>b>>c>>d; //從 test1_input.txt中讀取四個變數,並分別存入a,b,c,d中 cout<<a<<" "<<b<<" "<<c<<" "<<d<<endl; file_out<<a<<b<<c<<d<<endl; //將變數a,b,c,d輸出至test1_output.txt中 file_out.flush(); //刷新資料到硬碟中 file_In.close(); //斷掉物件與檔案的連結 file_out.close(); } ``` ## 第十四章 核心精神: 想要定義一個相對完整的架構,但又不想要重複定義太多東西 Reuse的方法分成兩種: * Composition: 將已有的物件定義在新的class中 (has a) * inheritance: 定義一個新的class作為已有物件的型態 (is a) 繼承架構: * base class: "general" class,供其他class繼承使用,通常較為抽象,非現實世界中真實存在的實物 * derived class: 繼承base class,自動擁有base class所定義的變數、函式,在此class中,只需列出自訂的新變數、函式或重新定義繼承的內容即可 觀念釐清: 1. assign operator(例如"="),constructor,destructor,copy constructor不會被繼承,但仍可以使用 2. derived class會繼承base class的private 變數跟函式,但不能存取 ### 不被繼承的Constructor derived class並不會繼承base class的constructor,但可以在定義自己的constructor時使用base class的constructor 注意: 若在derived class中的constructor沒有用到base class的constructor,則會自動呼叫base class的default constructor(沒有傳入任何參數的那個版本),此時,若base class有定義傳入至少一個參數的constructor,則會導致找不到default constructor而跳錯 另外,在建立的時候,會優先建立在上層(base class)定義的內容,才繼續建立在下層定義的內容 錯誤版本: ```cpp #include <bits/stdc++.h> using namespace std; class Employee //base class { private: string Name; string Sid; public: Employee(string name,string sid) :Name(name),Sid(sid) {} //沒寫default constructor }; class Hourly_Emplotee:public Employee //derived class { private: int hour_wage; public: //呼叫base class的constuctor來幫忙初始化 Hourly_Emplotee(string name,string sid,int money) :Employee(name,sid),hour_wage(money) {} //Hourly_Emplotee(int money) :hour_wage(money) {} //跳錯 //Hourly_Emplotee() {} //跳錯 //沒寫任何constructor的時候,compiler自動產生的constructor //反而不會呼叫base class的default constructor }; int main() { } ``` ![](https://hackmd.io/_uploads/BydXGiJLh.png) ### 不被繼承的destructor derived class並不會繼承base class的desstructor,與constructor不同的是,在建立derived class的desturctor時,compiler會自動呼叫base class的desstructor,不須手動寫上 另外,在刪除的時候,會優先刪除下層才定義的內容,才繼續刪除在上層(base class)定義的內容 ### protected 與public,private同地位,為了解決derived class無法使用base class中的private變數、函式的問題 在base class 中定義在protected區域中的變數與函式 * 對於外部來說,屬於private * 對於derived class來說,屬於public ```cpp #include <bits/stdc++.h> using namespace std; class Employee //base class { protected: string Name; string Sid; public: Employee(string name,string sid) :Name(name),Sid(sid) {} Employee() {} //default constructor }; class Hourly_Emplotee:public Employee //derived class { private: int hour_wage; public: Hourly_Emplotee(string name,string sid,int money) :Employee(name,sid),hour_wage(money) {} Hourly_Emplotee() {} //default constructor void edit_sid_to_666() { Sid="666"; //繼承base class的物件可以正常使用protected中的變數與函式 } }; int main() { Hourly_Emplotee emplotee1("Jason","12345",5000); emplotee1.edit_sid_to_666(); //正確 //emplotee1.Sid="666"; //跳錯,emplotee1.Sid在protected區域內,外部不可存取 } ``` ### Redifition 可以在derived class中重新定義base class中的內容 注意: 跟overloading不一樣的是,Redifition會強行取代base class中的同名函式,不論參數是否一樣 ```cpp! #include <bits/stdc++.h> using namespace std; class Employee //base class { protected: string Name; string Sid; public: Employee(string name,string sid) :Name(name),Sid(sid) {} Employee() {} //default constructor void print() { cout<<"now in Employee class"<<endl; } }; class Hourly_Emplotee:public Employee //derived class { private: int hour_wage; public: Hourly_Emplotee(string name,string sid,int money) :Employee(name,sid),hour_wage(money) {} Hourly_Emplotee() {} //default constructor void print(int temp) { cout<<"now in Hourly_Emplotee class"<<endl; } }; int main() { Hourly_Emplotee emplotee1("Jason","12345",5000); //emplotee1.print(); //錯誤,從base class中繼承下來的"沒傳任何參數的函式"被取代了 emplotee1.print(1); //正確,輸出"now in Hourly_Emplotee class" emplotee1.Employee::print(); //正確,輸出"now in Employee class"(base class 中的函式被取代後,仍有方法能被呼叫) //emplotee1.Sid="666"; //跳錯,emplotee1.Sid在protected區域內,外部不可存取 } ``` ### 繼承的種類 分成三種,分別為public,protected,private ![](https://hackmd.io/_uploads/rJTUVeg83.png) * 縱軸是在base class中不同區域的成員,橫軸是繼承的種類 * 綠色字體代表的意義為: * public: 連外部都可存取 * protected: 只有繼承者可存取 * private: 連繼承者都不可存取 * 例如使用private繼承,則原本在base class中是public的成員,在derived class中變成private的 ```cpp #include <bits/stdc++.h> using namespace std; class Employee //base class { protected: string Name; string Sid; public: string company_name="TSMC"; Employee(string name,string sid) :Name(name),Sid(sid) {} Employee() {} //default constructor }; class Hourly_Emplotee:private Employee //derived class { private: int hour_wage; public: Hourly_Emplotee(string name,string sid,int money) :Employee(name,sid),hour_wage(money) {} Hourly_Emplotee() {} //default constructor void Print() { //繼承下來的company_name相當於定義在此class的private區域 //本身還是可以存取 cout<<company_name<<endl; } }; int main() { Hourly_Emplotee emplotee1("Jason","12345",5000); emplotee1.Print(); //cout<<emplotee1.company_name<<endl; //但對於外部來說,無法存取private成員 } ``` 應用場景: 讓外部不可使用繼承下來,但用不到的函式 ```cpp #include <bits/stdc++.h> using namespace std; class Pet { public: void Speak() {cout<<"hallo"<<endl;} void Eat() {cout<<"yummy"<<endl;} }; class GoldFish: Pet //沒寫代表繼承種類是private { public: Pet::Eat; //重新定義成public函式 }; int main() { GoldFish denial; denial.Eat(); //外部可存取public函式,輸出 "yummy" //denial.Speak(); //跳錯,外部不可存取private函式 } ``` ### 多重繼承 很危險,不要亂用 ## 第十五章 ### Redifition的困境 假設 Base 是base class,其中包含了Print()函式 Derived 是derived class,其中redifine了Print()函式 ```cpp Derived obj; Base ptr=&obj; //因為obj也是一種Base型態,顧可以這樣做 ptr->print() //出事了阿伯,沒辦法指定這個函式到底要用Base的還是Derived的 //(compiler總是會選擇最底層(base)的那個) //ptr->Derived::print() //這樣也行不通,會跳錯 ``` 另一個完整的例子 ```cpp #include <bits/stdc++.h> using namespace std; class Figure { public: void center() {draw();} void draw() {cout<<"Figure draw"<<endl;} }; class Circle:public Figure { public: void draw() {cout<<"Circle draw"<<endl;} }; int main() { Circle my_circle; //呼叫繼承下來的center(),而center()又呼叫draw() //此時呼叫的draw()只能是Figure的,沒辦法指定要用Circle的draw() my_circle.center(); } ``` ### virtual function 概念: 先用而後定義;在compile的時候不先連結function,而是等到真的使用時再根據物件的型態使用不同的function 註:virtual關鍵字只需寫明一次,以下繼承並重新定義的同名函式會自動賦予virtual特性,多寫了也沒關係 ```cpp #include <bits/stdc++.h> using namespace std; class Figure { public: void center() {draw();} virtual void draw() {cout<<"Figure draw"<<endl;} }; class Circle:public Figure { public: void draw() {cout<<"Circle draw"<<endl;} }; int main() { Circle my_circle; //呼叫繼承下來的center(),而center()又呼叫draw() //此時呼叫的draw()因為是virtual,所以會根據該物件"當前的型態"來決定要用哪個draw //以這個例子來說,因為物件當前的型態是Circle,故會使用Circle的draw() my_circle.center(); } ``` 比較: redifined: 重新定義函式,讓原本的被隱藏起來 overridding: virtual的概念 overloading: 透過不同參數來決定使用哪個function override 關鍵字: 告知使用者此function上層是virtual,而此function overridding 上層的函式 final 關鍵字: 之後繼承此函式的class不能再redifined virtual缺點: late binding 是 "on the fly",可以動態調整 但需要更多空間跟時間 注意: 如果destructor沒寫virtual,以下程式碼會出事(只刪掉Figure有的部分) ```cpp Figure* fig=new Circle; delete fig; ``` ### Pure virtual 概念: * 使擁有Pure virtual function的class徹底抽象化 * 不可做成物件,但宣告成pointer是可行的 * 如果derived class沒有完成全部的pure virtual function,那該物件仍然不可被直接做出 ```cpp #include <bits/stdc++.h> using namespace std; class Figure { public: void center() {draw();} virtual void draw()=0; //宣告成pure virtual function }; class Circle:public Figure { public: void draw() {cout<<"Circle draw"<<endl;} }; int main() { Circle my_circle; my_circle.center(); //Figure fig1; //跳錯 } ``` ### Slicing Problem C++可以用base class型態去接其derived class,但會導致資料流失 可以用指標+virtual function來解決這個問題 ```cpp #include <bits/stdc++.h> using namespace std; class Figure { public: int center; virtual void Print() {cout<<"center= "<<center<<endl;} }; class Circle:public Figure { public: int radius; virtual void Print() {cout<<"center= "<<center<<endl<<"radius= "<<radius<<endl;} }; int main() { Figure fig; Circle my_circle; my_circle.center=5; my_circle.radius=2; fig=my_circle; //此時,關於radius的資訊被丟掉了 cout<<fig.center<<endl; //輸出5 //cout<<fig.radius<<endl; //跳錯,fig不存在radius Figure* fig_ptr=new Figure; Circle* my_circle_ptr=new Circle; my_circle_ptr->center=5; my_circle_ptr->radius=2; fig_ptr=my_circle_ptr; cout<<fig_ptr->center<<endl; //輸出5 //cout<<fig_ptr->radius<<endl; //跳錯,fig_ptr指向的空間雖然存在radius,但指標本身不知道 fig_ptr->Print(); //使用virtual function來存取資訊,順利輸出center,radius的資訊 } ``` ### Casting 繼承架構中Base class 跟 derived class 之間的轉型 分成Upcasting跟downcasting * Upcasting 概念是"derived class 也是一種base class型態" * downcasting 概念是"當base class的指標實際上指向的是derived class型態的記憶體位址時,可以轉型成derived class型態的指標" 實際例子 ```cpp #include <bits/stdc++.h> using namespace std; class Figure { public: int center; virtual void Print() {cout<<"center= "<<center<<endl;} }; class Circle:public Figure { public: int radius; virtual void Print() {cout<<"center= "<<center<<endl<<"radius= "<<radius<<endl;} }; int main() { Figure fig; Circle my_Circle; my_Circle.center=5; my_Circle.radius=2; fig=my_Circle; //Upcasting,把derived class型態轉型成base class型態 fig=static_cast<Figure> (my_Circle); //另一種Upcasting的寫法 //my_Circle=fig; //反之不成立 Figure* fig_ptr=new Circle; //Upcasting Circle* my_Circle_ptr=dynamic_cast<Circle*> (fig_ptr); //downcasting if(my_Circle_ptr==NULL) cout<<"NULL_1"<<endl; //不會進來 Figure* fig_ptr2=new Figure; //Upcasting //錯誤的downcasting,fig_ptr2指向的記憶體必須要是Circle才可以順利轉換,轉換失敗回傳NULL Circle* my_Circle_ptr2=dynamic_cast<Circle*> (fig_ptr2); if(my_Circle_ptr2==NULL) cout<<"NULL_2"<<endl; //輸出NULL_2 //Circle* my_Circle_ptr3=new Figure; //反過來寫會跳錯,因為Figure不是一種Circle Circle* my_Circle_ptr3=new Circle; Figure* fig_ptr3=dynamic_cast<Figure*> (my_Circle_ptr3); //dynamic_cast也可用於upcasting //實體物件用static_cast,指標用dynamic_cast } ``` ## 第十六章 想要實作Stash(不同Stash可以存不同的資料,但同一個Stash內部資料的型態是一致的),可以使用unsigned charactor * 缺點: 1. 使用者必須非常清楚自己存的資料型態 2. 若資料型態是pointer,在刪除後並未釋放指向的記憶體空間(因為全部資料底層都是unsigned charactor,destructor沒辦法區分) * 解決方案1: 使用polymorphism(待補) * 解決方案2: 使用template template 的概念是將"型態"也視為可變的變數,就像是不同型態function的集合 template 範例: ```C++ #include <bits/stdc++.h> using namespace std; class Test { int test; }; template<class T> //寫在會用到該template的函式上面,會自動將T換成收到的型態 void show(T a,T b,int c) //以產生對應的function { cout<<a<<" "<<b<<" "<<c<<endl; } int main() { show(2,3,5); show(2.2,3.6,9); //show(2,3.3,7); //錯,找不到對應的function(T的型態需一致) //Test test1,test2; //show(test1,test2,9); //錯,沒有對應的operator } ``` 也可以這樣寫,但要注意T1,T2一定都要用到,否則會跳錯 ``` template<class T1,class T2> f(...) ``` 注意:template的declear跟definition若寫在不同檔案可能會compile error -> 結論: 將template的declear跟definition都寫在header.h中,並在main.cpp中使用必對 ![](https://i.imgur.com/PebqCiE.png) * case 1: 只有include "header.h",導致main.cpp找不到showStuff(int,char,char)的definition,故出錯 * case 2: 在"header.cpp"中使用了showStuff(int,char,char),故在編譯的時候,該函式的definition已被包含在.o檔中,故main.cpp能正常使用 * case 3: 在"header.cpp"中使用了showStuff(int,char,char),故在編譯的時候,該函式的definition已被包含在.o檔中,但這個並不是main.cpp中使用的那個函式,故出錯 * case 4: "header.h","header.cpp"一起引用,便不會有找不到definition的問題 * case 5: declear 跟 definition 都寫在"header.h",接著被main.cpp引用,便不會有找不到definition的問題 class template 範例: ```C++ #include <bits/stdc++.h> using namespace std; template<class T> //template也能用在class上 class MyPair { private: T pair_first; T pair_second; public: MyPair(T input1,T input2); //constructor,definition寫在class外 ~MyPair() {} //destructor T first(); //取得第一個元素,definition寫在class外 T second() {return pair_second;} //取得第二個元素 }; template<class T> //記得加這行 MyPair<T>::MyPair(T input1,T input2) //不可寫成MyPair::MyPair(...) { pair_first=input1; pair_second=input2; } template<class T> //記得加這行 T MyPair<T>::first() //最前面的T代表回傳值 { return pair_first; } int main() { MyPair<int> pair1(5,7); //用"< >" 來指定T的型態 MyPair<int> pair2(5.3,7.1); //能過,因為前面已經宣告MyPair的T是int型態了 cout<<pair1.first()<<" "<<pair1.second()<<endl; } ``` * 注意: 1. template中的T可以為自訂型態,但要將operator(例如=,+等等)定義好 2. copy constructor記得處理好 3. 若T是指標,則記得處理destructor Tip: class constructor可以作為function的參數使用 ```C++ template<class T> T add(const Pair<T>& the_Pair); ``` ## 第十八章 為了在寫code的時候能專注於主線,而非繁瑣的錯誤處理,故有了Exception handleing觀念以解決此問題 * 概念是當發生錯誤時,會產生一個signal,並跳去執行Exception handler,**處理完後不會回到錯誤發生的地方** * 在function中暫時不知道該怎麼處理錯誤,但又想reuse function的程式碼,即適合使用Exception handleing(將throw跟catch寫在不同function) 語法: 1. try:處理主線情況 2. catch:處理例外情況(最多只能有一個參數) * 可以有很多個catch,以型態來區分 * catch(...)相當於"switch的default" 4. throw: 在try區塊中,丟出例外給catch * function可能也會throw exception,並交給當初呼叫function的地方用catch處理(目的是讓不同人呼叫function時,能設定不同的處理方式) throw list: 告訴使用者這個函式可能會用到那些throw,注意若在函式中寫了throw list,又用到了沒有註冊進throw list的throw,compile會過,但真的用到時會進到unexcept()錯誤處理 簡單的Exception handler範例 ```C++ #include <bits/stdc++.h> using namespace std; //throw是靠型態來認出要用哪個catch的 //自定義型態便可處理各式情況 class Devide_By_Zero {}; int main() { int a=5,b=0; try //主線情況 { if(b==0) throw Devide_By_Zero(); //丟出型態為Devide_By_Zero的class以決定要使用哪個Exception handler cout<<a/b<<endl; //若沒錯的話,才會執行這裡 } catch(Devide_By_Zero a) //主線情況出錯時的例外處理情況 { cout<<"you Devide_By_Zero!"<<endl; } catch(...) {cout<<"error!"<<endl;} //若沒有符合throw型態的catch,則會執行此catch } //本例會輸出 you Devide_By_Zero! ``` Exception handler結合函式的應用(較佳的架構) ```C++ #include <bits/stdc++.h> using namespace std; //throw是靠型態來認出要用哪個catch的 //自定義型態便可處理各式情況 class Devide_By_Zero {}; int calculate_1(int a,int b) throw (Devide_By_Zero) //可寫可不寫,目的是告訴使用者這個函式可能會丟出那些Exception { //注意: 下面一行throw後,會回到呼叫函式的地方查找catch,不是在function本身找catch //在不同地方呼叫同一個函式可能會進到不同的catch中做不同處理 if(b==0) throw Devide_By_Zero(); //丟出型態為Devide_By_Zero的class以決定要使用哪個Exception handler return a/b; } int calculate_2(int a,int b) throw (Devide_By_Zero) //可寫可不寫,目的是告訴使用者這個函式可能會丟出那些Exception { if(b==0) throw Devide_By_Zero(); //丟出型態為Devide_By_Zero的class以決定要使用哪個Exception handler return a/b; } int main() { int a=5,b=0; try //主線情況 { cout<<calculate_1(a,b)<<endl; } catch(Devide_By_Zero a) //主線情況出錯時的例外處理情況 { cout<<"you Devide_By_Zero in calculate_1!"<<endl; } try //主線情況 { cout<<calculate_2(a,b)<<endl; } catch(Devide_By_Zero a) //主線情況出錯時的例外處理情況 { cout<<"you Devide_By_Zero in calculate_2!"<<endl; } } //本例會輸出 //you Devide_By_Zero in calculate_1! //you Devide_By_Zero in calculate_2! ```