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.