精華區beta C_and_CPP 關於我們 聯絡資訊
C++物件導向程式語言簡明教程 <第一章 概論> 撰文:程式創作區小樹 使用編譯器:Turbo C++ 3.0 修改:lcr(小牛) 修改前言: 這篇小樹寫的 oop 概念蠻容易懂的, 蠻適合初學者看一下.. 只是筆者覺得寫的有點亂亂的 (^_^), 所以小弟在這整理了一下, 並沒有修改內容....請小樹哥哥見諒啦....:p Lcr (1996.10.13) 讀者須知: 本文章由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) //這是一個函式,用來判斷子彈│ │ { //是否撞到東西, 不是則傳回0│ │ return (x==X && Y==y); │ │ } │ │ }; │ │ │ │ _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) //這是一個函式,用來判斷子彈是否 │ │ { //撞到東西, 不是則傳回0 │ │ return (x==X && Y==y); │ │ } │ │}; │ └───────────────────────────────────┘ 其中_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歲以下兒童請勿觀看) } 這個例子只是博君一笑, 事實上繼承的用處是很大的. 他可以讓程式更擬 物化, 也可以讓程式具有重用性... 我這樣說你可能會很模糊, 沒關係,等到我 們開始實做程式的時候你自然就會瞭解了! <第一章結束> 小樹 -- ※ 發信站: 批踢踢實業坊(ptt.twbbs.org) ◆ From: cherry.cs.nccu.