精華區beta b885060xx 關於我們 聯絡資訊
發信人: YHH.bbs@bbs.iljhs.il.edu.tw (工藤新一), 看板: programming 標 題: OOP by C++ (1) [轉載] (轉載) 發信站: 宜蘭資教 山水蘭陽 (Tue Aug 19 23:22:59 1997) 轉信站: sobee!netnews.ntu!iljhs 本文轉載自 PCtrick 討論區,原作者為 YHH@iljhs C++物件導向程式語言簡明教程 <第一章 概論> 撰文:程式創作區小樹 使用編譯器:Turbo C++ 3.0 讀者須知: 本文章由C 開始談起,算是一篇C++的入門參考書 文內假設各位已經有C語言的基礎,是C++的初學者, 讀者至少必須對C有相當程度的熟悉, 如流程控制(if,for,while,switch...),指標,陣列,結構(struct), 都必須非常熟悉, 否則恐怕會有閱讀上的困難. 本文件的目的是想要讓C++更好學, 使大家都能輕易的瞭解C++的精義. 因此本文件歡迎傳閱, 若有需要也歡迎做適當的修改, 以修正錯誤或增加可讀性. 但請勿加入不雅的言語, 而且修改者務必註明修改者名稱及修改日期, 並保留原作者之簽名. 謝謝各位! <1> 從結構談起 首先我丟給各位一個題目, 假設各位正在設計一個遊戲, 螢幕上有十顆子彈,各在不同的位置, 往下一齊飛去, 直到飛出螢幕為止, 向這樣的程式你會怎麼做呢? 給你五分鐘想一想......... 要想喔! 想完了才可以繼續往下看喔! OK!想好了嗎? 讓我來公佈答案吧! 作法一: 剛學習C或者是慣用Basic的人也許會這麼做~~~~ void show(int x,int y);//這是一個副程式,可以在(x,y)上畫出一點, //至於要怎麼寫就先不管他,反正你知道有這麼一個函式就對了 void main() { int x[10],y[10];//這是記錄十顆子彈位置的陣列 while(1) for(int i=0;i<10;i++) { show(x[i],y[i]);//畫出每一個子彈 y[i]++;//將子彈往下移一格 } } 這種作法可以說是一般人最常用的作法了, 對小程式也就夠了! 缺點是一旦程式大起來了, 要處理的東西變多了, 例如螢幕上有十顆子彈,十艘船,十顆隕石,十顆背景行星或更多 那程式就會變得凌亂不堪了!!(想像一下) 光是讀懂程式是做什麼的就是一件很辛苦的事 有另外一種作法如下: 作法二: 對C比較有概念的人應該會這麼做~~~~ void show(int x,int y);//這是一個副程式,可以在(x,y)上畫出一點, //至於要怎麼寫就先不管他,反正你知道有這麼一個函式就對了 void main() { struct _Bullet//這是一個 "子彈" 的 "模子" { int x; int y; }; _Bullet Bullet[10];//用模子做出10顆子彈 while(1) for(int i=0;i<10;i++) { show(Bullet[i].x,Bullet[i].y);//將子彈秀出來 Bullet[i].y++;//將子彈往下移一格 } } 這個程式好瞭解多了, 你可以想像 _Bullet 是子彈的 "模子" 我們用這個模子做出了10顆名字分別為Bullet[0],Bullet[1]...的子彈 位置是子彈的一個"屬性"(就好像身高體重是人的屬性一樣) 程式可以由Bullet[i].x或Bullet[i].y來取用這些屬性 這樣子程式就好懂多了, 無論程式寫得多大, 也可以很容易的知道某個變數到底是誰的,做什麼用的 不會像上一個程式一樣凌亂不堪了! 總結: 其實兩個程式最大的差別,在於是否"擬物化" 在程式一,子彈被化成一個個的陣列,分居各處,沒什麼關連 在程式二,子彈被程式語言模擬成一個個的"子彈" 彷彿我們操作的真是一顆顆的子彈似的. 簡單的說,第一個程式是問題遷就程式,(硬把"子彈"拆成毫不相關的陣列) 而第二個程式則是程式模擬問題.(用struct _Bullet做出十顆"子彈") 而傳統程序導向和物件導向設計的差別之一就在此, 傳統程序導向的設計讓程式設計變得很抽象, 程式的結構與我們所經驗到的世界大不相同 這種結果導致程式很難被瞭解(因為違背了日常經驗嘛!) 除錯和維護起來就很困難!! 而物件導向程式設計基本上就是要將程式語言拿來模擬世界的運作!!! ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 這樣子設計程式其實就是在模擬世界的運行, 程式會更好了解, 更容易製作, 維護起來也輕鬆多了! <2> 物件與類別 物件和類別是物件導向程式設計中第一個要接觸到的觀念, 讓我們來看看什麼是物件,何謂類別 我們在上一篇中用"子彈的模子"(struct _Bullet) 做出了10顆"子彈"(Bullet[i]) 如果用比較學術性的講法, 其中"子彈的模子"就是"類別",在英文以"Class"來表示 那一顆顆的"子彈"就是"物件",英文是"Object" 在C++中,為了強調這種觀念,通常不用struct來做"模子" 而改用"class"來做!! 以後我們也將只用class來做"模子"(就是類別啦!!) 基本上兩個敘述是差不多的,但仍有一些小差別, 至於差別在哪兒,這個我們稍後再敘. 上一個程式中的"Struct _Bullet"其實也可以寫成這樣: class _Bullet { public://多了這個敘述 int x; int y; }; 效果一模一樣呢!!! 我們可以廣義的這樣說, 若有一行敘述如下: double a,b; 我們可不可以說double 是一個倍精度浮點數的模子呢? 而是不是a,b正是由double這個模子做出來的浮點數"物件"呢? 同樣的,所有的C語言基本資料型態, 例如int啦!char啦!指標啦!基本上都是一種"模子". 也就是一種"類別" 而由這些模子所做出來的變數其實都是一個個的物件! 更廣義的說,其實所有的資料型態都是類別 而所有的變數都是物件, 不是嗎? 只要這樣想, 應該就不難瞭解類別和物件的意義吧!!! <3> 類別成員函數 前面所講到的程式裡,我們用"子彈模子"(即子彈類別)做出了十顆"子彈" 然後用一些敘述及show()函式去使子彈前進並畫出來, 這樣子說實話還不夠像真實的子彈, 因為真正的子彈是"自己"飛的, 哪是由外力操縱的呢? 其實我們可以這麼做: void main() { class _Bullet//這是一個 "子彈" 的 "模子",改用class來寫 { public: int x; int y; void show(int x,int y);//這是一個函式,可以在(x,y)上畫出一點,內容先不管 void go()//這是一個函式,呼叫他可以讓子彈往下移一格並畫出來 { show(x,y); y++; } int collision(int X,int Y)//這是一個函式,用來判斷子彈是否撞到東西 { return (x==X && Y==y); //不是則傳回0 } }; _Bullet Bullet[10];//用模子做出10顆子彈 while(1) for(int i=0;i<10;i++) Bullet[i].go();//下命令給子彈,要子彈往下移一格,並將自己畫出 } 哇塞!太誇張了吧!! 函式區然可以放到"模子"裡面去!!!! 別懷疑,這是C++的語法, 而且放到"模子"裡的函式還可以以 物件名.函式名(參數); 的方法呼叫喔! ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 或許你會說:把函式放到class敘述中有什麼好處呢? 其實我們可以這樣想: Bullet[i]是一顆顆的子彈, 而這個子彈是有生命的, 也就是說我們可以下命令給他(用點想像力) 而Bullet[i].go()中的"go()"其實就是命令的名稱, 意思就是跟子彈說:"衝啊!衝到下一格去" 子彈接到命令後他"自己"自然就會照作, 而有的命令是必須有附帶一些訊息的, 例如collision是問子彈是否撞到了(X,Y)這個點, 因此點的位置必須告訴子彈, 而敘述要寫成Bullet[i].collision(X,Y); 如果子彈發現撞到了,他會告訴你 於是你可以做一些措施, 例如畫出爆炸畫面...或什麼的... 像此類放在類別敘述內的函式稱為類別的函式成員                   ~~~~~~~ 而像int x,y 這些在類別內的物件則稱為該類別的的資料成員 ~~~~~~~~ 這樣可以瞭解嗎? <4> 私用及公用成員,資訊隱藏 投過販賣機嗎? (廢話!!!) 販賣機其實是一個很複雜很複雜的機器, 可是他的使用方法卻非常非常的簡單, 只要投個硬幣進去(呼叫成員函式) 按下所要的飲料(成員函式的參數) 飲料自然就會掉出來!!! 你不必管販賣機是怎麼做到的, 你只要會操作就可以了! 原因是販賣機把複雜的部份藏起來了, 只露出了供人使用的部份. 同樣的道理,我們的子彈"類別"也可以這樣 其實我們希望不管子彈是怎麼飛的, 只要我們叫他飛他就飛就可以了. 而且操作方法是越簡單越好, 所以啦! 一些並不會被使用者使用到的函式,如show()啦! 以及一些不足為外人道的資料,如int x,y 啦! 如果能夠把他們藏起來(就像販賣機把機器部份包起來) 只給類別自己的成員函式去使用, 而露出那些公用的操作介面, 提供給所有的人使用, 那該有多好! 幸好C++早就想到這一點了, 提供了"private:"以及"public:"這兩個敘述供我們使用 請看改進後的子彈類別: class _Bullet//這是一個 "子彈" 的 "模子",改用class來寫 { private://私用成員(販賣機的內部),除了類本類別的函式成員外 int x; //,其他人皆不得存取或呼叫之 int y; void show(int x,int y);//這是一個函式,可以在(x,y)上畫出一點, public://公用成員(販賣機的操作方法),提供給所有的人使用 void go()//這是一個函式,呼叫他可以讓子彈往下移一格並畫出來 { show(x,y); y++; } int collision(int X,int Y)//這是一個函式,用來判斷子彈是否撞到東西 { return (x==X && Y==y); //不是則傳回0 } }; "private:"是一個C++的敘述, 他告訴編譯器說:以下的東東只能在這個類別內使用, 也就是只能給類別的函式成員使用, 因此在類別的外面絕對不能存取或呼叫這些私有的變數或函式, 否則編譯器是會告訴你語法錯誤的! "public:"則相反, 他告訴編譯器說:以下的東東所有的人都可以使用. 程式可以在任何地方使用他! 這樣子一來,子彈就像販賣機一樣, 你只要知道怎麼操作他, 而不必知道他如何動作了, 也不怕會不小心存取到不該存取的東西, 而破壞掉子彈的運行, 因為一旦你想要這麼做, 編譯器會阻止你的!!!! 像這種把沒有必要公開的資訊隱藏起來, 在學術上叫做封裝,也稱為資訊隱藏,(要記起來喔!) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 主要是為了降低程式設計師的負擔, 減少錯誤的發生. 以前物件導向的觀念還沒流行以前, 程式師要背負的一個很大的包袱就是要牢牢的記住所有變數的名稱及功用,(有幾千個喔!) 因為所有的變數都是隨時隨地可以存取的, 一個對變數錯誤的存取, 就會造成整個系統難以除錯的BUG!! 而物件導向將沒有必要公開的資料與函式封裝起來, 可以大大減少這種錯誤的發生, 況且, 資訊隱藏可以讓類別條理分明, 容易瞭解. 將外人不需要知道的東西包裝起來, 這樣子看的人(包括你自己喔!)可以很容易的知道在這個類別中 提供了哪些運作方法, 以及這個類別大概是做什麼的. 而不會被一大堆無用的資訊搞得一頭霧水, 浪費寶貴的時間和精力! OK!或許你現在還不能很瞭解資訊隱藏的好處! 不過相信我, 當你開始寫物件導向程式的時候, 你就會感謝C++的!!! class 和 struct 有什麼差別呢? 我終於可以告訴你了!!! 他們差在struct的是內定為"public:"的, 而class是內定為"private:"的, 也就是說 void main() { class _Bullet { int x,y; }; _Bullet Bullet; Bullet.x=5; //這一行不合法!!!!! }; 這樣瞭解嗎? <5> 函式複載 再出一個題目給大家, 寫一個函式,名為max 可以接收兩個整數,傳回比較大的那一個 一樣大就隨便傳一個 你會怎麼寫呢? 給你兩分鐘...... 一定要想喔!!! OK!想必大家都是這樣寫: int max(int a,int b) { if(a>b) return a; else return b; } 很好!再出一個題目給大家: 假設上一個題目的函式已存在, 我現在想要一個功能一樣的函式, 但是是處理倍精度浮點數的(double), 你會怎麼寫呢? Ok!想必應該是這樣: double maxbouble(double a,double b)//注意!函式名不同了!這是C 的規則! { if(a>b) return a; else return b; } 嗯...很好很好.....嘿嘿!!!! 如果我還要一個處理長整數,一個處理字元,一個處理指標的呢? ................... 天啊! 光是命名就讓人昏倒,而且使用起來很不自然,不是嗎? 告訴你,其實你可以不要寫的那麼辛苦的, 上一個例子也可以寫成這樣: double max(double a,double b)//注意喔!名字和int max(~~)一樣喔! { if(a>b) return a; else return b; } "不會吧!不是不可以這樣寫嗎?" 哈哈!告訴你,這就是C++的新語法"函式複載" 或許你會問:"他怎麼知道我要用的是哪一個?" 放心吧!他知道的! 例如如果你呼叫:max(1,2) ; 他就會用處理整數的那一個 如果你呼叫:max(1.3,0.005); 他就會用處理浮點數的那一個 神奇吧! 複載在學術上又稱為靜態連結, 因為到底要呼叫那個函式其實在編譯的時候就決定了 另外還有一種叫動態連結的, 要等到執行的時候才能決定 這個我們以後再說. <6> 建構函式 一個物件在建立的時候, 有時候我們會希望能替他初始化, 例如說我們希望在建立子彈物件時, 若沒有指定初值, 例如:_Bullet Bullet; 則子彈剛建立的時候位置是(0,0), 若有指定初值, 例如:_Bullet Bullet(10,10) 則子彈的位置可以設為我們要的位置 則這樣子要怎麼做呢? C++提供了建構函式(constructor) 可以讓我們在物件剛建立的時候做一些事情, 將物件初始化, 請看下例: class _Bullet//這是一個 "子彈" 的 "模子",改用class來寫 { private://私用成員 int x; int y; void show(int x,int y);//這是一個函式,可以在(x,y)上畫出一點, public://公用成員 _Bullet(){x=y=0;}//無參數之建構函式 _Bullet(int X,int Y){x=X;y=Y;}//有兩個參數之建構函式 void go()//這是一個函式,呼叫他可以讓子彈往下移一格並畫出來 { show(x,y); y++; } int collision(int X,int Y)//這是一個函式,用來判斷子彈是否撞到東西 { return (x==X && Y==y); //不是則傳回0 } }; 其中_Bullet()是一個無參數的建構函式, 當物件以 _Bullet Bullet;的方法建立的時候, C++會自動的呼叫此函式來作物件的初始化, 而_Bullet(int X,int Y)則是一個有兩個整數參數的建構函式, 例如當物件以 _Bullet Bullet(2,17)來建立的時候, C++會自動的呼叫此函式來作物件的初始化, 要注意的是建構函式的名稱必須和類別名稱一樣, 建構函式不可以有傳回值(連寫void都不行) 結束時也最好不要用return;命令結束, 不然在某些編譯器上會有問題. 其他的事項都和一般類別內函式相同. <7> new 和 delete 在C 中,配置記憶體的方法最常用的就是malloc()和free()了! 當然在C++中,你一樣可以運用這兩個函式來配置記憶體. 但是使用這兩個函式來配置記憶體在C++中卻有一些不妥!! 因為malloc()雖然會配置記憶體給物件用, 但是卻不會呼叫物件的建構函式, 這樣子我們就不能再建立物件之初將其初始化了!!! 幸好C++想到了這點, 提供了兩個新的記憶體配置運算子----new 和 delete!! 他們的用法相當簡單! 請看: int *p=new int;//建立指標p,並配置一個int的記憶體給他 int *q; //和上面的效果一樣,但是先宣告後配置 q=new int; int *r=new int[10];//建立指標r,並配置10個int的記憶體給他 _Bullet *s=new _Bullet[10];//和上面一樣,只不過int換成_Bullet _Bullet *t=new _Bullet(1,2)[10];//和上面一樣,但指定了初值 delete p;//歸還p所配置的記憶體 delete q;//歸還q所配置的記憶體 delete [] r;//歸還r所配置的記憶體 delete [] s;//歸還s所配置的記憶體 delete [] t;//歸還t所配置的記憶體 值得注意的是, 使用new時, C++會自動的呼叫每一個物件的的建構函式. 例如在t中, *(t).x , *(t+1).x ,....*(t+9).x都會是1 *(t).y , *(t+1).y ,....*(t+9).y都會是2 使用delete時, 若當初配置的是單一物件則不要加括號, 若當初配置的是物件陣列的話則一定要加括號!!! 不然會有問題! 切記!切記! <8> 解構函式 有建構當然就會有解構啦!!! 有的時候我們會希望在物件被消滅的時候(例如被delete的時候) 能夠做一些措施. 而做這種做施的函式就叫解構函式.(destructor) 請看下例: class test { int *p; public: test(){p=new int;} //建構函式 }; void main() { test a; } 這個類別在建立時會配置一個int的記憶體給指標p, 但是當程式結束時並沒有將P所指到的記憶體歸還給系統, 這樣會造成一些BUG. 解決的方法是加入解構函式 請看: class test { int *p; public: test(){p=new int;} //建構函式 ~test(){delete p;} //解構函式 }; void main() { test a; } 這個新的版本中加入了解構函式, 當物件將要被毀滅時,(程式結束時,或物件被delete掉時) C++會自動執行解構函式,以妥善的進行善後工作 值得注意的是解構函式名為類別名前面再加上 ~ 符號而成 不可以有參數,也不可以有傳回值 不得複載!!! 函式中不可以有return;敘述. 否則可能會有問題!! <9> 繼承 讀過生物吧!!(又是一句廢話!!) 動物是一種生物, 靈長目又是一種動物, 人類又是一種靈長目, 李登輝是一個人類的個體. 向這種 "是一種" 的關係就叫繼承. C++是為了模擬世界而創的, 像這種繼承的機制當然是不會放過的啦! 請看我們的例子: 生物有一些生物的特性, 生物會繁殖, 會生長. 動物既然是一種生物, 當然也一定會繁殖會生長囉! 可是動物還加了自己的一些特性:動物會動(廢話!) 靈長目也是一種動物囉!! 所以動物該有的特性他都有囉! 可是靈長目他又多了一種自己的特性, 就是他有IQ囉! 同樣的,人類是一種靈長目, 所以人類會繁殖,會生長,會動,有IQ, 而且人類還多了自己的特性----人類有名字!!! 李登輝是個人類! 因此他有自己的名字:他叫李登輝, 有自己的IQ---雖然我不知道有多少 ,會動,會繁殖,也會生長!! 以上拉拉雜雜的一大堆以C++的方法來表示就是: class life //生物類別 { public: void gorw(){~~~};//命令生物"生長"的函式,內容先不管 void bear(){~~~};//命令生物"繁殖"的函式,內容先不管 }; class animal:public life //動物類別,繼承自生物,也就是說"動物是一種生物" { public: void move(){~~~};//命令動物"移動"的函式,內容先不管 }; class primates:public animal //靈長目,繼承自動物 { int IQ;//這是靈長目的IQ }; class human:public primates //人類,是一種靈長目 { char name[6];//名字,三個中文字 public: human(char *p){strcpy(name,p);}//設定名字的建構函式 }; void main()//程式開始 { human LDW("李登輝");//誕生李登輝並替他取名字(將name[]設為"李登輝") LDW.grow();//李登輝在"生長"(天天喝克寧,你就會長得跟"小樹"一樣喔!) LDW.move();//李登輝做"運動"(嘿休!嘿休!) LDW.bear();//李登輝在"繁殖"!!??%^#$%(18歲以下兒童請勿觀看) } 這個例子只是博君一笑, 事實上繼承的用處是很大的. 他可以讓程式更擬物化, 也可以讓程式具有重用性..... 我這樣說你可能會很模糊, 沒關係,等到我們開始實做程式的時候你自然就會瞭解了! <第一章結束> <待續> 小樹 ※ 來源:‧山水蘭陽資訊站 bbs.iljhs.il.edu.tw‧[FROM: dia38.iljhs.il.] .