精華區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> 何謂物件導向 所謂的物件導向程式設計, 簡單的說就是擬人化的程式設計. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 也就是說我們所設計的不再是一個個的函式, 而是一個個有生命力的物件. 其實各位可以把物件想成一個個不同職業的人, 物件的成員函式就是要求這個 人服務的管道. 而類別則是造人的模子! 我們用各種不同的模子作出各種不同職業的人, 然後藉由人與人的互相溝 通, 而架構起整個程式. 這樣說或許有點模糊, 但是等你看完整個教程再回來 看這一段, 相信你一定會發出會心的一笑. <2> 事件驅動 接下來我們要實際的做一個物件導向的程式, 既然要做程式, 就必須要有 題目. 事件驅動核心是一個很能充份表達物件導向精神的題目, 而且又不會太 難, 所以我們決定以它為這個教程的範例. 那麼什麼是事件驅動呢? 相信寫過Windows的程式的讀者必不陌生. 因為 Windows本身就是一個事件驅動的核心!!! 以前我們在寫程式時, 程式必須主動 的查詢電腦的各種狀況, 例如鍵盤是否被按下去啦?! 滑鼠是否移動了啦?!特定 的時間間隔是否到了啦?! 像以上我們所說的這些東西就稱為事件!! 以從前的 作法, 我們所寫的程式必須主動不斷的去查詢事件是否發生, 若發生,則依發生 事件的種類分去處理. 這是一種很複雜的作法.... 所謂的事件驅動式的程式設計, 就是有一個核心,(他是一個物件,也就是一 個"人",名字叫"Kernel"好了!! ) 他會主動去偵測事件是否發生, 若有事件發生, 則通知專門處理這個事件的人(物件)來處理(可能不只一個喔!!) 也就是說, 整個 程式只有核心是主動的, 其他的部份各司所職, 平常時並不動作, 只有當與自己 有關的事件發生時才會被核心通知去處理. 這樣子程式會很有條理, 有問題時也 可以很容易的查出錯誤在哪裡. 而我們要架構的,就是這這樣的一個核心. <3> 萬物的起源--基本類別 人是一種動物, 動物是一種生物, 生物是一種物體, 所以人也是一種物體. 所有的東西都是一種物體. 由上可知, 在自然界存在有一種"最基本類別" 一切 類別都是他的子類別, 在C++當然也不例外. 通常這個最基本類別會叫做"Object", 也就是物體的意思. 不過這個字太常用了, 很多類別庫都用這個名字. 為了避免 與他們衝到, 我們把我們的最基本類別稱作"_Thing", 請看_Thing的程式碼:(存於 thing.h中): ┌──────────────────────────────────┐ │#ifndef _THING_ //防止重複include用 │ │#define _THING_ │ │ │ │#include "class.h" │ │class _Thing │ │{ │ │ public: │ │ _Thing(){} //建構函式 │ │ virtual ~_Thing(){} //解構函式,必須宣告為虛擬函式 │ │ //呼叫此函式則會傳回物件所屬類別的代碼 │ │ virtual unsigned Is(){return IAm_Thing;} │ │}; │ │ │ │#endif │ └──────────────────────────────────┘ 既然_Thing是最基本類別, 因此他有的特性萬物都會擁有. 因為萬物都是一 種_Thing.(萬物都是一種"東西"), 現在讓我們來看看他有哪些特性: <1> 建構函式:雖然是空的,不過擺著比較好看 <2> 解構函式:解構函式必須是虛擬函式,為什麼以後再說 <3> Is()函式:這是一個報姓名的函式,意思是問物件說你是哪一種人啊?(什麼類 別?) 於是物件會傳回一數值, 我們可由此數值以及class.h中的 記錄得知此物件是什麼類別生出來的 <4> class.h :這是一個含括檔,記錄所有類別的代碼,詳細內容如下: ┌──────────────────────────────────┐ │#ifndef _CLASS_ //防止重複include用 │ │#define _CLASS_ │ │ │ │const unsigned IAm_Thing=0; │ │const unsigned IAm_Letter=1; │ │const unsigned IAm_EventReceiver=2; │ │const unsigned IAm_Kernel=3; │ │const unsigned IAm_KeyBoard=4; │ │ │ │#endif │ └──────────────────────────────────┘ 最基本類別是寫物件導向程式一定要先定義的, 所以我們就先寫出來啦! 至於為什麼一定要定義呢? 原因有二: <1> 好看,整個類別繼承的很完整 <2> 方便做包含者類別 第二個原因一時之間很難解釋是什麼意思, 先不要管他, 我們繼續作下去便 是了!! <4> 設計類別種類及內容 我們來複習一下前面所講到的核心是什麼樣子: 所謂的事件驅動式的程式設 計, 就是有一個核心,(他是一個物件,也就是一個"人",名字叫"Kernel"好了!! ) 他會主動去偵測事件是否發生, 若有事件發生, 則通知專門處理這個事件的人( 物件)來處理(可能不只一個喔!!) 也就是說, 整個程式只有核心是主動的, 其他 的部份各司所職, 平常時並不動作, 只有當與自己有關的事件發生時才會被核心 通知去處理. 問題來了: 第一:Kernel怎麼知道當一事件發生時要通知誰呢? (想一下喔..) 所以必須先由想要要求事件處理權的人先向Kernel報到, 例如物件 LittleTree希望Kernel在鍵盤事件發生時通知他, 他必須先這樣做: Kernel.RequestKeyEvent(&LittleTree), 意思是告訴Kernel說 LittleTree 希望Kernel在鍵盤事件發生時告訴LittleTree, 並把LittleTree的位址告訴 Kernel好讓Kernel通知他! 同理,若物件KenLin希望Kernel在滑鼠事件發生時通知他, 他必須先這樣 做:Kernel.RequestMouseEvent(&KenLin) 意思是告訴Kernel說KenLin 希望 Kernel在滑鼠有動作時告訴KenLin, 並把KenLin的位址告訴Kernel好讓Kernel 通知他! 先說明一點:Kernel一開始是不活動的!! 也就是說就算事件發生了,Kernel也會置之不理!! 因為程式執行之初是專門讓各事件處理人向Kernel報到用的! 當一切處理就緒,呼叫Kernel.Go()!! (意思是說Kernel,開始幹活吧!) Kernel便開始活動!!此時一旦有事件發生, Kernel便會依照事件的種類通知事前曾登記過的事件處理人. 第二:Kernel要怎麼做出來呢? (想一下喔..) 當然是用模子(類別)做出來的啊!! Kernel的模子叫做 _Kernel. 還記得 先前講過的_Thing類別嗎? _Kernel是一種"東西", 所以_Kernel也是一種 _Thing啦!!! 換句話說,_Kernel是繼承自_Thing的! 以C++的表示法如下: (存於Kernel.h檔中) ┌───────────────────────────────────┐ │#ifndef _KERNEL_ │ │#define _KERNEL_ │ │ │ │#include "Evrece.h" //先不要管這一行是做什麼的 │ │#include "thing.h" │ │#include "class.h" │ │class _Kernel:public _Thing │ │{ │ │ public: │ │ _Kernel(); //建構函式. │ │ virtual ~_Kernel(); //解構函式. │ │ virtual unsigned Is(){return IAm_Kernel;} │ │ void Go(); //叫Kernel工作的函式. │ │ void RequestKeyEvent(_EventReceiver *); //要求鍵盤事件的函式. │ │ void RequesrMouseEvent(_EventReceiver *); //要求滑鼠事件的函式. │ │}; │ │ │ │#endif │ └───────────────────────────────────┘ 而一個應用程式,要寫成下面的樣子: #include "Kernel.h" void main() { _Kernel Kernel; //用模子做出Kernel. ~~~~~~~; //做出其他的物件,並向Kernel申請事件的處理權! //此時Kernel並不工作. _Kernel.Go(); //Kernel開始工作啦!! } 第三:Kernel要如何詳細的告訴事件處理人所發生的事件的詳細情形呢? 比如說一 個鍵盤事件, 有可能是A鍵被按了,也可能是B, Kernel要如何告訴事件處理人 到底是那個鍵被按了呢? (想一下喔..) 當然是"寫信"告訴他啦!! 首先必須要有一個信信類別 _Letter, 此類 別必須能夠詳細記載各種可能的事件, 當事件發生時,Kernel先用_Letter類 別做出一封信, 然後在信中記載事件的詳細情形!! 再將信信傳給事件處理人 , 以下是Kernel的處理過程: _Letter Letter; //先做出一封信 ~~~~~~~~; //將所發生的事詳細的寫入信中 LittleTree.ReceiveLetter(Letter); //將信寄給事件處理人 讓我們來把_Letter類別做出來:(此檔存於Letter.h中) ┌──────────────────────────────────┐ │#ifndef _LETTER_ │ │#define _LETTER_ │ │ │ │#include "thing.h" │ │#include "event.h" │ │#include "class.h" │ │class _Letter:public _Thing //信也是一種"東西" │ │{ │ │ public: │ │ _Letter(){EventKind=EvNothing;Message=NULL;}; //建構. │ │ virtual ~_Letter(); //解構. │ │ virtual unsigned Is(){return IAm_Letter;} │ │ unsigned EventKind; //記載事件的種類. │ │ unsigned Key; //若是鍵盤事件則記錄所按之鍵值. │ │ unsigned MouseX,MouseY; //若是滑鼠事件則記錄滑鼠目前之位置. │ │ char *Message; //若是訊息事件則記錄訊息於此. │ │}; │ │ │ │#endif │ └──────────────────────────────────┘ 現在讓我們來看看他有哪些特性: <1> include "event.h": event.h中記載了所有事件的代碼, 請看: ┌─────────────────────────────┐ │ │ │ #ifndef _EVENT_ │ │ #define _EVENT_ │ │ │ │ const unsigned EvNothing=0; //沒事 │ │ const unsigned EvKeyDown=1; //鍵盤被按下去了! │ │ const unsigned EvKeyUp=2; //鍵盤被放開 │ 我們將僅做出鍵盤事件, 也就是說在我們的教程中Kernel只會偵察鍵盤事 件啦! 其他的事件, 若各位有興趣可以自己做做看! 第四:雖然我們已做出_Letter類別,可以寄信給專門處理事件的物件, 可是當初各 個物件向Kernel申請事件處理權的時候, 只留下位址而已,我們要如何根據此 位址來傳信給他呢? (想一想喔..) 以下的文章請一口氣看完, 瀏覽一遍後再回頭來詳細看會比較好瞭解! 所有能夠收信的物件(人)都是一種事件接收者-- _EventReceiver 我們這樣寫: (EvRece.h) ┌──────────────────────────────────┐ │#ifndef _EVENTRECEIVER_ │ │#define _EVENTRECEIVER_ │ │ │ │#include "class.h" │ │#include "thing.h" │ │typedef int Bool │ │class _EventReceiver:public _Thing //事件接收者也是一種_Thing │ │{ │ │ _EventReceiver(){} │ │ virtual ~_EventReceiver(); │ │ virtual unsigned Is(){return IAm_EventReceiver;} │ │ //這是收信信用的函式,成功的處理完所發生的事件後則傳回True,│ │ //否則傳回False │ │ virtual Bool ReceiveLetter(_Letter &){return 1} │ │}; │ │ │ │#endif │ └──────────────────────────────────┘ 基本上,_Eventreceiver和_Thing一樣,都是一種基本類別, 所謂的基本類別, 通常是不拿來直接定義物件的!! 而是拿來當作其他類別的父類別!!因為 _EventReceiver類別多了一個虛擬函式-- ReceiveLetter() 所以所有繼承自此類 別的類別所做出來的物件都會有接收信信的能力, 換句話說, 所有想要有收信信能 力的人都需要是一種_EventReceiver, 比如說Kernel不是一種_EventReceiver他當 然不能收信, 我這樣講你可能不懂, 讓我舉個例子: ┌───────────────────────────────────┐ │class _KeyEventReceiver:public _EventReceiver //鍵盤事件接收者類別 │ │{ │ │ public: │ │ //當鍵盤事件發生時Kernel藉由此函式通知他 │ │ virtual Bool ReceiverLetter(_Letter &Letter) │ │ { │ │ cout << Letter.Key; //此類別收到信信後的反應 │ │ return 1; │ │ } │ │}; │ │ │ │void main() │ │{ │ │ _Kernel Kernel; //生出Kernel │ │ _KeyEventReceiver LittleTree; //生出LittleTree │ │ Kernel.RequestKeyEvent(&LittleTree); │ │ //LittleTree向Kernel申請鍵盤事件處理權│ │ Kernel.Go(); //Kernel開始幹活吧! │ │} │ └───────────────────────────────────┘ 在Kernel.Go()中,Kernel到底是怎麼呼叫LittleTree的呢? 讓我們來看看新 的Kernel的程式碼: ┌───────────────────────────────────┐ │class _Kernel:public _Thing │ │{ │ │ unsigned KeyEventReceiverCount; │ │ //記錄有多少人申請鍵盤事件處理權 │ │ _EventReceiver *KeyEventReceiver[30]; │ │ //用來記錄所有申請鍵盤事件處理權的物件的位址,最多30人 │ │ public: │ │ _Kernel(){KeyEventReceiverCount=0;} //建構函式 │ │ virtual ~_Kernel(); //解構函式 │ │ virtual Is(){return IAm_Kernel;} │ │ void Go(); //叫Kernel工作的函式 │ │ void RequestKeyEvent(_EventReceiver *); //要求鍵盤事件的函式 │ │ void RequesrMouseEvent(_EventReceiver *); //要求滑鼠事件的函式 │ │ _Letter Letter; //做出一封信 │ │}; │ │ │ │void _Kernel::RequestKeyEvent(_EventReceiver *Receiver) │ │{ │ │ KeyEventReceiver[KeyEventReceiverCount]=Receiver; │ │ KeyEventReceiverCount++; │ │} │ │ │ │void _Kernel:::Go() │ │{ │ │ while (1) │ │ { │ │ if(bioskey(1) != 0)//如果鍵盤事件發生 │ │ { │ │ Letter.EventKind=EvKeyDown; │ │ //將信中的事件種類改成鍵盤事件 │ │ Letter.Key=bioskey(0);//由鍵盤讀入一鍵 │ │ for(unsigned i=0;i<KeyEventReceiverCount;i++) │ │ KeyEventReceiver[i]->ReceiveLetter(Letter);//送信的動作 │ │ //將此信傳給所有鍵盤事件處理人 │ │ } │ │ } │ │} │ └───────────────────────────────────┘ 第五: 要怎麼讓Kernel停止工作呢? (想一想..) 任何物件只要呼叫Kernel.End()就可以停止核心的工作了! 可是這樣會有 兩個問題: 1.Kernel是怎麼把自己停下來的呢? 2.各物件又不知道Kernel的位址,要怎麼呼叫他呢? 我們先解決第二個問題: 其實很簡單, 只要在_Letter類別中記錄下核心的 位址就好了啊! ┌──────────────────────────────────┐ │class _Letter:public _Thing //信也是一種"東西" │ │{ │ │ public: │ │ _Letter(){EventKind=EvNothing;Message=NULL;}; //建構 │ │ virtual ~_Letter(); //解構 │ │ virtual unsigned Is(){return IAm_Letter;} │ │ unsigned EventKind; //記載事件的種類. │ │ unsigned Key; //若是鍵盤事件則記錄所按之鍵值. │ │ unsigned MouseX,MouseY; //若是滑鼠事件則記錄滑鼠目前之位置│ │ char *Message; //若是訊息事件則記錄訊息於此. │ │ _Kernel KernelAddress; //這是記錄核心的位址用. │ │}; │ └──────────────────────────────────┘ 如此一來,收到信信的事件處理人就可以根據Kernel的位址跟Kernel溝通了. 至於第一個問題,請看更新後的程式碼: ┌────────────────────────────────────┐ │ class _Kernel:public _Thing │ │ { │ │ unsigned KeyEventReceiverCount; //記錄有多少人申請鍵盤事件處理權. │ │ _EventReceiver *KeyEventReceiver[30]; │ │ //用來記錄所有申請鍵盤事件處理權的物件的位址,最多30人. │ │ int EndFlag; //多了這一個結束旗標 │ │ │ │ public: │ │ _Kernel(){KeyEventReceiverCount=0; │ │ EndFlag=0;Letter.KernelAddress=this;} //建構函式 │ │ virtual ~_Kernel(); //解構函式. │ │ virtual Is(){return IAm_Kernel;} │ │ void Go(); //叫Kernel工作的函式. │ │ void RequestKeyEvent(_EventReceiver *); //要求鍵盤事件的函式. │ │ void RequesrMouseEvent(_EventReceiver *); //要求滑鼠事件的函式. │ │ void End(); //多了這一個結束函式. │ │ _Letter Letter; //做出一封信. │ │ }; │ │ │ │ void _Kernel::RequestKeyEvent(_EventReceiver *Receiver) │ │ { │ │ KeyEventReceiver[KeyEventReceiverCount]=Receiver; │ │ KeyEventReceiverCount++; │ │ } │ │ │ │ void _Kernel:::Go() │ │ { │ │ while (1) │ │ { │ │ if(EndFlag==1) //若結束旗標為1則結束程式. │ │ break; │ │ if(bioskey(1) ==1) //如果鍵盤事件發生. │ │ { │ │ Letter.EventKind=EvKeyDown; //將信中的事件種類改成鍵盤事件. │ │ Letter.Key=bioskey(0); //由鍵盤讀入一鍵. │ │ //將此信傳給所有鍵盤事件處理人. │ │ for(unsigned i=0;i<KeyEventReceiverCount;i++) │ │ KeyEventReceiver[i]->ReceiveLetter(Letter); //送信的動作. │ │ } │ │ } │ │ } │ │ │ │ void _Kernel::End() │ │ { │ │ EndFlag=1; //將結束旗標設為1. │ │ Letter.EventKind=EvEnd; │ │ for(unsigned i=0;i<KeyEventReceiverCount;i++) │ │ KeyEventReceiver[i]->ReceiveLetter(Letter); │ │ //告訴所有的事件處理人程式即將結束,請做好善後準備.│ │ } │ └────────────────────────────────────┘ 上面的程式碼中,在建構函式的地方, 用到了this這個指位器! this是一個C++ 自己建立的指位器, 永遠指向現在正在操作的物件!! 當建構函式執行時, this當 然是指向Kernel啦! 所以把this拷貝一份給Letter.KernelAddress, 這樣 Letter.KernelAddress就能正確的指到Kernel了! <5> 編碼及範例 接下來是一個範例, 程式碼我都已經收錄在檔區了! 麻煩大家去抓下來看一看. 程式並不難,重要的是物件導向的觀念. 整個程式其實就是一個物件和物件間的溝通史! 看完就知道整個物件導向的程式是怎麼運作的了! 事實上一個事件驅動的核心絕對不 只這麼簡單. 要考慮的事情是很多的!! 有興趣的人可以參考旗標的"事件驅動程式設 計"這本書是古清德先生寫的! 裡面有更詳細的說明! 相信看完這整個教程, 您對物件 導向的程式設計方法會有多一層的瞭解. 期待再與您見面, Bye!Bye! 全篇完 小樹 -- ※ 發信站: 批踢踢實業坊(ptt.twbbs.org) ◆ From: cherry.cs.nccu.