摘要:消息方法和虛函數(shù)方法廣泛用作軟件接口,本文對它們的各自特點(diǎn)及在MFC和COM中的使用特點(diǎn)進(jìn)行了評述。消息方法一般適用性廣,但速度慢;虛函數(shù)方法速度快,但適應(yīng)性差。綜合兩者的特點(diǎn),提出了包含消息處理的虛函數(shù)方法和構(gòu)造內(nèi)部函數(shù)表的消息處理方法,該兩種方法在VC6.0中測試通過,便于在軟件設(shè)計時靈活選擇。
關(guān)鍵詞:接口;消息處理;虛函數(shù);MFC;COM
中圖分類號:TP311.5 文獻(xiàn)標(biāo)志碼:A 文章編號:1674-9324(2012)01-0073-03
在可擴(kuò)展的軟件體系中,常常在模塊之間設(shè)定一定的接口,通過接口來適應(yīng)未來的系統(tǒng)擴(kuò)充需要。狹義的接口是指只包含虛函數(shù)的類,這種類通常沒有數(shù)據(jù)成員;廣義的接口是指軟件模塊之間的聯(lián)結(jié)紐帶,該紐帶保證各模塊之間可互不干擾地獨(dú)立升級,便于大型軟件系統(tǒng)的多人并行開發(fā)。接口的定義方式有多種,如全局函數(shù)方式,類的成員函數(shù)方式,類的虛函數(shù)方式、消息方式等,其中靈活性較強(qiáng)、便于擴(kuò)展且常用的是消息方式和虛函數(shù)方式。在實(shí)際應(yīng)用系統(tǒng)中,Windows窗口系統(tǒng)就是采用的消息模式來擴(kuò)展視窗的功能,窗口程序開發(fā)框架MFC也是基于消息方法來架構(gòu)的1]。而從動態(tài)鏈接和嵌入(OLE)發(fā)展起來的ActiveX控件技術(shù)則是基于COM接口2],該接口建立在虛函數(shù)方法上。本文對這兩種接口進(jìn)行了比較,提供了簡單的教學(xué)測試用例,以便在實(shí)際開發(fā)中采用適合的接口模式。
一、通過消息函數(shù)提供接口
消息方法的接口是使用一個消息函數(shù)來處理所有可能的軟件功能擴(kuò)展,每一個功能對應(yīng)一個消息ID號,然后根據(jù)消息類別來解釋參數(shù)的含義,進(jìn)行相應(yīng)的處理。以下用簡單的例子來進(jìn)行說明,比如,先設(shè)定幾種消息的編號,對應(yīng)不同的功能:#define MODE_1 1;#define MODE_2 2,然后,編寫消息函數(shù)如下:void msgproc(int msg,int para){switch(msg){case MODE_1:printf("This is mode 1!");break;case MODE_2:printf("This is mode 2!");break;}}消息函數(shù)傳入一個消息定義值,另外預(yù)留一個參數(shù),通過對消息值的一一判斷,采取相應(yīng)的動作。在主程序中測試時,就可以用同一個消息函數(shù)調(diào)用不同的功能。msgproc(MODE_1,0);msgproc(MODE_2,0);以上例子很簡單,但說明了消息函數(shù)的工作原理,即根據(jù)消息值進(jìn)行分支處理。在Windows系統(tǒng)中,窗口的標(biāo)準(zhǔn)消息函數(shù)原型為:WindowProc(HWND,UINT,WPARAM,LPARAM);第一個參數(shù)為窗口句柄,第二個參數(shù)為消息編號,第三和第四個參數(shù)為預(yù)留的消息處理可能要用到的其它參數(shù),因此,Windows窗口的消息函數(shù)共4個參數(shù),每個參數(shù)的大小都是4個字節(jié)。不管是什么窗口界面程序,核心的處理函數(shù)就是該消息函數(shù)。在窗口增加新的命令時,消息類型都為WM_COMMAND,而參數(shù)WPARAM為該命令的編號ID,LPARAM為可能用到的其它參數(shù)。通過這種方法,可不斷增加新的命令,只要新命令的ID號與原有的不重復(fù),就可以在保持兼容的情況下不斷擴(kuò)展。從以上可以看出,軟件接口的消息模式的優(yōu)點(diǎn)是:①接口簡單,便于支持各種程序語言,便于跨語言擴(kuò)展;②可不斷擴(kuò)充新的消息類型,擴(kuò)展性強(qiáng),兼容性好。但消息方法在有以上優(yōu)點(diǎn)的同時,也具有以下缺點(diǎn):①消息函數(shù)要處理所有消息,在消息種類太多時,會使消息處理函數(shù)十分龐大。②消息函數(shù)的類型是固定的,能傳遞的參數(shù)有限,必須通過傳遞結(jié)構(gòu)指針或全局?jǐn)?shù)據(jù)的方式來傳遞其余參數(shù),過程繁瑣,安全性差;③消息處理函數(shù)一般需要對消息類型逐一進(jìn)行判斷,效率不高,在軟件功能模塊越來越多時,會產(chǎn)生反應(yīng)遲鈍的現(xiàn)象。為了提高處理速度,可以采用另一種接口方法,即虛函數(shù)方法。
二、通過C++的虛函數(shù)方法提供接口
C++語言具有面向?qū)ο蟮墓δ?,即設(shè)定一個虛擬基類(父類),然后子類從父類派生,但可以繼承同樣的虛擬接口,然后外部就可用父類的接口調(diào)用所有的子類,也就是所謂的面向?qū)ο蠓椒?。這實(shí)際上是在軟件模塊之間設(shè)定了一個規(guī)定的接口,達(dá)到先分離再組合,靈活擴(kuò)展軟件功能的目的。例如,設(shè)定一個父類為:class base {public:virtual void mode1()=0;virtual void mode2()=0;};通過設(shè)定一些接口為虛函數(shù),表示繼承的子類將具有同樣的接口形式,但具體的表現(xiàn)卻可以不同。如派生一個子類為:class child:public base {public:void mode1(){printf("This is mode 1!"); }void mode2(){printf("This is mode 2!"); }};外部調(diào)用時,就可以產(chǎn)生一個具體的子類,然后用父類的接口進(jìn)行調(diào)用:child r;r.mode1();r.mode2();如果有不同的子類,可以有不同的表現(xiàn),但調(diào)用形式完全一樣,這就達(dá)到了靈活擴(kuò)充系統(tǒng)功能的作用。使用時,先將子類轉(zhuǎn)換為父類,然后統(tǒng)一用父類的虛擬接口來調(diào)用,如:base* pBase = new child;pBase ->model1();pBase ->model2();delete pBase;需要注意的是,在釋放該對象時,如果統(tǒng)一用父類指針釋放,則需要將析構(gòu)函數(shù)也設(shè)置為虛擬函數(shù),才能正確地調(diào)用子類的析構(gòu)函數(shù)。如果不使用虛擬析構(gòu)函數(shù),則需要先轉(zhuǎn)換為子類的指針,才能安全析構(gòu)該子類。在虛函數(shù)的內(nèi)部實(shí)現(xiàn)中,一般是在內(nèi)存中構(gòu)造一個函數(shù)表,虛擬函數(shù)的調(diào)用是函數(shù)表的位置加上一個偏移值來確定的3]。這增加了一層間接性,同時也增加了一層靈活性。與函數(shù)調(diào)用相比,增加了一個根據(jù)偏移值取函數(shù)地址的過程,該操作顯然比消息方法的逐一判斷要快得多。因此,虛函數(shù)方法的優(yōu)點(diǎn)是:(1)根據(jù)虛函數(shù)的位置取址后調(diào)用處理函數(shù),只需進(jìn)行一個索引操作,速度快;(2)支持繼承體系,同一接口可對不同的派生對象操作,而父類的虛擬函數(shù)接口保持不變。由于虛表的構(gòu)造是編譯器內(nèi)部處理的,因此又具有以下缺點(diǎn):(1)擴(kuò)充處理函數(shù)時不方便,由于需要對內(nèi)部的虛函數(shù)表進(jìn)行擴(kuò)充,一般需要重新編譯,不能做到二進(jìn)制上的即插即用;(2)由于各種語言的虛函數(shù)表的構(gòu)造方法并不統(tǒng)一,故難以進(jìn)行跨語言擴(kuò)展。為了將虛函數(shù)表的布局方法統(tǒng)一起來,微軟公司提出了二進(jìn)制上兼容的COM接口標(biāo)準(zhǔn),ActiveX控件就采用該標(biāo)準(zhǔn)。
三、提高虛函數(shù)的可擴(kuò)充性
有時候,父類的虛函數(shù)種類已經(jīng)固定了,添加新的虛函數(shù)很困難,容易引起兼容性問題。如何在這種情況下保持盡量多的擴(kuò)充性呢?這可以借鑒消息函數(shù)的方法。將一個虛函數(shù)設(shè)置成消息函數(shù)的形式,傳入消息類型和必要的處理參數(shù),就可以適應(yīng)未來的擴(kuò)充需要。如在父類中設(shè)置一個虛擬的消息函數(shù)形式:virtual void mode3(int msg,int para)=0;在子類中實(shí)現(xiàn)該函數(shù)為:void mode3(int msg,int para){switch(msg){case MODE_1:printf("This is extent mode 1!");break;case MODE_2:printf("This is extent mode 2!");break;}}可以看到,該函數(shù)與一般的消息函數(shù)是類似的。外部調(diào)用時,可以采用:r.mode3(MODE_1,0);r.mode3(MODE_2,0);
因此,為適應(yīng)軟件擴(kuò)充的需要,對于已經(jīng)較為固定的處理類型,可在父類中設(shè)定虛函數(shù)類型,子類中實(shí)現(xiàn)即可;而對于可能擴(kuò)充的功能,可用虛擬消息函數(shù)的形式保持接口,便于擴(kuò)展。
四、提高消息函數(shù)的處理速度
虛函數(shù)的接口方法天生具有速度優(yōu)勢,但使用虛函數(shù)需要確定函數(shù)類型,在系統(tǒng)初始設(shè)計時可能無法給定,如果采用虛擬消息函數(shù)的形式,則處理速度下降,體現(xiàn)不出速度優(yōu)勢。那么,在消息方法中,能否提高消息函數(shù)的處理速度呢?實(shí)際上,消息模式下也可構(gòu)造函數(shù)地址表,達(dá)到和虛函數(shù)方法相當(dāng)?shù)恼{(diào)用效率。首先將每種功能調(diào)用設(shè)置為函數(shù),如:void func1(){printf("This is mode 1!");}void func2(){printf("This is mode 2!");}然后定義函數(shù)類型:typedef void (*PFUN)();在消息處理函數(shù)內(nèi)構(gòu)造函數(shù)表,讓它和消息值對應(yīng)起來,根據(jù)消息值直接查找到函數(shù)指針,然后調(diào)用即可。void msgproc1(int msg,int para){static PFUN table]={0,func1,func2};tablemsg]();}而外部調(diào)用和通常的消息函數(shù)完全一樣:msgproc1(MODE_1,0);msgproc1(MODE_2,0);
在消息類型很多時,該方法明顯快于分支判斷方法,可以達(dá)到和虛函數(shù)方法同等級別的處理速度。但缺點(diǎn)是對消息值的取值有所限制,因?yàn)橐獙⑾⒅底鳛楹瘮?shù)表的索引值來使用。
五、MFC的消息映射方法分析
MFC是微軟開發(fā)的用于Windows界面程序設(shè)計的基本類庫[1],MFC中的消息映射機(jī)制沒有采用虛函數(shù),是否是因?yàn)樘摵瘮?shù)的空間代價呢?虛函數(shù)的實(shí)現(xiàn)需要占用內(nèi)存,一個類的虛表的大小就是虛函數(shù)的個數(shù)乘以一個指針的大小。假設(shè)Windows的通用消息有200個,用虛函數(shù)方法實(shí)現(xiàn)的話,視窗類的虛表就有200*4個字節(jié)= 800字節(jié),所有的視窗類的派生類都要承受800字節(jié)的代價。假設(shè)有100個類派生自視窗類,那么代價就是800*100字節(jié)也就是80K字節(jié)。在MFC2.0版本發(fā)布的時候是1992年,當(dāng)時個人臺式機(jī)的內(nèi)存才幾兆,這是值得考慮的因素了。由于虛表是和類綁定在一起的,而不是和類對象(也叫類的實(shí)例)綁定在一起的,類的實(shí)例僅增加一個指向該類虛表的指針而已。也就是說,如果你有100個視窗派生類,哪怕生成了10000個派生類的實(shí)例,虛表占用的內(nèi)存也是80K,但派生類的每個實(shí)例要保留指向虛表的指針,即4 字節(jié),因此增加了40 K字節(jié)的內(nèi)存使用。從這點(diǎn)來看,似乎MFC沒有采用虛函數(shù),內(nèi)存的確是一個考慮因素。但實(shí)際上,只要類具有虛函數(shù),就必然有指向虛表指針的內(nèi)存消耗,而MFC中的常用消息處理類都是具有虛函數(shù)的,所以該負(fù)擔(dān)并非由消息處理機(jī)制引起。因此可以說,MFC的消息機(jī)制沒有采用虛函數(shù),并非因?yàn)樘摵瘮?shù)的空間占用問題,著眼點(diǎn)應(yīng)該是側(cè)重于容易增加新的消息類型。如果使用虛函數(shù)機(jī)制來實(shí)現(xiàn)消息處理,對于每個可能的消息都必須在基類中定義一個虛函數(shù),而其首要的困難是無法猜測未來會出現(xiàn)什么消息,也無法確定需要定義什么樣的函數(shù)原型。而使用消息映射,解決這個問題則相對容易,因?yàn)檫@將由未來的程序設(shè)計者決定他們的消息該如何處理,所以消息函數(shù)方法的靈活性強(qiáng)于虛函數(shù)方法。當(dāng)然,在消息類型已經(jīng)固定、內(nèi)存充足的情況下,采用虛函數(shù)方法(或COM接口),則又是值得考慮的方案了。
六、COM接口的虛函數(shù)方法分析
COM技術(shù)作為Windows平臺上的組件對象模型2],為組件化程序設(shè)計提供了基礎(chǔ)平臺。它是在C++虛函數(shù)接口基礎(chǔ)上提出的二進(jìn)制標(biāo)準(zhǔn),只要符合COM規(guī)范,不僅可以用C++語言打造COM接口,也可用其它語言打造COM接口,這為軟件組件之間的交互提供了便捷高效的通道。以虛函數(shù)作為接口在功能擴(kuò)充時有一定困難,一般認(rèn)為:“接口一旦發(fā)布,不能修改”。其本質(zhì)問題在于C++以虛表加上偏移值的方式實(shí)現(xiàn)虛函數(shù)調(diào)用,而偏移值又是根據(jù)虛函數(shù)聲明的位置隱式確定的,這就造成了脆弱性。如果隨意增加新的虛函數(shù),造成虛表的排列發(fā)生變化,則現(xiàn)有的二進(jìn)制可執(zhí)行文件就可能調(diào)用到錯誤的函數(shù)。要擴(kuò)充新的接口而又不影響原有的二進(jìn)制文件,一種做法是:把新的虛函數(shù)放到接口聲明的末尾。這么做不夠優(yōu)雅,因?yàn)樾碌奶摵瘮?shù)與原有類似功能的函數(shù)不在一起;同時也很危險,因?yàn)樵擃惾绻焕^承,那么新增的虛函數(shù)會改變派生類中的虛表偏移值變化,會影響派生類的接口,造成在二進(jìn)制上不能兼容。COM 采用的較為安全的辦法是通過鏈?zhǔn)嚼^承來擴(kuò)展現(xiàn)有接口,通過派生得到新類,得到新類后,更改版本號,就可使用新的接口了。該法和前面方法實(shí)質(zhì)一樣,但沒有安全問題。這種帶版本的接口擴(kuò)充方法解決了二進(jìn)制兼容性的問題,客戶端源代碼也不受影響。帶版本的接口擴(kuò)充解決了兼容性問題,但也造成管理上的困難。因?yàn)槊看胃膭佣家肓诵碌慕涌陬?,管理起來很麻煩。如果我們能直接原地擴(kuò)充接口類,就不會同時管理如此多的接口類。在Windows和Linux系統(tǒng)中,核心功能接口一直在擴(kuò)充,它保持兼容性的辦法很原始,就是給每個系統(tǒng)調(diào)用賦予一個固定的數(shù)字代號,等于把虛函數(shù)表的排列固定下來,因此只要不與已有功能沖突,可以隨時添加新的功能。這與消息函數(shù)的快速處理方法是一樣的,也就是自己構(gòu)造虛表,實(shí)現(xiàn)上更底層一些,擴(kuò)充接口時只需聲明新的功能號,適合于擴(kuò)充范圍較有限的情況。純粹從接口使用方面來說,通過名稱來調(diào)用接口更靈活一些,這就像使用Internet域名比使用 IP地址更普遍一樣。虛函數(shù)是通過虛表加上偏移值進(jìn)行調(diào)用,而非虛函數(shù)是通過名稱進(jìn)行綁定,通過內(nèi)部決議名把可執(zhí)行文件鏈接到一起,因此擴(kuò)充時不會影響已有函數(shù)。因此非虛函數(shù)比虛函數(shù)更健壯,但困難是必須有一個編譯和鏈接的過程,而且內(nèi)部名稱在不同語言中可能不一致,所以難以做到在二進(jìn)制層面上即插即用。如果要采用跨語言的二進(jìn)制軟件模塊,則需要暴露C語言的接口,然后用動態(tài)鏈接庫的方式進(jìn)行調(diào)用即可,這也是Windows系統(tǒng)中最廣泛采用的二進(jìn)制軟件接口方法,即動態(tài)鏈接庫方式。
通過以上比較可以看出,消息模式的接口方法適用性更廣,而虛函數(shù)方法天生具有速度優(yōu)勢。因此,對于處理函數(shù)較為有限,固定且希望效率較高,可優(yōu)先選用虛函數(shù)方法。但如果希望不斷擴(kuò)展功能,且采用不同的開發(fā)語言,則用消息模式更有利一些。兩種方法可互相借鑒,在消息模式中可自定義函數(shù)表,使執(zhí)行速度達(dá)到與虛函數(shù)方法相當(dāng),而虛函數(shù)方法也可定義一個特殊的消息處理虛函數(shù),使之在接口固定的情況下,功能可不斷擴(kuò)充,或者遵循COM接口規(guī)范,通過新的接口類來擴(kuò)展功能。因此在實(shí)際軟件開發(fā)中,一般來說,如果開發(fā)語言單一,功能較為固定,速度要求高,則優(yōu)先選取虛函數(shù)方法;如果跨語言開發(fā),功能需不斷擴(kuò)充,而對速度要求不苛刻,則優(yōu)先選取消息函數(shù)方法。兩者的優(yōu)勢還可通過混合方式進(jìn)行互補(bǔ),以滿足實(shí)際需要。
參考文獻(xiàn):
[1]侯捷.深入淺出MFC(第二版)[M].武漢:華中科技大學(xué)出版社,2001.
[2]潘愛民.COM原理與應(yīng)用[M].北京:清華大學(xué)出版社,1999.
[3]Stanley B.Lippman.深度探索C++對象模型[M].侯捷,譯.武漢:華中科技大學(xué)出版社,2001.
作者簡介:徐明毅(1973-),男,重慶市人,副教授,主要從事水工結(jié)構(gòu)數(shù)值模擬研究和結(jié)構(gòu)分析軟件開發(fā)。