VB 程序大揭祕(轉載)用VB的不得不看的好東西!

http://cnprogram.myrice.com/article/vb/vb396.html
程聯盟--技術文章

VB 程序大揭祕   
   

   1.Visual Basic程序概況

  我用W32Dasm(Ver 8.93)解開一個比較複雜的VB程序,其中用到了許多API 函數比如GetPrivateProfileString、OSfCreateShellLink、SHBrowseForFolder 等來自很多DLL的API。解開以後卻發現程序只用到了一個DLL:msvbvm50.dll(我用的還是VB5)!VC、Delphi等程序語言編譯出的程序可是直接引用DLL的。經過研究發現程序使用瞭如下幾個主要的來自MSVBVM50.dll的API:

rtcRandomize :Randomize 函數的對應API;
rtcMidCharVar :Mid 函數的對應API;
rtcLeftCharVar、rtcRightCharVar :看出來了吧,這些是Left、Right函數的對應API;
rtcUpperCaseVar :UCase 函數的對應API;
rtcKillFiles :Kill 語句的對應API;
rtcFileCopy :FileCopy 語句的對應API;
rtcFileLength :EOF、FileLen函數的對應API;
rtcGetTimer :Randomize Timer中獲取Timer的對應API;
rtcShell :Shell函數的的對應API;
rtcMakeDir :MkDir 語句的對應API;
rtcRemoveDir :RmDir 語句的對應API;
rtcDir :Dir 函數的對應API;
rtcSpaceVar :Space 函數的對應API;

  沒問題的人應該看出來了:VB的所有函數、語句、方法都是由調用MSVBVM50.dll 中的API實現的,一般是由“rtc”接上函數或語句的全名,涉及字符串的API一般還得在最後加上“Var”。另外還有一些函數是這樣寫的:

__vbaUbound : UBound 的對應API;
__vbaFileOpen :Open 語句的對應API;
__vbaStrCmp :比較兩個字符串:If String1 = String2 Then ......
__vbaVarOr :Or 運算符的對應API;
__vbaRedim :Redim 語句的對應API;
__vbaRedimPreserve :Redim 語句加上 Preserve 參數的對應API;
__vbaGet、vbaPut :Get、Put語句的對應API……

  在運行時,VB程序就調用它們完成工作。

  2.其它DLL的調用

  第一部分解決了。我們知道了VB程序實際上不是一個真正的可執行文件,它只是機械性地調用MSVBVM50.dll中的API執行程序。那麼VB程序既然只調用了MSVBVM50.dll,它又是怎樣調用其他DLL中的API呢?

  注意這個API。它能引起我們的注意:

  DllFunctionCall:看到了嗎?它就是我們的主角。

  從字面上看就能看懂了:它用來調用其它DLL。這樣可以使程序使用的函數集中在MSVBVM50.dll裏(怎麼有點像封建制度,中央集權……)。

  3.重中之重:VB程序的啓動

  我們已經知道了VB程序的運行方法。那麼它是怎樣啓動的呢?

  再看看程序調用的API。其中有一個API雷打不動,每個VB程序都有:

  ThunRTMain

  首先,VB程序調用ThunRTMain。ThunRTMain爲程序初始化進程,並獲取進程ID。

  隨後它加載vb5chs.dll,爲打開新窗口準備。然後它開始用LoadString等API 獲取窗口屬性,比如字體、標題、顏色等。再調用IMM32.dll,開始利用它打開新窗口。然後使用GetModuleFileName獲得VB程序名,隨後用CreateSemaphore增加信號機。信號機的作用是:當監控值大於0時,信號機工作。再調用OLE32.dll,使用CreateWindowEx打開一個叫做“DDE Server”的隱藏窗口,讓它從中作梗。退出OLE32.DLL,MSVBVM50又開始調用程序管理器。

  前面的工作爲我們的VB程序註冊了一個類名:VBFocusRT5,下面就可以使用這個類名創建VB窗體。首先使用大量循環讀取半角/全角字符,然後讀取各個控件的屬性,再使用Local_Function把這些屬性、方法、事件等“拼”成一個完整的控件,最後把上面做的所有工作綜合起來,開始VB程序。

  從過程來看,使用時間最多的自然是加載控件了,其次是加載字符集。VB程序速度慢主要是指啓動速度慢。這是難以避免的,希望VB7推出時能改進這一點。

  不知大家看沒看出來,編譯後的VB程序只是源程序的翻版,連控件屬性、方法和事件名都一模一樣。VB程序的慢就是來自這裏,它們只是機械地、無休止地調用MSVBVM50.dll裏的API來運行程序。要想徹底擺脫這一點,只能改革VB程序編譯時的方法,使其成爲一個標準的資源性Win32程序。

  附:VB程序與VC++程序啓動速度大火拼

  注意:這裏提到的只是“啓動”速度。實際上,VB程序啓動後的運行速度與其它程序語言編譯出來的EXE速度差不多(甚至更快),只不過是啓動速度太慢而已。

  我們知道,Windows附帶的計算器是用VC++編制的。我編了一個示例計算器程序,流程很簡單,單擊Command1時把Text1與Text2相加,再賦值到Text3。

  代碼只有一行:

  Private Sub Command1_Click()

  Text3 = CStr(Val(Text1) + Val(Text2))

  End Sub

  把它編譯爲EXE。爲了表現出速度差異,我選擇了一臺比較慢的電腦:

  Pentium 166 MMX + 80M EDO + 3.2G硬盤。

  啓動速度對比:爲了結果公平,共測試五次,取平均值。

單位:秒


運行次數 VB計算器 VC++計算器
1 2.43 0.87
2 0.85 0.74
3 0.92 0.92
4 1.02 0.78
5 0.87 0.84
平均速度 1.22 0.83

  你會發現,VB計算器第一次比較慢,剩下幾次就快了。這是因爲ThunRTMain 把所有控件信息寫入內存,每次打開程序時檢測是否有可用控件信息而且符合本程序(大概比爾也知道VB慢吧)。另外,我們只能算加法的計算器啓動速度就和功能衆多的Windows計算器差不多,更可以知道我們如果用VB編出一個和Windows計算器功能相同的計算器的啓動速度了。:( VB也不全是缺點,至少它的程序設計環境是其它程序語言所不具備或不擅長的。

  像VB這樣簡單易學,可以像畫圖一樣構造程序界面的程序語言可以說只有VB 一個,它爲編程初學者指明瞭方向。VB是有它存在的理由的,至少,我SuperAPI還在用它。:)

  後記:

  寫這篇文章的靈感來自於兩天前VB論壇裏cy72提出的問題。昨天半夜沒上網,集中精力調試一個VB程序,終於找出了答案。在DLL裏轉來轉去的感覺真的很難受,加之我對彙編還不太懂。以每秒3條語句的速度進行,調試了49218 步,共用了4個半小時。我自從接觸VB以來從沒感覺過VB程序是這樣複雜。

  尤其值得一提的是,4個半小時中4個小時是泡在近似無限的循環中,這種長時間重複一件枯燥而乏味的事情我可總算是見識到了,各位調試VB程序時大可不必心煩意亂,你只要想想長時間按着F7、F5鍵,在迎面撲來的一堆堆成山的天書般的彙編語言中尋找有用東西的滋味你就知道調試VB程序是最簡單 
VB到底爲我們做了什麼?
真是想不到系列之一:VB到底爲我們做了什麼?    AdamBear(原作)

關鍵字     VB、底層、WIN32、API、COM



《真是想不到系列》
    每次看大師的東西到了精彩之處,我就會拍案叫絕:"哇噻,真是想不到!"。在經過很多次這

種感慨之後,我發現只要我們動了腦筋,我們自己也能有讓別人想不到的東西。於是想到要把這些

想不到的東拿出來和大家一起分享,希望拋磚引玉,能引出更多讓人想不到的東西。

               真是想不到系列之一:VB到底爲我們做了什麼?
關鍵字:VB、底層、WIN32、API、COM
難度:中級
要求:熟悉VB,會用VC調試器,瞭解WIN32 SDK、COM。
   VB一直以來被認爲有以下優缺點:優點是上手快、開發效率高;缺點是能力有限,運行效率低。

這正是有些軟件把VB做爲首選語言,而有些軟件肯定不會用VB做的原因。而很多VC,DELPHI的程序

員都認爲VB裏搞開發不自由,它讓我們做事變容易的同時,也讓我們發揮的餘地越來越小。的確,

簡單和功能強大這兩者本身就是一對矛盾。那怕一行代碼不寫,僅僅起動運行一個空窗體這樣簡單

動作,VB在底下就爲我們做了大量複雜的工作(決不僅僅是註冊窗口類、顯示窗口、起動消息循環

這麼簡單),這些工作對程序員是透明的。我們在感謝VB開發小組對我們程序員體貼入微的同時,

不禁也要責怪爲什麼在文檔中對這些底層的動作隻字未提,雖然這些動作對最終的程序也許並無影

響,但我們擁有知情權,更何況這些動作有時的確會影響我們的工作(我將在本系列後面的《VB多

線程》中談到這種影響)。
   然而,所有希望從本文得到"未公開技術祕密"的朋友你將會很失望,因爲我能夠知道的和你一樣

多,我們所能做的一切就是站在外面來猜VB在裏面做了什麼?所以我決不是要帶大家一起去將VB反

向工程,而是想通過猜想VB的內部工作來將一些原來比較模糊的概念搞清楚。作爲一個系列的第一

篇文章,它的目的是爲了後面的深入打下基礎,所以我會在需要的時候指出我們必須掌握的知識點

,如果你不清楚,請及時地學習相關書籍來補課,具體見《參考書目》。
   最後,要聲明我在本文中所做的各種實驗和推斷僅是我個人的觀點,不能保證其正確性,並且不

承擔任何相關的法律責任。
   好,開始吧!首先準備好我們的武器,我下面要使用的工具主要有:VB6中文企業版+SP5(廢話

),還有SPY++、Dependency Walk和OLE Viewer(以下簡稱SPY和DEPEND和OLEVIEW,SPY在VB光盤的

common/tools/vb/下的SPY目錄中,OLEVIEW是其下OLETOOLS目錄中的OLEVIEW.EXE,注意其下還有一

個OLE2VW32.EXE功能類似,不過本文所指的是OLEVIEW.EXE,還Denpend在其下的Unsupprt/DEPEND裏

)。還要用用VC(上面提的工具在VC裏有),因爲我們還要看看VB生成的代碼,搞VB高級開發的朋

友一定要會用VC調試器,懂點彙編更好。當然,本文的重點不在這兒,所以沒有VC也不要緊。
    打開VB6新建一標準EXE工程,在"工程"->"引用"對話框裏應該已有四個引用,簡單點就是:1、

Visual Basic For Application(VBA) 2、VB運行時對象庫 3、VB對象庫 4、OLE自動化。前面三個

是任何VB工程都必須的,你想不要都不行,不信你試着去掉對它們的引用。那麼這三個核心類型庫

各有什麼用,在最終生成的可執行程序中扮演怎樣的角色,這是本文要分析的第一個問題。
   1)VB、VBA、VBS的區別你搞清楚了嗎?
   首先VBS不應該和VB、VBA放在一起比較,它是微軟按照自己定義的ActiveX Scripting規範完全

從頭開始寫成的腳本語言,雖然它的語法結構和VB非常相似,但VBS僅僅依靠自動化對象來擴充其功

能(只有後期綁定),它不能用implements來實現接口,不可能在VBS裏直接使用API,沒有VarPtr

這樣能得到指針的函數,而VBS缺少的這些功能正是VB和VBA所特有的。當然,這不是說VBS不如VB或

VBA,Windows已經爲VBS提供了足夠強大的功能,我們可以用VBS來做腳本COM組件,而且借自動化對

象的能力VBS可以說能力無限,所以有病毒用VBS來寫,對程序員來說VBS最重要的功能莫過於可以給

自己的軟件提供宏功能,就象VC中提供的VBS宏功能那樣。注意,VBS是Free的,這和在Office中使

用VBA來提供宏功能不同,要集成VBA需要價格不低的許可證費用,關於腳本語言可參見MSDN中

Platform SDK/Tools and Languages/Scripting。(在本系列後面的文章《腳本功能》中我會實做

一個用VBS來提供宏功能的小軟件)
   那麼VB和VBA又有什麼不同呢?好吧,眼見爲實,開始我們的實驗吧!
   如果裝了Office 2000以上版本,那麼打開OLEVIEW,點擊File下的View TypeLib查看位於

E:/Program Files/Common Files/Microsoft Shared/VBA/VBA6下的VBE6.dll的類型庫,再用同樣的

方法看看MSVBVM60.dll的類型庫,你會發現它們的類型庫基本上一模一樣,除了VBE6多了一個

VBEGlobal接口和實現這個接口的Global對象,這個Global對象我們也可以在VBA編程環境(比如用

WORD的VB編輯器)中用對象瀏覽器看到。它有二個方法Load和UnLoad,還有一個Userforms屬性,這

是因爲VBA6使用MS form 2.0 form設計器(FM20.dll)來設計和使用Userform窗體(而在VB6中,我們

可以使用多個設計器。比如通過使用MS form 2.0 form設計器,我們就能在VB中使用VBA所使用的

Userform用戶窗體)。和VBA的Global對象類似,在VB中也有GLobal對象,從VB的對象瀏覽器中可以

知道它在vb6.olb這個類型庫中,這個類型庫就是每個工程都必須引用的VB對象庫,所有的VB內置對

象都在這裏。而VBA的Userform中使用的對象都在FM20.dll中。
   除了上述不同外,VB和VBA還有一個最大的不同,就是VBA不能生成EXE可執行文件,但可以猜想

在IDE環境中VBA和VB都要把代碼編譯成p-code來執行,後面我將用實驗來證明的確是這樣,雖然在

具體的實現上VB和VBA有很大的不同。
   從上面的分析上可以看到VB和VBA還是有很大不同的,這種不同主要體現在編程環境和對象結構

上,但在本質上它們之間卻有着不可割捨的血源關係。如果剛纔你仔細地觀察了MSVBVM60.dll的類

型庫,你就會發現如下的片斷:
   // Generated .IDL file (by the OLE/COM Object Viewer)
   [
     dllname("VBA6.DLL",
     uuid(35BFBDA0-2BCC-1069-82D5-00DD010EDFAA),
     helpcontext(0x000f6ec4)
   ]
   module Strings {
       [entry(0x60000000), helpcontext(0x000f665f)]
       short _stdcall Asc([in] BSTR String);
       [entry(0x60000001), helpcontext(0x000f6e9f)]
       BSTR _stdcall _B_str_Chr([in] long CharCode);
    ……………
   }
   什麼?在MSVBVM60.dll中的對象其方法卻定義在VBA6.DLL中?!VB安裝目錄下不就有個VBA6.DLL

嗎?再用OLEVIEW看看它,哇噻,真是想不到它居然和MSVBVM60.DLL的一模一樣。怎麼回事?趕快再

拿出DEPEND來看看VBA6.dll、MSVBVM60.dll和VBE6.dll這三個DLL的輸出函數。哈,又有新發現,我

們可以發現在三個DLL的輸出函數中從編號512到717絕大部分都是一模一樣的一些以rtc開頭的函數

,比如595的rtcMsgBox(rtc是什麼?應該是Run Time Component? Control? Code?有誰知道嗎?)

,這說明三個DLL都有着相同的運行時VBA函數。
   我們再用DEPEND來觀察一下VB6.EXE, 我們可以發現VB6.EXE引入了VBA6.DLL中一些它特有的以Eb

和Tip開頭的函數,從這些函數的名稱上可以發現它們的功能都是IDE相關的,比如79的EbShowCode

和82的TipDeleteModule。VB6.EXE恰恰沒有引入任何rtc開頭的函數(注意一)。我們再來看看

MSVBVM60.DLL,隨便找一個用了MsgBox函數的編譯後的文件,用DEPEND來觀察它,就會發現它引入

MSVBVM60.DLL輸出的595號rtcMsgBox函數(注意二)。並且引入MSVBVM60.DLL中很多以下劃線開頭

的函數,比如__vbaVarAbs(注意三)。其實從這個三個"注意"中我們已經可以進行一些猜想,無論

對錯,你可以先想想。
   如果你沒有跟着我做實驗,而僅僅是看這篇文章的話,我猜想你應該有點昏了。如果你自己動手

做了這些實驗,現在你應該充滿了疑問而急侍看到結論。所以請一定要親手試一試,學習研究問題

的方法比看結論更重要。
   到這裏至少我們可以得出結論:VB和VBA本就是同宗的姐妹,只不過姐姐VB的功夫要比妹妹VBA歷

害些。不過姐姐只會單打獨鬥是女強人;妹妹卻只會傍大款。姐姐有生育能力,是真正的女人;妹

妹卻不會生崽,但深譜相夫之道,一番教導指揮之下可使她老公增色不少,而VBS呢,也是大戶人家

的女兒,不過沒有VB和VBA姐妹優秀的血統,嬌小玲瓏幹不得粗活只能指揮些自動聽話的對象來幹活

,她樂於助人品德好不象VBA那樣只認大款,VB、VBA、vbs三個女人我都喜歡。

   2)Native Code(本地代碼)到底做了什麼?
   打起精神,我們再深入一步。用OLEVIEW得到的類型庫還不能正確的反映各對象方法對應的DLL中

的函數入口,你應該已經發現用OLEVIEW得到的IDL文件中各個方法的entry屬性值都是0x600000XX這

樣的假東西。要得到類型庫中各方法在DLL中的真正入口,我們需要自己來寫段程序。
   即使在VB中我們也可以非常容易地獲取類型庫信息,再加上點COM初始化和調用代碼,我們就能

用自己的代碼實現VB6才引入的CallByName函數(在本系列後面的《Hack COM》中我會更深入談談

COM,作爲一名VB程序員對COM的理解非常重要)。由於本文的關鍵不是指導如何在VB裏使用類型庫

,所以下面提供的方法儘量從簡。
   新建一個標準EXE工程,添加對TypeLib Infomation的引用,在form中放一個名爲lblInfo的標籤

,然後添加如下代碼:
'程序1
Private Sub form_Load()
   Dim oTLInfo As TypeLibInfo
   Dim oMemInfo As MemberInfo
   Dim sDllName As String
   Dim sOrdinal As Integer
   
   Set oTLInfo = TLI.TypeLibInfoFromFile("MSVBVM60.DLL"
   
   lblInfo = "MATH模塊包含以下方法:" & vbCrLf
   
   For Each oMemInfo In oTLInfo.TypeInfos.NamedItem("Math".Members
       With oMemInfo
           .GetDllEntry sDllName, vbNullString, sOrdinal
           
           lblInfo = lblInfo & .Name _
               & "定義在" & sDllName & "中," _
               & "其編號爲" & sOrdinal _
               & vbCrLf
       End With
       
   Next
End Sub
   運行以後我們就可以知道MATH模塊中的Abs方法定義在VBA6.DLL中,其編號爲656。在DEPEND中查

看VBA6.DLL中編號爲656的函數,果然就是rtcAbsVar,用VBE6.DLL試試結果相同。
   還記得前面的注意一吧,VB6.EXE沒有引入rtc開頭的函數這說明在IDE環境中執行的VBA方法實際

上是通過COM調用VBA對象庫中的方法(跟蹤p-code是噩夢,所以我無法驗證它用的是什麼綁定方式

)。而注意二中提到的最終可執行程序中引入了rtcMsgBox,如我們所料最終的程序會直接調用它,

這要比COM調用快一點,但在跟蹤最終程序時,我發現rtcMsgBox內部卻是經過了二萬五千里長徵後

纔會去調用MessageBoxA這個API,其間有多次對其它對象的COM調用,慢!可能是因爲顯示的是模態

對話框,在多進程多線程環境有很多需要考慮的因素吧,如果你是瘋狂在意效率的程序員,你應該

試試用API來重寫MsgBox,絕對快不少。再來看看注意三,讓我們把以下的程序編譯成使用本地代碼

的"程序2.EXE"(爲了後面的實驗,可以在工程屬性的編譯選項卡中將它設成"無優化"和"生成符號

化調試信息"程序2.EXE""):
'程序2
Private Declare Sub DebugBreak Lib "kernel32" ()
Private Sub Main()
   Dim i As Long, j As Long
   Dim k
   i = &H1234
   DebugBreak
   k = 1234
   j = Abs(k)
   j = Abs(i)
   MsgBox "ss"
   j = VarPtr(i)
End Sub
   用DEPEND觀察"程序2.EXE",我們可以發現"程序2.EXE"並沒有如我們預期的一樣在引入595的

rtcMsgBox的同時引入656的rtcAbsVar,相反它引入了__vbaVarAbs和__vbaI4Abs,看看函數名就知

道一個針對的是Variant,一個針對的是long。這說明VB在最終生成的代碼中對象Abs這樣的可以進

一步針對不同類型優化的VBA函數進行了相應的處理,觀察一下所有以__vba開頭的函數絕大部分都

是那些最基本最常用的VBA函數,可以說__vba開頭的VBA函數是rtc開頭的VBA函數的優化版本,它們

基本上是VB開發小組重新寫的,絕大多數在函數內部實現自身功能,而rtc開頭的函數大多數是調用

COM服務對象來完成工作。從這麼多__vba開頭的函數上可以看出VB小組在Native Code(本地代碼)

的優化上下了不少功夫,這決對不是吹牛。它的確高度優化了不少科學計算相關的函數,以ABS爲例

Native Code要比p-code快4倍以上。但是並不是所有的計算函數都經過了這樣的優化,比如Rnd函數

,它就沒有對應的__vba開頭的優化函數,而是直接對應到rtcRandomNext函數上,雖然

rtcRandomNext也已經優化過,但內部依然用了COM調用,還是不如自己重寫的快,我不明白爲什麼

VB開發小組沒有考慮爲它寫一個對應的__vbaRnd。
   不要以爲上面的分析沒有意義,因爲我們可以從現象看本質,也可以從本質來解釋現象。比如我

們再做一個實驗,給你的代碼加入一個類模塊,你可以試試聲明一個和內部方法同名的公有的方法

(這是一個很有用的技術,在本系列後面的《錯誤處理》中我們會用到這種方法),比如我們可以

聲明一個Public Function Rnd(x) as single,同樣我們可以自己寫一個同名的MsgBox。但是你試

試能不能聲明一個Public Function abs(x) ,這時VB肯定會彈出一個莫名其妙的編譯錯誤提示框告

訴你"缺少標識符",這種錯誤發生在你的函數名和VB關鍵字衝突的時候。但是爲什麼同樣是MATH模

塊中的函數,abs是關鍵字,rnd卻不是,VB文檔裏是不會告訴你爲什麼的,但如果你認真的看了我

上面的實驗分析,我們就能猜想這是因爲VB對需要進一步優化的函數已經做了高度優化處理,VB開

發小組爲了保護他們的勞動成果,並顯示他們對自己優化技術的自信,而禁止我們重寫這些函數,

同時VB開發小組也承認還有些函數有待進一步優化,所以准許我們重寫之。在這裏我要提出一個偉

大的猜想:凡是能夠被重寫的函數就能夠被優化,就象凡是大於2的偶數就能夠被分解成兩個質因數

的和一樣。
   說到優化,還應該談談直接API調用和使用API類型庫的差別,還必須談談VB所使用的後端優化器

(和VC用的是一樣的優化器),還想談談如何盡最大可能來使用vTable綁定……(準備在本系列中

另寫一篇《優化》來談這些問題)。
   看了本地代碼,我們再來看看p-code,要是你看了MSDN中關於p-code的原理,你肯定會頭大。平

心而論p-code真是一個了不起的技術,代碼大小平均可以縮小50%。我們把程序2編譯成p-code看看

,還是用DEPEND來觀察,發現它並沒有引入__vba開頭函數(沒有使用優化的VBA函數?),卻引入

了CallEngine這樣的東西(肯定是爲了調用p-code僞碼解釋引擎),而且和Native Code一樣都引入

了rtcMsgBox(編譯生成的p-code在調用MsgBox時應該比在IDE環境中運行的p-code快)。
   如果你迫不及待地運行了程序2,你就會發現它將彈出一個應用程序錯誤對話框,說程序發生異

常。別怕,這是因爲調用了DebugBreak這個API的緣故,這個API其實就是產生一個Int 3中斷,使得

我們能夠中斷程序執行。如果你裝了VC這樣的支持即時調試的調試器,你可以在錯誤對話框中點擊"

取消",這樣可以起動調試器來調試程序。我就是這樣跟蹤程序運行的。如果你想看看VB生成的程序

反彙編代碼可以自己試試,我們可以用同樣的技術在VB或VBA的IDE中來中斷程序執行,比如我們完

全可以在Word的VB編輯器中運行上面程序2的代碼,從而中斷於Word的進程中,並可觀察到VBA生成

的p-code代碼。比如VB和VBA在IDE中生成的p-code代碼就會發現它們這間有很大的不同。
   所以,IDE中運行的程序和最終生成的程序是完全不同的。用SPY++看看你在IDE中運行的窗體,

你會發現它在VB的主線程下,也就是說在IDE中你用程序做出的窗體和VB IDE工作窗口一樣屬於VB

IDE,你的程序在IDE中運行時申請的資源也屬於VB IDE。有些程序在IDE中運行會讓IDE死掉(在VB5

中寫純API多線程就千萬別在IDE中運行,定死無疑,相比之下VB6的IDE健壯得多)。還有些程序可

能在IDE中能正常工作,但生成EXE後就工作不了。總之,在寫系統程序時要考慮到這種不同可能引

起的問題。
   
   3)VB的編譯技術,要我怎麼誇你,又要我怎麼罵你。
   看了上面對Native Code的高度評價,你可能會對VB做出的東西更有信心了,腰板更直了。是的

,作爲VB程序員沒有什麼需要害羞的,一個功力深厚的VB程序員理應拿比普通VC程序員更多的工資

,因爲他的生產力是VC程序員的好幾倍,而做出的程序在質量上和VC做的相差無幾。
   甚至有大師開玩笑說VB的內置對象就是用VB寫出的,比如我們可以自己寫form.cls、Label.ctl

,呵呵,我們還真不能排除這種可能性(雖然用VB不可能直接生成vb6.olb)。如果真是這樣,看來

VB小組自己都對自己的編譯優化技術非常有信心。
   實際上我們看看VB安裝目錄下的C2.exe的屬性,再看看VC的C2.DLL的屬性,就會發現它們是同一

個東西,同樣Link.exe也是VC的,所以我們完全可以對VB程序的後端優化編譯器以及聯結放心了。

它們根本就是VC開發小組東西,或者VB、VC都是同一個編譯器開發小組在做編譯模塊。總之,我們

可以壯着膽說我們VB做的程序其二次優化和聯結用的是和VC一樣的技術,嘿嘿,你有的我也有,我

有的你沒有的(純屬詭辯)。
   還有,沒有任何編譯器比VB編譯器更快,因爲在IDE中VB就是一種解釋型語言,這纔是VB開發效

率高的關鍵,快得幾乎感覺不得編譯過程。其請求時編譯,後臺編譯技術更是一隻獨秀,厲害啊!

想想看,別的語言的程序員有多少時間花在了等待代碼編譯和重新聯結上啊!
   不要高興得太早,因爲最終的目的還是要生成可執行文件。在VB中沒有分塊編譯和增量聯結的功

能,VB在生成可執行程序時總是編譯所有模塊並完全重新聯結,而在別的編譯語言中我們可以僅編

譯最近修改過的文件(分塊編譯),聯結時將新生成的代碼附在可執行程序的後面,並將原來的代

嗎標記爲作廢(增量聯結,最終的可執行程序會越來越大,但聯結時間大大縮短)。做實驗看看,

會發現在VB中每次生成可執行文件所花時間都是相同的。我不知VB開發小組爲什麼不提供分塊編譯

和增量聯結的功能,可能VB開發小組認爲生成可執行文件在VB中不是經常要做的工作。但是實際上

這種理由是說不過去的,因爲如前面所說IDE中運行程序和最終程序有很大不同,如我們要經常編譯

出可執行文件才能真正對它進行Profile,又如我們要調試多線程程序不能在VB IDE中做,在這些情

況下每次修改後都要重新生成可執行文件,我們浪費了不少時間去編譯已編譯過的代碼,聯結已聯

結過的程序。我猜想這是因爲VB生成可執行程序時進行了全局優化,所以必須得全部重新編譯聯結

。但提供一個新的功能讓我們能夠生成不進行全局優化的可以分塊編譯的調試版本,對Vb開發小組

應該不是難事吧!(我有一個變通的解決方案,還在試驗中)
  在來看看VB6安裝目錄下的VBAEXE6.lib,怎麼只有1k大一點,可以猜想裏面應該不會有代碼,

多半是些象vTable這樣的函數地址跳轉表,或者是些全局常量,我也不知道。但至少說明VB可以用

靜態聯結庫了,爲什麼不把這個功能提供給我們,讓我們有更多的選擇。
   再做個實驗看看,做一個標準EXE工程,裏面只有一個標準模塊,模塊裏面只一個Sub Main,Sub

Main裏面什麼也沒有,將它生成爲EXE文件。看看,嚯,有16k多。你要是有時間跟蹤這個什麼也不

做的程序看看,就會知道它要做很多事,初始化Err和App對象,準備COM調用,準備VB、VBA對象庫

,甚至爲使用ActiveX控制也做了準備,嘿嘿,看服務多周到。你必須得用VB對象庫中的控制,不用

也不行。你再多找幾個EXE工程看看,有很多東西相同,都是一個模子做出的,而且你沒有選擇模子

自由。ActiveX工程也是一樣,都是Dual雙接口,你做的ActiveX控制都必須要躲在一個Extender

Object後面。是的,在VB裏有很多東西你沒有選擇的自由。如果需要這種自由要麼不用VB,要麼就

得采取一些未公開的非官方的古怪的技巧(本系列文章最重要的目的之一,就是介紹這樣的非官方

技巧)。
   這又到文章開頭說的,VB讓我們做事情變得容易的同時也讓我們失去了不少自由。在最終代碼的

生成上則也採取了公式化的做法。當然,我們應該全面地來看待這個問題,如同生產線上生產的東

西不一定比手工的精緻,羣養的家禽不如野味好吃的道理一樣,如果需要精緻的野味,意味着更多

的勞動和更大的成本,這和VB所追求的更容易更便宜的目標是相違背的。
   
   4)VB程序員也得有HACK精神。
   本文的最後這個標題是嚴重離題了,但我想在此爲本系列文章定下一個充滿HACK精神的基調。

HACK精神是什麼?沒有準確的定義,我的理解是:HACK精神 = 總想探尋未知領域的好奇心 + 凡事

總想知道爲什麼的研究欲 + 總想拿出自己的東西的創新精神 + 解決問題的耐心和恆心。 VB的程序

員也一樣需要這種精神。
    
    最後,我們都知道VB開發小組已經達上.NET的快車飛起來了,不能不說VB6以後再沒有VB的新版

本了。微軟已經用.NET爲我們劃出了新的圈子,VB.NET是這個新圈子裏的新產物。在圈子裏面我們

能夠飛得更高,但是圈子外面的天空更大,所以我依然樂意站在圈子外,虔誠地祈禱真正的VB7的誕

生,阿門。 
 《VB真是想不到系列》
    每次看大師的東西到了精彩之處,我就會拍案叫絕:"哇噻,真是想不到!"。在經過很多次這種感慨之後,我發現只要我們動了腦筋,我們自己也能有讓別人想不到的東西。於是想到要把這些想不到的東拿出來和大家一起分享,希望拋磚引玉,能引出更多讓人想不到的東西。
    
                  VB真是想不到系列之二:VB《葵花寶典》--指針技術
關鍵字:VB、指針、動態內存分配、效率、安全
難度:中級至高級
要求:熟悉VB,掌握基本的C,瞭解彙編,瞭解內存分配原理。
    想當年東方不敗,黑木崖密室一戰,僅憑一根繡花針獨戰四大高手,神出鬼沒,堪稱天下武林第一高手。若想成爲VB裏的東方不敗,熟習VB《葵花寶典》,掌握VB指針技術,乃是不二的法門。
    欲練神功,引刀……,其實掌握VB指針技術,並不需要那麼痛苦。因爲說穿了,也就那麼幾招,再勤加練習,終可至神出鬼沒之境。廢話少說,讓我們先從指針的定義說起。
    
    一、指針是什麼?
    不需要去找什麼標準的定義,它就是一個32位整數,在C語言和在VB裏都可以用Long類型來表示。在32位Windows平臺下它和普通的32位長整型數沒有什麼不同,只不過它的值是一個內存地址,正是因爲這個整數象針一樣指向一個內存地址,所以就有了指針的概念。
    有統計表明,很大一部分程序缺陷和內存的錯誤訪問有關。正是因爲指針直接和內存打交道,所以指針一直以來被看成一個危險的東西。以至於不少語言,如著名的JAVA,都不提供對指針操作的支持,所有的內存訪問方面的處理都由編譯器來完成。而象C和C++,指針的使用則是基本功,指針給了程序員極大的自由去隨心所欲地處理內存訪問,很多非常巧妙的東西都要依靠指針技術來完成。
    關於一門高級的程序設計語言是不是應該取消指針操作,關於沒有指針操作算不算一門語言的優點,我在這裏不討論,因爲互聯網上關於這方面的沒有結果的討論,已經造成了佔用幾個GB的資源。無論最終你是不是要下定決心修習指針技術《葵花寶典》,瞭解這門功夫總是有益處的。
    注意:在VB裏,官方是不鼓勵使用什麼指針的,本文所講的任何東西你都別指望取得官方的技術支持,一切都要靠我們自己的努力,一切都更刺激!
    讓我們開始神奇的VB指針探險吧!

    二、來看看指針能做什麼?有什麼用?
    先來看兩個程序,程序的功能都是交換兩個字串:
【程序一】:'標準的做法SwapStr
    Sub SwapStr(sA As String, sB As String)
        Dim sTmp As String
        sTmp = sA: sA = sB: sB = sTmp
    End Sub

【程序二】:'用指針的做法SwapPtr
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _            (Destination As Any, Source As Any, ByVal Length As Long)

    Sub SwapPtr(sA As String, sB As String)
        Dim lTmp As Long
        CopyMemory lTmp, ByVal VarPtr(sA), 4
        CopyMemory ByVal VarPtr(sA), ByVal VarPtr(sB), 4
        CopyMemory ByVal VarPtr(sB), lTmp, 4
    End Sub

    你是不是以爲第一個程序要快,因爲它看着簡單而且不用調用API(調用API需要額外的處理,VB文檔明確指出大量調用API將降低程序性能)。但事實上,在VB集成環境中運行,程序二要比程序一快四分之一;而編譯成本機代碼或p-code,程序二基本上要比程序一快一倍。下面是兩個函數在編譯成本機代碼後,運行不同次數所花時間的比較:
運行100000次,SwapStr需要170毫秒,SwapPtr需要90毫秒。
運行200000次,SwapStr需要340毫秒,SwapPtr需要170毫秒。
運行2000000次,SwapStr需要3300毫秒,SwapPtr需要1500毫秒。
    的確,調用API是需要額外指令來處理,但是由於使用了指針技術,它沒有進行臨時字串的分配和拷貝,因此速度提高了不少。
    怎麼樣,想不到吧!C/C++程序員那麼依賴指針,無非也是因爲使用指針往往能更直接的去處理問題的根源,更有駕馭一切的快感。他們不是不知道使用指針的危險,他們不是不願意開衛星定位無級變速的汽車,只是騎摩托更有快感,而有些地方只有摩托才走得過去。
    和在C裏類似,在VB裏我們使用指針也不過三個理由:
    一是效率,這是一種態度一種追求,在VB裏也一樣;
    二是不能不用,因爲操作系統是C寫的,它時刻都在提醒我們它需要指針;
    三是突破限制,VB想照料我們的一切,VB給了我們很強的類型檢查,VB象我們老媽一樣,對我們關心到有時我們會受不了,想偶爾不聽媽媽的話嗎?你需要指針!
    但由於缺少官方的技術支持,在VB裏,指針變得很神祕。因此在C裏一些基本的技術,在VB裏就變得比較困難。本文的目的就是要提供給大家一種簡單的方法,來將C處理指針的技術拿到VB裏來,並告訴你什麼是可行的,什麼可行但必須要小心的,什麼是可能但不可行的,什麼是根本就不可能的。    

    三、 程咬金的三板斧   
    是的,程序二基本上就已經讓我們看到VB指針技術的模樣了。總結一下,在VB裏用指針技術我們需要掌握三樣東西:CopyMemory,VarPtr/StrPtr/ObjPtr, AdressOf. 三把斧頭,程咬金的三板斧,在VB裏Hack的工具。
1、CopyMemory
    關於CopyMemory和Bruce McKinney大師的傳奇,MSDN的Knowledge Base中就有文章介紹,你可以搜索"ID: Q129947"的文章。正是這位大師給32位的VB帶來了這個可以移動內存的API,也正是有了這個API,我們才能利用指針完成我們原來想都不敢想的一些工作,感謝Bruce McKinney爲我們帶來了VB的指針革命。
    如CopyMemory的聲明,它是定義在Kernel32.dll中的RtlMoveMemory這個API,32位C函數庫中的memcpy就是這個API的包裝,如MSDN文檔中所言,它的功能是將從Source指針所指處開始的長度爲Length的內存拷貝到Destination所指的內存處。它不會管我們的程序有沒有讀寫該內存所應有的權限,一但它想讀寫被系統所保護的內存時,我們就會得到著名的Access Violation Fault(內存越權訪問錯誤),甚至會引起更著名的general protection (GP) fault(通用保護錯誤) 。所以,在進行本系列文章裏的實驗時,請注意隨時保存你的程序文件,在VB集成環境中將"工具"->"選項"中的"環境"選項卡里的"啓動程序時"設爲"保存改變",並記住在"立即"窗口中執行危險代碼之前一定要保存我們的工作成果。

2、VatPtr/StrPtr/ObjPtr
    它們是VB提供給我們的好寶貝,它們是VBA函數庫中的隱藏函數。爲什麼要隱藏?因爲VB開發小組,不鼓勵我們用指針嘛。
    實際上這三個函數在VB運行時庫MSVBVM60.DLL(或MSVBVM50.DLL)中是同一個函數VarPtr(可參見我在本系列第一篇文章裏介紹的方法)。
其庫型庫定義如下:
        [entry("VarPtr"), hidden]
        long _stdcall VarPtr([in] void* Ptr);
        [entry("VarPtr"), hidden]
        long _stdcall StrPtr([in] BSTR Ptr);
        [entry("VarPtr"), hidden]
        long _stdcall ObjPtr([in] IUnknown* Ptr);   
    即然它們是VB運行時庫中的同一個函數,我們也可以在VB裏用API方式重新聲明這幾個函數,如下:
Private Declare Function ObjPtr Lib "MSVBVM60" Alias "VarPtr" _
(var As Object) As Long
Private Declare Function VarPtr Lib "MSVBVM60" _
(var As Any) As Long
(沒有StrPtr,是因爲VB對字符串處理方式有點不同,這方面的問題太多,在本系列中另用一篇《VB字符串全攻略》來詳談。
    順便提一下,聽說VB.NET裏沒有這幾個函數,但只要還能調用API,我們就可以試試上面的幾個聲明,這樣在VB.NET裏我們一樣可以進行指針操作。
    但是請注意,如果通過API調用來使用VarPtr,整個程序二SwapPtr將比原來使用內置VarPtr函數時慢6倍。)
    如果你喜歡刨根問底,那麼下面就是VarPtr函數在C和彙編語言裏的樣子:
    在C裏樣子是這樣的:
    long VarPtr(void* pv){
        return (long)pv;
    }
    所對就的彙編代碼就兩行:
    mov        eax,dword ptr [esp+4]
    ret        4           '彈出棧裏參數的值並返回。
    之所以讓大家瞭解VarPtr的具體實現,是想告訴大家它的開銷並不大,因爲它們不過兩條指令,即使加上參數賦值、壓棧和調用指令,整個獲取指針的過程也就六條指令。當然,同樣的功能在C語言裏,由於語言的直接支持,僅需要一條指令即可。但在VB裏,它已經算是最快的函數了,所以我們完全不用擔心使用VarPtr會讓我們失去效率!速度是使用指針技術的根本要求。
    一句話,VarPtr返回的是變量所在處的內存地址,也可以說返回了指向變量內存位置的指針,它是我們在VB裏處理指針最重要的武器之一。

3、ByVal和ByRef
    ByVal傳遞的參數值,而ByRef傳遞的參數的地址。在這裏,我們不用去區別傳指針/傳地址/傳引用的不同,在VB裏,它們根本就是一個東西的三種不同說法,即使VB的文檔裏也有地方在混用這些術語(但在C++裏的確要區分指針和引用)
    初次接觸上面的程序二SwapPtr的朋友,一定要搞清在裏面的CopyMemory調用中,在什麼地方要加ByVal,什麼地方不加(不加ByVal就是使用VB缺省的ByRef)
    準確的理解傳值和傳地址(指針)的區別,是在VB里正確使用指針的基礎。
    現在一個最簡單的實驗來看這個問題,如下面的程序三:
【程序三】:'體會ByVal和ByRef
    Sub TestCopyMemory()
        Dim k As Long
        k = 5
Note:   CopyMemory ByVal VarPtr(k), 40000, 4
        Debug.Print k
    End Sub
    上面標號Note處的語句的目的,是將k賦值爲40000,等同於語句k=40000,你可以在"立即"窗口試驗一下,會發現k的值的確成了40000。
    實際上上面這個語句,翻譯成白話,就是從保存常數40000的臨時變量處拷貝4個字節到變量k所在的內存中。
    現在我們來改變一個Note處的語句,若改成下面的語句:
Note2:   CopyMemory ByVal VarPtr(k), ByVal 40000, 4
    這句話的意思就成了,從地址40000拷貝4個字節到變量k所在的內存中。由於地址40000所在的內存我們無權訪問,操作系統會給我們一個Access Violation內存越權訪問錯誤,告訴我們"試圖讀取位置0x00009c40處內存時出錯,該內存不能爲'Read'"。
    我們再改成如下的語句看看。
Note3:   CopyMemory VarPtr(k), 40000, 4
    這句話的意思就成了,從保存常數40000的臨時變量處拷貝4個字節到到保存變量k所在內存地址值的臨時變量處。這不會出出內存越權訪問錯誤,但k的值並沒有變。
    我們可以把程序改改以更清楚的休現這種區別,如下面的程序四:
【程序四】:'看看我們的東西被拷貝到哪兒去了
    Sub TestCopyMemory()
        Dim i As Long, k As Long
        k = 5
        i = VarPtr(k)
NOTE4: CopyMemory i, 40000, 4
        Debug.Print k
        Debug.Print i
        i = VarPtr(k)
NOTE5: CopyMemory ByVal i, 40000, 4
        Debug.Print k
    End Sub

程序輸出:
5
40000
40000
    由於NOTE4處使用缺省的ByVal,傳遞的是i的地址(也就是指向i的指針),所以常量40000拷貝到了變量i裏,因此i的值成了40000,而k的值卻沒有變化。但是,在NOTE4前有:i=VarPtr(k),本意是要把i本身做爲一個指針來使用。這時,我們必須如NOTE5那樣用ByVal來傳遞指針i,由於i是指向變量k的指針,所以最後常量40000被拷貝了變量k裏。
    希望你已經理解了這種區別,在後面問題的討論中,我還會再談到它。

4、AddressOf
    它用來得到一個指向VB函數入口地址的指針,不過這個指針只能傳遞給API使用,以使得API能回調VB函數。
    本文不準備詳細討論函數指針,關於它的使用請參考VB文檔。

5、拿來主義。
    實際上,有了CopyMemory,VarPtr,AddressOf這三把斧頭,我們已經可以將C裏基本的指針操作拿過來了。
    如下面的C程序包括了大部分基本的指針指針操作:
    struct POINT{
        int x; int y;
    };
    int Compare(void* elem1, void* elem2){}
    void PtrDemo(){
    //指針聲明:
        char c = 'X';        //聲明一個char型變量
        char* pc; long* pl; //聲明普通指針
        POINT* pPt;          //聲明結構指針
        void* pv;            //聲明無類型指針
        int (*pfnCastToInt)(void *, void*);//聲明函數指針:
    //指針賦值:
       pc = &c;              //將變量c的地址值賦給指針pc
       pfnCompare = Compare; //函數指針賦值。
    //指針取值:
       c = *pc;              //將指針pc所指處的內存值賦給變量c
    //用指針賦值:
       *pc = 'Y'             //將'Y'賦給指針pc所指內存變量裏。
    //指針移動:
       pc++; pl--;
    }

    這些對指針操作在VB裏都有等同的東西,
    前面討論ByVal和ByRef時曾說過傳指針和傳地址是一回事,實際上當我們在VB裏用缺省的ByRef聲明函數參數時,我們已經就聲明瞭指針。
    如一個C聲明的函數:long Func(char* pc)
    其對應的VB聲明是:Function Func(pc As Byte) As Long
    這時參數pc使用缺省的ByRef傳地址方式來傳遞,這和C裏用指針來傳遞參數是一樣。
    那麼怎麼才能象C裏那樣明確地聲明一個指針呢?
    很簡單,如前所說,用一個32位長整數來表達指針就行。在VB裏就是用Long型來明確地聲明指針,我們不用區分是普通指針、無類型指針還是函數指針,通通都可用Long來聲明。而給一個指針賦值,就是賦給它用VarPar得到的另一個變量的地址。具體見程序五。
【程序五】:同C一樣,各種指針。
    Type POINT
        X As Integer
        Y As Integer
    End Type
    Public Function Compare(elem1 As Long, elem2 As Long) As Long
    '
    End Function
    Function FnPtrToLong(ByVal lngFnPtr As Long) As Long
        FnPtrToLong = lngFnPtr
    End Function
    Sub PtrDemo()
       Dim l As Long, c As Byte, ca() As Byte, Pt As POINT
       Dim pl As Long, pc As Long, pv As Long, pPt As Long, pfnCompare As Long
       c = AscB("X")
       pl = VarPtr(l)     '對應C裏的long、int型指針
       pc = VarPtr(c)     '對應char、short型指針
       pPt = VarPtr(Pt)   '結構指針
       pv = VarPtr(ca(0)) '字節數組指針,可對應任何類型,也就是void*
       pfnCompare = FnPtrToLong(AddressOf Compare) '函數指針
       CopyMemory c, ByVal pc, LenB(c)   '用指針取值
       CopyMemory ByVal pc, AscB("Y"), LenB(c) '用指針賦值
       pc = pc + LenB(c) : pl = pl - LenB(l)   '指針移動
    End Sub
    我們看到,由於VB不直接支持指針操作,在VB裏用指針取值和用指針賦值都必須用CopyMemory這個API,而調用API的代價是比較高的,這就決定了我們在VB裏使用指針不能象在C裏那樣自由和頻繁,我們必須要考慮指針操作的代價,在後面的"指針應用"我們會再變談這個問題。
    程序五中關於函數指針的問題請參考VB文檔,無類型指針void*會在下面"關於Any的問題"裏說。
    程序五基本上已經包括了我們能在VB裏進行的所有指針操作,僅此而已。

    下面有一個小測試題,如果現在你就弄懂了上面程咬金的三板斧,你就應該能做得出來。
    上面提到過,VB.NET中沒有VarPtr,我們可以用聲明API的方式來引入MSVBVM60.DLL中的VarPtr。現在的問題如果不用VB的運行時DLL文件,你能不能自己實現一個ObjPtr。答案在下一節後給出。

    四、指針使用中應注意的問題
    1、關於ANY的問題
    如果以一個老師的身份來說話,我會說:最好永遠也不要用Any!是的,我沒說錯,是永遠!所以我沒有把它放在程咬金的三板斧裏。當然,這個問題和是不是應該使用指針這個問題一樣會引發一場沒有結果的討論,我告訴你的只是一個觀點,因爲有時我們會爲了效率上的一點點提高或想偷一點點懶而去用Any,但這樣做需要要承擔風險。
    Any不是一個真正的類型,它只是告訴VB編譯器放棄對參數類型的檢查,這樣,理論上,我們可以將任何類型傳遞給API。
    Any在什麼地方用呢?讓我們來看看,在VB文檔裏的是怎麼說的,現在就請打開MSDN(Visual Studio 6自帶的版本),翻到"Visual Basic文檔"->"使用Visual Basic"->"部件工具指南"->"訪問DLL和Windows API"部分,再看看"將 C 語言聲明轉換爲 Visual Basic 聲明"這一節。文檔裏告訴我們,只有C的聲明爲LPVOID和NULL時,我們才用Any。實際上如果你願意承擔風險,所有的類型你都可以用Any。當然,也可以如我所說,永遠不要用Any。    
    爲什麼要這樣?那爲什麼VB官方還要提供Any?是信我的,還是信VB官方的?有什麼道理不用Any?
    如前面所說,VB官方不鼓勵我們使用指針。因爲VB所標榜的優點之一,就是沒有危險的指針操作,所以的內存訪問都是受VB運行時庫控制的。在這一點上,JAVA語言也有着同樣的標榜。但是,同JAVA一樣,VB要避免使用指針而得到更高的安全性,就必須要克服沒有指針而帶來的問題。VB已經盡最大的努力來使我們遠離指針的同時擁有強類型檢查帶來的安全性。但是操作系統是C寫的,裏面到處都需要指針,有些指針是沒有類型的,就是C程序員常說的可怕的void*無類型指針。它沒有類型,因此它可以表示所有類型。如CopyMemory所對應的是C語言的memcpy,它的聲明如下:
    void *memcpy( void *dest, const void *src, size_t count );
    因memcpy前兩個參數用的是void*,因此任何類型的參數都可以傳遞給他。
    一個用C的程序員,應該知道在C函數庫裏這樣的void*並不少見,也應該知道它有多危險。無論傳遞什麼類型的變量指針給上面memcpy的void*,C編譯器都不會報錯或給任何警告。
    在VB裏大多數時候,我們使用Any就是爲了使用void*,和在C裏一樣,VB也不對Any進行類型檢查,我們也可以傳遞任何類型給Any,VB編譯器也都不會報錯或給任何警告。
    但程序運行時會不會出錯,就要看使用它時是不是小心了。正因爲在C裏很多錯誤是和void*相關的,所以,C++鼓勵我們使用satic_cast<void*>來明確指出這種不安全的類型的轉換,已利於發現錯誤。
    說了這麼多C/C++,其實我是想告訴所有VB的程序員,在使用Any時,我們必須和C/C++程序員使用void*一樣要高度小心。
    VB裏沒有satic_cast這種東西,但我們可以在傳遞指針時明確的使用long類型,並且用VarPtr來取得參數的指針,這樣至少已經明確地指出我們在使用危險的指針。如程序二經過這樣的處理就成了下面的程序:
【程序五】:'使用更安全的CopyMemory,明確的使用指針!
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Long)
    Sub SwapStrPtr2(sA As String, sB As String)
        Dim lTmp As Long
        Dim pTmp As Long, psA As Long, psB As Long
        pTmp = VarPtr(lTmp): psA = VarPtr(sA): psB = VarPtr(sB)
        CopyMemory pTmp, psA, 4
        CopyMemory psA, psB, 4
        CopyMemory psB, pTmp, 4
    End Sub
    注意,上面CopyMemory的聲明,用的是ByVal和long,要求傳遞的是32位的地址值,當我們將一個別的類型傳遞給這個API時,編譯器會報錯,比如現在我們用下面的語句:
【程序六】:'有點象【程序四】,但將常量40000換成了值爲1的變量.
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, Length As Long)
    Sub TestCopyMemory()
        Dim i As Long,k As Long, z As Interger
        k = 5 : z = 1
        i = VarPtr(k)
        '下面的語句會引起類型不符的編譯錯誤,這是好事!
        'CopyMemory i, z, 4
        '應該用下面的
        CopyMemory i, ByVal VarPtr(z), 2
        Debug.Print k
    End Sub
    編譯會出錯!是好事!這總比運行時不知道錯在哪兒好!
    象程序四那樣使用Any類型來聲明CopyMemory的參數,VB雖然不會報錯,但運行時結果卻是錯的。不信,你試試將程序四中的40000改爲1,結果i的值不是我們想要的1,而是327681。爲什麼在程序四中,常量爲1時結果會出錯,而常量爲40000時結果就不錯?
    原因是VB對函數參數中的常量按Variant的方式處理。是1時,由於1小於Integer型的最大值32767,VB會生成一個存儲值1的Integer型的臨時變量,也就是說,當我們想將1用CopyMemroy拷貝到Long型的變量i時,這個常量1是實際上是Integer型臨時變量!VB裏Integer類型只有兩個字節,而我們實際上拷貝了四個字節。知道有多危險了吧!沒有出內存保護錯誤那只是我們的幸運!
    如果一定要解釋一下爲什麼i最後變成了327681,這是因爲我們將k的低16位的值5也拷貝到了i值的高16位中去了,因此有5*65536+1=327681。詳談這個問題涉及到VB局部變量聲明順序,CopyMemory參數的壓棧順序,long型的低位在前高位在後等問題。如果你對這些問題感興趣,可以用本系列第一篇文章所提供的方法(DebugBreak這個API和VC調試器)來跟蹤一下,可以加深你對VB內部處理方式的認識,由於這和本文討論的問題無關,所以就不詳談了。到這裏,大家應該明白,程序三和程序四實際上有錯誤!!!我在上面用常量40000而不用1,不是爲了在文章中湊字數,而是因爲40000這個常量大於32767,會被VB解釋成我們需要的Long型的臨時變量,只有這樣程序三和程序四才能正常工作。對不起,我這樣有意的隱藏錯誤只是想加深你對Any危害的認識。
    總之,我們要認識到,編譯時就找到錯誤是非常重要的,因爲你馬上就知道錯誤的所在。所以我們應該象程序五和程序六那樣明確地用long型的ByVal的指針,而不要用Any的ByRef的指針。
    但用Any已經如此的流行,以至很多大師們也用它。它唯一的魅力就是不象用Long型指針那樣,需要我們自己調用VarPtr來得到指針,所有處理指針的工作由VB編譯器來完成。所以在參數的處理上,只用一條彙編指令:push ,而用VarPtr時,由於需要函數調用,因此要多用五條彙編指令。五條多餘的彙編指令有時的確能我們冒着風險去用Any。
    VB開發小組提供Any,就是想用ByRef xxx As Any來表達void* xxx。我們也完全可以使用VarPtr和Long型的指針來處理。我想,VB開發小組也曾猶豫過是公佈VarPtr,還是提供Any,最後他們決定還是提供Any,而繼續隱瞞VarPtr。的確,這是個兩難的決定。但是經過我上面的分析,我們應該知道,這個決定並不符合VB所追求的"更安全"的初衷。因爲它可能會隱藏類型不符的錯誤,調試和找到這種運行時才產生的錯誤將花貴更多的時間和精力。
    所以我有了"最好永遠不要用Any"這個"驚人"的結論。

    不用Any的另一個好處是,簡化了我們將C聲明的API轉換成VB聲明的方式,現在它變成了一句話:除了VB內置的可以進行類型檢查的類型外,所以其它的類型我們都應該聲明成Long型。

    2、關於NULL的容易混淆的問題
    有很多文章講過,一定要記在心裏:
    VbNullChar 相當於C裏的'/0',在用字節數組構造C字串時常用它來做最後1個元素。
    vbNullString 這纔是真正的NULL,就是0,在VB6中直接用0也可以。
    只有上面的兩個是API調用中會用的。還有Empty、Null是Variant,而Nothing只和類對象有關,一般API調用中都不會用到它們。


    另:本文第三節曾提出一個小測驗題,做出來了嗎?現在公佈正確答案:
   【測驗題答案】
    Function ObjPtr(obj as Object) as long
Dim lpObj As Long
        CopyMemory lpObj, Obj, 4
        ObjectPtr = lpObj
    End Function    

    五、VB指針應用
    如前面所說VB裏使用指針不象C裏那樣靈活,用指針處理數據時都需要用CopyMemory將數據在指針和VB能夠處理的變量之間來回拷貝,這需要很大的額外開銷。因此不是所有C裏的指針操作都可以移值到VB裏來,我們只應在需要的時候纔在VB裏使用指針。
    1、動態內存分配:完全不可能、可能但不可行,VB標準    
    在C和C++裏頻繁使用指針的一個重要原因是需要使用動態內存分配,用Malloc或New來從堆棧裏動態分配內存,並得到指向這個內存的指針。在VB裏我們也可以自己
用API來實現動態分配內存,並且實現象C裏的指針鏈表。
     但我們不可能象C那樣直接用指針來訪問這樣動態分配的內存,訪問時我們必須用CopyMemory將數據拷貝到VB的變量內,大量的使用這種技術必然會降低效率,以至於要象C那樣用指針來使用動態內存根本就沒有可行性。要象C、PASCAL那樣實現動態數據結構,在VB裏還是應該老老實實用對象技術來實現。
    本文配套代碼中的LinkedList裏有完全用指針實現的鏈表,它是使用HeapAlloc從堆棧中動態分配內存,另有一個調用FindFirstUrlCacheEntry這個API來操作IE的Cache的小程序IECache,它使用了VirtualAlloc來動態分配內存。但實際上這都不是必須的,VB已經爲我們提供了標準的動態內存分配的方法,那就是:
    對象、字符串和字節數組
    限於篇幅,關於對象的技術這裏不講,LinkedList的源代碼裏有用對象實現的鏈表,你可以參考。
    字符串可以用Space$函數來動態分配,VB的文檔裏就有詳細的說明。
    關於字節數組,這裏要講講,它非常有用。我們可用Redim來動態改變它的大小,並將指向它第一個元素的指針傳給需要指針的API,如下:
    dim ab() As Byte , ret As long
    '傳遞Null值API會返回它所需要的緩衝區的長度。
    ret = SomeApiNeedsBuffer(vbNullString)
    '動態分配足夠大小的內存緩衝區
    ReDim ab(ret) As Byte
    '再次把指針傳給API,此時傳字節數組第一個元素的指針。
    SomeApiNeedsBuffer(ByVal VarPtr(ab(1)))
    在本文配套程序中的IECache中,我也提供了用字節數組來實現動態分配緩衝區的版本,比用VirtualAlloc來實現更安全更簡單。
     
    2、突破限制
    下面是一個突破VB類型檢查來實現特殊功能的經典應用,出自Bruce Mckinney的《HardCore Visual Basic》一書。
    將一個Long長整數的低16位作爲Interger型提取出來,
【程序七】 '標準的方法,也是高效的方法,但不容易理解。
     Function LoWord(ByVal dw As Long) As Integer    
        If dw And &H8000& Then       
            LoWord = dw Or &HFFFF0000    
        Else       
            LoWord = dw And &HFFFF&    
        End If
     End Function
【程序八】    '用指針來做效率雖不高,但思想清楚。
     Function LoWord(ByVal dw As Long) As Integer    
        CopyMemory ByVal VarPtr(LoWord), ByVal VarPtr(dw), 2
     End Function

     3、對數組進行批量操作
     用指針進行大批量數組數據的移動,從效率上考慮是很有必要的,看下面的兩個程序,它們功能都是將數組的前一半數據移到後一半中:
【程序九】:'標準的移動數組的做法
    Private Sub sh*tArray(ab() As MyType)
        Dim i As Long, n As Long
        n = CLng(UBound(ab) / 2)
        For i = 1 To n
            Value(n + i) = Value(i)
            Value(i).data = 0
        Next
    End Sub
【程序十】:'用指針的做法
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
           (ByVal dest As Long, ByVal source As Long, ByVal bytes As Long)
    Private Declare Sub ZeroMemory Lib "kernel32" Alias "RtlZeroMemory" _
           (ByVal dest As Long, ByVal numbytes As Long)
    Private Declare Sub FillMemory Lib "kernel32" Alias "RtlFillMemory" _
           (ByVal dest As Long, ByVal Length As Long, ByVal Fill As Byte)

    Private Sub sh*tArrayByPtr(ab() As MyTpye)
        Dim n As Long
        n = CLng(UBound(ab) / 2)
        Dim nLenth As Long
        nLenth = Len(Value(1))
        'DebugBreak
        CopyMemory ByVal VarPtr(Value(1 + n)), _
                    ByVal VarPtr(Value(1)), n * nLenth
        ZeroMemory ByVal VarPtr(Value(1)), n * nLenth
    End Sub
    當數組較大,移動操作較多(比如用數組實現HashTable)時程序十比程序九性能上要好得多。
    程序十中又介紹兩個在指針操作中會用到的API: ZeroMemory是用來將內存清零;FillMemory用同一個字節來填充內存。當然,這兩個API的功能,也完全可以用CopyMemory來完成。象在C裏一樣,作爲一個好習慣,在VB裏我們也可以明確的用ZeroMemory來對數組進行初始化,用FillMemory在不立即使用的內存中填入怪值,這有利於調試。
    4、最後的一點
    當然,VB指針的應用決不止這些,還有什麼應用就要靠自己去摸索了。對於對象指針和字符串指針的應用我會另寫文章來談,做爲本文的結束和下一篇文章《VB字符串全攻略》的開始,我在這裏給出交換兩個字符串的最快的方法:
    【程序十一】'交換兩個字符串最快的方法
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _            (Destination As Any, Source As Any, ByVal Length As Long)

    Sub SwapStrPtr3(sA As String, sB As String)
        Dim lTmp As Long
        Dim pTmp As Long, psA As Long, psB As Long
        pTmp = StrPtr(sA): psA = VarPtr(sA): psB = VarPtr(sB)
        CopyMemory ByVal psA, ByVal psB, 4
        CopyMemory ByVal psB, pTmp, 4
    End Sub
    對不起,爲了一點點效率,又用了Any!關於StrPtr,下一篇文章我會來談。
    自己來試試吧!欲練神功,趕快行動!    

《VB真是想不到系列》
    每次看大師的東西到了精彩之處,我就會拍案叫絕:"哇噻,真是想不到!"。在經過很多次這種感慨之後,我發現只要我們動了腦筋,我們自己也能有讓別人想不到的東西。於是想到要把這些想不到的東拿出來和大家一起分享,希望拋磚引玉,能引出更多讓人想不到的東西。
本系列文章可見:
    http://www.csdn.net/develop/list_article.asp?author=AdamBear

               VB真是想不到系列之三:VB指針葵花寶典之函數指針
關鍵字:VB、HCAK、指針、函數指針、效率、數組、對象、排序
難度:中級
要求:熟悉VB,瞭解基本的排序算法,會用VC更好。

引言:    
    不知大家在修習過本系列第二篇《VB指針葵花寶典》後有什麼感想,是不是覺得寶典過於偏重內功心法,而少了厲害的招式。所以,今天本文將少講道理,多講招式。不過,還是請大家從名門正派的內功心法開始學起,否則會把九陰真經練成九陰白骨爪。
    今天,我們重點來談談函數指針的實際應用。
    接着上一篇文章,關於字串的問題,聽CSDN上各位網友的建議,我不準備寫什麼《VB字符串全攻略》了,關於BSTR的結構,關於調用API時字串在UNICODE和ANSI之間的轉換問題,請參考MSDN的Partial Books裏的《Win32 API Programming with Visual Basic》裏的第六章《Strings》。今天就讓我們先忘掉字符串,專注於函數指針的處理上來。

    一、函數指針
    AddressOf得到一個VB內部的函數指針,我們可以將這個函數指針傳遞給需要回調這個函數的API,它的作用就是讓外部的程序可以調用VB內部的函數。
    但是VB裏函數指針的應用,遠不象C裏應用那麼廣泛,因爲VB文檔裏僅介紹瞭如何將函數指針傳遞給API以實現回調,並沒指出函數指針諸多神奇的功能,因爲VB是不鼓勵使用指針的,函數指針也不例外。
    首先讓我們對函數指針的使用方式來分個類。
    1、回調。這是最基本也是最重要的功能。比如VB文檔裏介紹過的子類派生技術,它的核心就是兩個API:SetWindowLong和CallWindowProc。
    我們可以使SetWindowLong這個API來將原來的窗口函數指針換成自己的函數指針,並將原來的窗口函數指針保存下來。這樣窗口消息就可以發到我們自己的函數裏來,並且我們隨時可以用CallWindowProc來調用前面保存下來的窗口指針,以調用原來的窗口函數。這樣,我們可以在不破壞原有窗口功能的前提下處理鉤入的消息。
    具體的處理,我們應該很熟悉了,VB文檔也講得很清楚了。這裏需要注意的就是CallWindowProc這個API,在後面我們將看到它的妙用。
    在這裏我們稱回調爲讓"外部調用內部的函數指針"。
    2、程序內部使用。比如在C裏我們可以將C函數指針作爲參數傳遞給一個需要函數指針的C函數,如後面還要講到的C庫函數qsort,它的聲明如下:
    #define int (__cdecl *COMPARE)(const void *elem1, const void *elem2)
    void qsort(void *base, size_t num, size_t width,
        COMPARE pfnCompare);
它需要一個COMPARE類型函數指針,用來比較兩個變量大小的,這樣排序函數可以調用這個函數指針來比較不同類型的變量,所以qsort可以對不同類型的變量數組進行排序。
    我們姑且稱這種應用爲"從內部調用內部的函數指針"。
    3、調用外部的函數
    也許你會問,用API不就是調用外部的函數嗎?是的,但有時候我們還是需要直接獲取外部函數的指針。比如通過LoadLibrary動態加載DLL,然後再通過GetProcAddress得到我們需要的函數入口指針,然後再通過這個函數指針來調用外部的函數,這種動態載入DLL的技術可以讓我們更靈活的調用外部函數。
    我們稱這種方式爲"從內部調用外部的函數指針"
    4、不用說,就是我們也可控制"從外部調用外部的函數指針"。不是沒有,比如我們可以加載多個DLL,將其中一個DLL中的函數指針傳到另一個DLL裏的函數內。
    上面所分的"內"和"外"都是相對而言(DLL實際上還是在進程內),這樣分類有助於以後我們談問題,請記住我上面的分類,因爲以後的文章也會用到這個分類來分析問題。

    函數指針的使用不外乎上面四種方式。但在實際使用中卻是靈活多變的。比如在C++裏繼承和多態,在COM裏的接口,都是一種叫vTable的函數指針表的巧妙應用。使用函數指針,可以使程序的處理方式更加高效、靈活。
    VB文檔裏除了介紹過第一方式外,對其它方式都沒有介紹,並且還明確指出不支持“Basic 到 Basic”的函數指針(也就是上面說的第二種方式),實際上,通過一定的HACK,上面四種方式均可以實現。今天,我們就來看看如何來實現第二種方式,因爲實現它相對來說比較簡單,我們先從簡單的入手。至於如何在VB內調用外部的函數指針,如何在VB裏通過處理vTable接口函數指針跳轉表來實現各種函數指針的巧妙應用,由於這將涉及COM內部原理,我將另文詳述。
    其實VB的文檔並沒有說錯,VB的確不支持“Basic 到 Basic”的函數指針,但是我們可以繞個彎子來實現,那就是先從"Basic到API",然後再用第一種方式"外部調用內部的函數指針"來從"API到BASIC",這樣就達到了第二種方式從"Basic 到 Basic"的目的,這種技術我們可以稱之爲"強制回調",只有VB裏纔會有這種古怪的技術。
    說得有點繞口,但是仔細想想窗口子類派生技術裏CallWindowProc,我們可以用CallWindowProc來強制外部的操作系統調用我們原來的保存的窗口函數指針,同樣我們也完全可以用它來強制調用我們內部的函數指針。
    呵呵,前面說過要少講原理多講招式,現在我們就來開始學習招式吧!
    考慮我們在VB裏來實現和C裏一樣支持多關鍵字比較的qsort。完整的源代碼見本文配套代碼,此處僅給出函數指針應用相關的代碼。    
    '當然少不了的CopyMemory,不用ANY的版本。
    Declare Sub CopyMemory Lib "kernel32" Alias _
"RtlMoveMemory" (ByVal dest As Long, ByVal source As Long, _
                 ByVal numBytes As Long)

    '嘿嘿,看下面是如何將CallWindowProc的聲明做成Compare聲明的。
    Declare Function Compare Lib "user32" Alias _
"CallWindowProcA" (ByVal pfnCompare As Long, ByVal pElem1 As Long, _
                   ByVal pElem2 As Long, ByVal unused1 As Long, _
                   ByVal unused2 As Long) As Integer
'注:ByVal xxxxx As Long ,還記得吧!這是標準的指針聲明方法。
     
    '聲明需要比較的數組元素的結構
    Public Type TEmployee
        Name As String
        Salary As Currency
    End Type

    '再來看看我們的比較函數
    '先按薪水比較,再按姓名比較
    Function CompareSalaryName(Elem1 As TEmployee, _
                               Elem2 As TEmployee, _                                                 
                               unused1 As Long, _
                               unused2 As Long) As Integer
        Dim Ret As Integer
        Ret = Sgn(Elem1.Salary - Elem2.Salary)
        If Ret = 0 Then
            Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare)
        End If
        CompareSalaryName = Ret
    End Function
    '先按姓名比較,再按薪水比較
    Function CompareNameSalary(Elem1 As TEmployee, _
                               Elem2 As TEmployee, _
                               unused1 As Long, _
                               unused2 As Long) As Integer
        Dim Ret As Integer
        Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare)
        If Ret = 0 Then
            Ret = Sgn(Elem1.Salary - Elem2.Salary)
        End If
        CompareNameSalary = Ret
    End Function

    最後再看看我們來看看我們最終的qsort的聲明。
    Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _
              ByVal nElemSize As Integer, ByVal pfnCompare As Long)
    上面的ArrayPtr是需要排序數組的第一個元素的指針,nCount是數組的元素個數,nElemSize是每個元素大小,pfnCompare就是我們的比較函數指針。這個聲明和C庫函數裏的qsort是極爲相似的。
    和C一樣,我們完全可以將Basic的函數指針傳遞給Basic的qsort函數。
    使用方式如下:
    Dim Employees(1 To 10000) As TEmployee
    '假設下面的調用對Employees數組進行了賦值初始化。
    Call InitArray()
    '現在就可以調用我們的qsort來進行排序了。
    Call qsort(VarPtr(Employees(1)), UBound(Employees), _
               LenB(Employees(1)), AddressOf CompareSalaryName)
    '或者先按姓名排,再按薪水排
    Call qsort(VarPtr(Employees(1)), UBound(Employees), _
               LenB(Employees(1)), AddressOf CompareNameSalary)    

    聰明的朋友們,你們是不是已經看出這裏的奧妙了呢?作爲一個測驗,你能現在就給出在qsort裏使用函數指針的方法嗎?比如現在我們要通過調用函數指針來比較數組的第i個元素和第j個元素的大小。
    沒錯,當然要使用前面聲明的Compare(其實就是CallWindowProc)這個API來進行強制回調。
    具體的實現如下:
    Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _
              ByVal nElemSize As Integer, ByVal pfnCompare As Long)
        Dim i As Long, j As Long
        '這裏省略快速排序算法的具體實現,僅給出比較兩個元素的方法。
        If Compare(pfnCompare, ArrayPtr + (i - 1) * nElemSize, _
                   ArrayPtr + (j - 1) * nElemSize, 0, 0) > 0 Then
            '如果第i個元素比第j個元素大則用CopyMemory來交換這兩個元素。
        End IF
    End Sub   

    招式介紹完了,明白了嗎?我再來簡單地講解一下上面Compare的意思,它非常巧妙地利用了CallWindowProc這個API。這個API需要五個參數,第一個參數就是一個普通的函數指針,這個API能夠強馬上回調這個函數指針,並將這個API的後四個Long型的參數傳遞給這個函數指針所指向的函數。這就是爲什麼我們的比較函數必須要有四個參數的原因,因爲CallWindowProc這個API要求傳遞給的函數指針必須符合WndProc函數原形,WndProc的原形如下:
    LRESULT (CALLBACK* WNDPROC) (HWND, UINT, WPARAM, LPARAM);
    上面的LRESULT、HWND、UINT、WPARAM、LPARAM都可以對應於VB裏的Long型,這真是太好了,因爲Long型可以用來作指針嘛!
    再來看看工作流程,當我們用AddressOf CompareSalaryName做爲函數指針參數來調用qsort時,qsort的形參pfnCompare被賦值成了實參CompareSalaryName的函數指針。這時,調用Compare來強制回調pfnCompare,就相當於調用瞭如下的VB語句:
     Call CompareSalaryName(ArrayPtr + (i - 1) * nElemSize, _
                            ArrayPtr + (j - 1) * nElemSize, 0, 0)
     這不會引起參數類型不符錯誤嗎?CompareSalaryName的前兩個參數不是TEmployee類型嗎?的確,在VB裏這樣調用是不行的,因爲VB的類型檢查不會允許這樣的調用。但是,實際上這個調用是API進行的回調,而VB不可能去檢查API回調的函數的參數類型是一個普通的Long數值類型還是一個結構指針,所以也可以說我們繞過了VB對函數參數的類型檢查,我們可以將這個Long型參數聲明成任何類型的指針,我們聲明成什麼,VB就認爲是什麼。所以,我們要小心地使用這種技術,如上面最終會傳遞給CompareSalaryName函數的參數"ArrayPtr + (i - 1) * nElemSize"只不過是一個地址,VB不會對這個地址進行檢查,它總是將這個地址當做一個TEmployee類型的指針,如果不小心用成了"ArrayPtr + i * nElemSize",那麼當i是最後一個元素時,我們就會引起內存越權訪問錯誤,所以我們要和在C裏處理指針一樣注意邊界問題。
    
    函數指針的巧妙應用這裏已經可見一斑了,但是這裏介紹的方法還有很大的侷限性,我們的函數必須要有四個參數,更乾淨的做法還是在VC或Delphi裏寫一個DLL,做出更加符合要求的API來實現和CallWindowProc相似的功能。我跟蹤過CallWindowProc的內部實現,它要做許多和窗口消息相關的工作,這些工作在我們這個應用中是多餘的。其實實現強制回調API只需要將後幾個參數壓棧,再call第一個參數就行了,不過幾條彙編指令而已。
    正是因爲CallWindowProc的侷限性,我們不能夠用它來調用外部的函數指針,以實現上面說的第三種函數指針調用方式。要實現第三種方式,Matt Curland大師提供了一個噩夢一般的HACK方式,我們要在VB裏憑空構造一個IUnknown接口,在IUnknown接口的vTable原有的三個入口後再加入一個新入口,在新入口裏插入機器代碼,這個機器代碼要處理掉this指針,最後才能調用到我們給的函數指針,這個函數指針無論是內部的還是外部的都一樣沒問題。在我們深入討論COM內部原理時我會再來談這個方法。
    另外,排序算法是個見仁見智的問題,我本來想,在本文提供一個最通用性能最好的算法,這種想法雖好,但是不可能有在任何情況下都“最好”的算法。本文提供的用各種指針技術來實現的快速排序方法,應該比用對象技術來實現同樣功能快不少,內存佔用也少得多。可是就是這個已經經過了我不少優化的快速排序算法,還是比不了ShellSort,因爲ShellSort實現上簡單。從算法的理論上來講qsort應該比ShellSort平均性能好,但是在VB裏這不一定(可見本文配套代碼,裏面也提供了VBPJ一篇專欄的配套代碼ShellSort,非常得棒,本文的思想就取自這個ShellSort)。
    但是應當指出無論是這裏的快速排序還是ShellSort,都還可以大大改進,
因爲它們在實現上需要大量使用CopyMemroy來拷貝數據(這是VB裏使用指針的缺點之一)。其實,我們還有更好的方法,那就是Hack一下VB的數組結構,也就是COM自動化裏的SafeArray,我們可以一次性的將SafeArray裏的各個數組元素的指針放到一個long型數組裏,我們無需CopyMemroy,我們僅需交換Long型數組裏的元素就可以達到實時地交換SafeArray數組元素指針的目的,數據並沒有移動,移動的僅僅是指針,可以想象這有快多。在下一篇文章《VB指針葵花寶典之數組指針》中我會來介紹這種方法。
    

後記:
    我學習所以我快樂。
VB真是想不到系列之四:VB指針葵花寶典之SafeArray
關鍵字:VB、HCAK、指針、SafeArray、數組指針、效率、數組、排序
難度:中級或高級
要求:熟悉VB,瞭解基本的排序算法,會用VC更好。

引言:
    上回說到,雖然指針的運用讓我們的數組排序在性能上有了大大的提高,但是CopyMemory始終是我們心裏一個揮之不去的陰影,因爲它還是太慢。在C裏我們用指針,從來都是來去自如,隨心所欲,四兩撥千斤;而在VB裏,我們用指針卻要瞻前顧後,哪怕一個字節都要用到CopyMemory乾坤大挪移,真累。今天我們就來看看,能不能讓VB裏的指針也能指哪兒打哪兒,學學VB指針的凌波微步。
    各位看官,您把茶端好了。
    
    一、幫VB做點COM家務事
    本系列開張第一篇裏,我就曾說過VB的成功有一半的功勞要記到COM開發小組身上,COM可是M$公司打的一手好牌,從OLE到COM+,COM是近十年來M$最成功技術之一,所以有必要再吹它幾句。
    COM組件對象模型就是VB的基礎,Varinat、String、Current、Date這些數據類型都是COM的,我們用的CStr、CInt、CSng等Cxxx函數根本就是COM開發小組寫的,甚至我們在VB裏用的數學函數,COM裏都有對應的VarxxxDiv、VarxxxAdd,VarxxxAbs。嘿嘿,VB開發小組非常聰明。我們也可以說COM的成功也有VB開發小組和天下無數VB程序員的功勞,Bill大叔英明地將COM和VB捆綁在一起了。
    所以說,學VB而不需要了解COM,你是幸福的,你享受着VB帶給你的輕鬆寫意,她把那些瑣碎的家務事都幹了,但同時你又是不幸的,因爲你從來都不曾瞭解你愛的VB,若有一天VB對你發了脾氣,你甚至不知該如何去安慰她。所以,本系列文章將拿出幾大篇來教大家如何幫VB做點COM方面的家務事,以備不時之需。
    想一口氣學會所有COM家務事,不容易,今天我們先拿數組來開個頭,更多的技術我以後再一一道來。
    
    二、COM自動化裏的SafeArray
    就象洗衣機、電飯堡、吹塵器,VB洗衣服、做飯、打掃衛生都會用到COM自動化。它包含了一切COM裏通用的東西,所有的女人都能用COM自動化來幹家務,無論是犀利的VC、溫柔的VB、還是小巧的VBScript,她們都能用COM自動化,還能通過COM自動化閒話家常、交流感情。這是因爲COM自動化提供了一種通用的數據結構和數據轉換傳遞的方式。而VB的數據結構基本上就是COM自動化的數據結構,比如VB裏的數組,在COM裏叫做SafeArray。所以在VB裏處理數組時我們要清楚的知道我們是在處理SafeArray,COM裏的一種安全的數組。
    準備下廚,來做一道數組指針排序的菜,在看主料SafeArray的真實結構這前,先讓我們來了解一下C裏的數組。
    在C和C++裏一個數組指針和數組第一個元素的指針是一回事,如對下:
    #include <iostream>
    using namespace std;
    int main() {
      int a[10];
      cout << "a = " << a << endl;
      cout << "&a[0] =" << &a[0] << endl;
    } ///:~
    可以看到結果a和&a[0]是相同的,這裏的數組是才數據結構裏真實意義上的數組,它們在內存裏一個接着一個存放,我們通過第一個元素就能訪問隨後的元素,我們可以稱這樣的數組爲"真數組"。但是它不安全,因爲我們無法從這種真數組的指針上得知數組的維數、元素個數等非常重要的信息,所以也無法控制對這種數組的訪問。我們可以在C裏將一個二維數組當做一維數組來處理,我們還可以通過一個超過數組大小的索引去訪問數組外的內存,但這些都是極不安全的,數組邊界錯誤可以說是C裏一個非常容易犯卻不易發現的錯誤。
    因此就有了COM裏的SafeArray安全數組來解決這個問題,在VB裏我們傳遞一個數組時,傳遞的實際上COM裏的SafeAraay結構指構的指針,SafeAraay結構樣子如下:
    Private Type SAFEARRAY
       cDims As Integer        '這個數組有幾維?
       fFeatures As Integer     '這個數組有什麼特性?
       cbElements As Long       '數組的每個元素有多大?
       cLocks As Long           '這個數組被鎖定過幾次?
       pvData As Long           '這個數組裏的數據放在什麼地方?
       'rgsabound() As SFArrayBOUND
    End Type
    緊接在pvData這後的rgsabound是個真數組,所以不能在上面的結構裏用VB數組來聲明,記住,在VB裏的數組都是SafeArray,在VB裏沒有聲明真數組的方法。
    不過這不是問題,因爲上面SFArrayBOUND結構的真數組在整個SAFEARRAY結構的位置是不變的,總是在最後,我們可以用指針來訪問它。SFArrayBOUND數組的元素個數有cDims個,每一個元素記錄着一個數組維數的信息,下面看看它的樣子:
    Private Type SAFEARRAYBOUND
        cElements As Long      '這一維有多少個元素?
        lLbound As Long        '它的索引從幾開始?
    End Type
    還有一個東西沒說清,那就是上面SAFEARRAY結構裏的fFeatures,它是一組標誌位來表示數組有那些待性,這些特性的標誌並不需要仔細的瞭解,本文用不上這些,後面的文章用到它們時我會再來解釋。
   看了上面的東西,各位一定很頭大,好在本文的還用不了這麼多東西,看完本文你就知道其實SafeArray也不難理解。先來看看如下的聲明:
   Dim MyArr(1 To 8, 2 To 10) As Long
   這個數組做爲SafeArray在內存裏是什麼樣子呢?如圖一:
cDims = 2
fFeatures =
FADF_AUTO AND FADF_FIXEDSIZE
位置
0
cbElements = 4   LenB(Long) 4
cLocks = 0 8
pvData(指向真數組) 12
rgsabound(0).cElements = 8 16
rgsabound(0).lLbound = 1 18
rgsabound(1).cElements = 9 22
rgsabound(1).lLbound = 2 26







cDims = 2





fFeatures =
FADF_AUTO AND FADF_FIXEDSIZE


位置
0




cbElements = 4   LenB(Long)

4




cLocks = 0

8




pvData(指向真數組)

12




rgsabound(0).cElements = 8

16




rgsabound(0).lLbound = 1

18




rgsabound(1).cElements = 9

22




rgsabound(1).lLbound = 2


26





圖一 :SafeArray內存結構
cDims表示它是個2維數組,sFeatures表示它是一個在堆棧裏分配的固定大小的數組,cbElements表示它的每個元素大小是Long四個字節,pvData指向真的數組(就是上面說的C裏的數組),rgsabound這個真數組表明數組二個維的大小和每個維的索引開始位置值。
    先來看看從這個上面我們能做些什麼,比如要得到一個數組的維數,在VB裏沒有直接提供這樣的方法,有一個變通的方法是通過錯誤捕獲如下:
    On Error Goto BoundsError
    For I = 1 To 1000     '不會有這麼多維數的數組
        lTemp = LBound(MyArr, I)
    Next
BoundErro:
    nDims = I - 1
    MsgBox "這個數組有" & nDims & "維"

    現在我們知道了SafeArray的原理,所以也可以直接得到維數,如下:
    '先得到一個指向SafeArray結構的指針的指針,原理是什麼,我後面說。
    ppMyArr = VarPtrArray(MyArr)
    '從這個指針的指針得到SafeArray結構的指針
    CopyMemory pMyArr, ByVal ppMyArr, 4
    '再從這個指針所指地址的頭兩個字節取出cDims
    CopyMemory nDims, ByVal pMyArr, 2
    MsgBox "這個數組有" & nDims & "維"

    怎麼樣,是不是也明白了LBound實際上是SafeArray裏的rgsabound的lLbound,而UBound實際上等於lLbound +cElements - 1,現在我提個問,下面iUBound應該等於幾?    
    Dim aEmptyArray() As Long
    iUBound = UBound(aEmptyArray)
    正確的答案是-1,不奇怪,lLbound -cElements - 1 = 0 - 0 - 1 = -1    
    所以檢查UBound是不是等於-1是一個判斷數組是不是空數組的好辦法。

    還有SafeArray結構裏的pvData指向存放實際數據的真數組,它實際就是一個指向真數組第一個元素的指針,也就是說有如下的等式:
    pvDate = VarPtr(MyArr(0))
    在上一篇文章裏,我們傳給排序函數的是數組第一個元素的地址VarPtr(xxxx(0)),也就是說我們傳的是真數組,我們可以直接在真數組上進行數據的移動、傳遞。但是要如何得到一個數組SafeArray結構的指針呢?你應該注意到我上面所用的VarPtrArray,它的聲明如下:
    Declare Function VarPtrArray Lib "msvbvm60.dll" _
            Alias "VarPtr" (Var() As Any) As Long
    它就是VarPtr,只不過參數聲明上用的是VB數組,這時它返回來的就是一個指向數組SafeArray結構的指針的指針。因爲VarPtr會將傳給它的參數的地址返回,而用ByRef傳給它一個VB數組,如前面所說,實際上傳遞的是一個SafeArray結構的指針,這時VarPtrArray將返回這個指針的指針。所以要訪問到SafeArray結構需要,如下三步:
用VarPtrArray返回ppSA,再通過ppSA得到它指向的pSA,pSA纔是指向SafeArray結構的指針,我們訪問SafeArray結構需要用到就是這個pSA指針。
    現在你應該已經瞭解了SafeArray大概的樣子,就這麼一點知識,我們就能在VB裏對數組進行HACK了。

    三、HACK數組字串指針
    這已經是第三篇講指針的東西了,我總在說指針能夠讓我們怎麼樣怎麼樣,不過你是不是覺得除了我說過的幾個用途外,你也沒覺得它有什麼用,其實這是因爲我和大家一樣急於求成。在講下去之前,我再來理一理VB裏指針應該在什麼情況下用。
     只對指針類型用指針!廢話?我的意思是說,象Integer, Long, Double這樣的數值類型它們的數據直接存在變量裏,VB處理它們並不慢,沒有HACK的必要。但是字串,以及包括字串、數組、對象、結構的Variant,還有包括字串、對象結構的數組它們都是指針,實際數據不放在變量裏,變量放的是指針,由於VB不直接支持指針,對他們的操作必須連同數據拷貝一起進行。有時我們並不想賦值,我們只想交換它們指針,或者想讓多個指針指向同一個數據,讓多個變量對同一處內存操作,要達到這樣的目的,在VB裏不HACK是不行的。
    對數組尤其如此,比如我們今天要做的菜:對一個字串數組進行排序。我們知道,對字串數組進行排序很大一部分時間都用來交換字串元素,在VB裏對字串賦值時要先將原字串釋放掉,再新建一個字串,再將源字串拷貝過來,非常耗時。用COM裏的概念來說,比如字串a、b的操作a=b,要先用SysFreeString(a)釋放掉原來的字串a, 再用a = SysAllocString(b)新建和拷貝字串,明白了這一點就知道,在交換字串時不要用賦值的方式去交換,而應該直接去交換字串指針,我在指針葵花寶典第一篇裏介紹過這種交換字串的方法,這可以大大提高交換字串的速度。但是這種交換至少也要用兩次CopyMemory來將指針寫回去,對多個字串進行交換時調用CopyMemory的次數程幾何增長,效率有很大的損失。而實際上,指針只是32位整數而已,在C裏交換兩個指針,只需要進行三次Long型整數賦值就行了。所以我們要想想我們能不能將字串數組裏所有字串指針拿出來放到一個Long型指針數組裏,我們只交換這個Long型數組裏的元素,也就相當於交換了字串指針,排好序後,再將這個Long型指針數組重新寫回到字串數組的所有字串指針裏,而避免了多次使用CopyMemory來一次次兩兩交換字串指針。這樣我們所有的交換操作都是對一個Long型數組來進行,要知道交換兩個Long型整數,在VB裏和在C裏是一樣快的。
    現在我們的問題成了如何一次性地將字串數組裏的字串指針拿出來,又如何將調整後的字串指針數組寫回去。
    不用動數組的SafeArray結構,我們用StrPtr也能完成它。我們知道,字串數組元素裏放的是實際上是字串指針,也就是BSTR指針,把這些指針放到一個Long型數組裏很簡單,用下面的方法:
    Private Sub GetStrPtrs()
        Dim Hi As Long, Lo As Long
        Hi = UBound(MyArr)
        Lo = LBound(MyArr)
        ReDim lStrPtrs(0 To 1, Lo To Hi) As Long
        Dim i As Long
        For i = Lo To Hi
            lStrPtrs(0, i) = StrPtr(MyArr(i))   'BSTR指針數組
            lStrPtrs(1, i) = i                  '原數組索引
        Next
    End Sub
    爲什麼要用2維數組,這是排序的需要,因爲當我們交換lStrPtrs裏的Long型指針時,原來的字串數組MyArr裏的字串指針並沒有同時交換,所以用lStrPtrs裏的Long型指針訪問字串時,必須通過原來的索引,因此必須用2維數組同時記錄下每個Long型指針所指字串在原字串數組裏的索引。如果只用1維數組,訪問字串時就又要用到CopyMemory了,比如訪問lStrPtrs第三個元素所指的字串,得用如下方法:
     CopyMemory ByVal VarPtr(StrTemp), lStrPtrs(3), 4
雖然只要我們保證StrTemp足夠大,再加上一些清理善後的工作,這種做法是可以的,但實際上我們也看到這樣還是得多次調用CopyMemory,實際上考慮到原來的字串數組MyArr一直就沒變,我們能夠通過索引來訪問字串,上面同樣的功能現在就成了:
     StrTemp = MyArr(lStrPtrs(1,3)) '通過原字串數組索引讀出字串。
    不過,當我們交換lStrPtrs裏的兩個Long型指針元素時,還要記得同時交換它們的索引,比如交換第0個和第3個元素,如下:
     lTemp1 = lStrPtrs(0, 3) : lTemp2 = lStrPtrs(1, 3)
     lStrPtrs(0, 3) = lStrPtrs(0, 0) : lStrPtrs(1, 3) = lStrPtrs(1, 0)
     lStrPtrs(0, 0) = lTemp1 : lStrPtrs(1, 0) = lTemp2
     當我們排好序後,我們還要將這個lStrPtrs裏的指針元素寫回去,如下:
     For i = Lo To Hi
        CopyMemory(ByVal VarPtr(MyArr(i)), lStrPtrs(0,i), 4)
     Next   
     我已經不想再把這個方法講下去,雖然它肯定可行,並且也肯定比用CopyMemory來移動數據要快,因爲我們實際上移動的僅僅是Long型的指針元素。但我心裏已經知道下面有更好更直接的方法,這種轉彎抹角的曲線救國實在不值得浪費文字。


    四、HACK數組的BSTR結構,實時處理指針。
    最精采的來了,實時處理指針動態交換數據,好一個響亮的說法。
    我們看到,上一節中所述方法的不足在於我們的Long型指針數組裏的指針是獨立的,它沒有和字串數組裏的字串指針聯繫在一起,要是能聯繫在一起,我們就能在交換Long型指針的同時,實時地交換字串元素。
    這可能嗎?
    當然,否則我花那麼筆墨去寫SafeArray幹什麼!
    在上一節,我們的目的是要把字串數組裏的BSTR指針數組拿出來放到一個Long型數組裏,而在這一節我們的目的是要讓我們Long型指針數組就是字串數組裏的BSTR指針數組。拿出來再放回去的方法,我們在上一節看到了,現在我們來看看,不拿出來而直接用的方法。
    這個方法還是要從字串數組的SafeArray結構來分析,我們已經知道SafeArray結構裏的pvData指向的就是一個放實際數據的真數組,而一個字串數組如MyArr它的pvData指向的是一個包含BSTR指針的真數組。現在讓我們想想,如果我們將一個Long型數組lStrPtrs的pvData弄得和字串數組MyArr的pvData一樣時會怎樣?BSTR指針數組就可以通過Long型數組來訪問了,先看如何用代碼來實現這一點:
    '模塊級變量
    Private MyArr() As String '要排序的字串數組
    Private lStrPtrs() As Long '上面數組的字串指針數組,後面會憑空構造它
    Private pSA As Long        '保存lStrPtrs數組的SafeArray結構指針
    Private pvDataOld As Long '保存lStrPtrs數組的SafeArray結構的原
                               '    pvData指針,以便恢復lStrPtrs

   
    '功能: 將Long型數組lStrPtrs的pvData設成字串數組MyArr的pvData
    '    以使Long指針數組的變更能實時反應到字串數組裏
    Private Sub SetupStrPtrs()
        Dim pvData As Long
       
        ' 初始化lStrPtrs,不需要將數組設得和MyArr一樣大
        '     我們會在後面構造它
        ReDim lStrPtrs(0) As Long
       
       '得到字串數組的pvData
        pvData = VarPtr(MyArr(0))

       '得到lStrPtrs數組的SafeArray結構指針
        CopyMemory pSA, ByVal VarPtrArray(lStrPtrs), 4
       
        '這個指針偏移12個字節後就是pvData指針,將這個指針保存到pvDataOld
        '    以便最後還原lStrPtrs,此處也可以用:
        '        pvDataOld = VarPtr(lStrPtrs(0))
        CopyMemory pvDataOld, ByVal pSA + 12, 4
       
        '將MyArr的pvData寫到lStrPtrs的pvData裏去
        CopyMemory ByVal pSA + 12, pvData, 4
       
        '完整構造SafeArray必須要構造它的rgsabound(0).cElements
        CopyMemory ByVal pSA + 16, UBound(MyArr) - LBound(MyArr) + 1, 4
        '還有rgsabound(0).lLbound
        CopyMemory ByVal pSA + 20, LBound(MyArr), 4
    End Sub
    看不懂,請結合圖一再看看,應該可以看出我們是憑空構造了一個lStrPtrs,使它幾乎和MyArr一模一樣,唯一的不同就是它們的類型不同。MyArr字串數組裏的fFeatures包含FADF_BSTR,而lStrPtrs的fFeatures包含FADF_HAVEVARTYPE,並且它的VARTYPE是VT_I4。不用關心這兒,我們只要知道lStrPtrs和MyArr它們指向同一個真數組,管他是BSTR還是VT_I4,我們把真數組裏的元素當成指針來使就行了。
    注意,由於lStrPtrs是我們經過了我們很大的改造,所以當程序結束前,我們應該將它還原,以便於VB來釋放資源。是的,不釋放也不一定會引起問題,因爲程序運行結束後,操作系統的確是會回收我們在堆棧裏分配了卻沒有釋放的lStrPtrs原來的野指針pvOldData,但當你在IDE中運行時,你有60%的機會讓VB的IDE死掉。我們是想幫VB做點家務事,而不是想給VB添亂子,所以請記住在做完菜後,一定要把廚房打掃乾淨,東西該還原的一定要還原。下面看看怎麼樣來還原:    
    '還原我們做過手腳的lStrPtr
    Private Sub CleanUpStrPtrs()
        'lStrPtr的原來聲明爲:ReDim lStrPtrs(0) As Long
        '    按聲明的要求還原它
        CopyMemory pSA, ByVal VarPtrArray(lStrPtrs), 4
        CopyMemory ByVal pSA + 12, pvDataOld, 4
        CopyMemory ByVal pSA + 16, 1, 4
        CopyMemory ByVal pSA + 20, 0, 4
    End Sub
    
    好了,精華已經講完了,如果你還有點想不通,看看下面的實驗:
    '實驗
    Sub Main()
        '初始化字串數組
        Call InitArray(6)
       
        '改造lStrPtrs
        Call SetupStrPtrs
       
        '下面說明兩個指針是一樣的
        Debug.Print lStrPtrs(3)
        Debug.Print StrPtr(MyArr(3))
        Debug.Print
        '先看看原來的字串
        Debug.Print MyArr(0)
        Debug.Print MyArr(3)
        Debug.Print
       
        '現在來交換第0個和第3個字串
        Dim lTmp As Long
        lTmp = lStrPtrs(3)
        lStrPtrs(3) = lStrPtrs(0)
        lStrPtrs(0) = lTmp
       
        '再來看看我們的字串,是不是覺得很神奇
        Debug.Print MyArr(0)
        Debug.Print MyArr(3)
        Debug.Print
       
        '還原
        Call CleanUpStrPtrs
    End Sub
    在我的機器上,運行結果如下:
1887420
1887420

OPIIU
WCYKOTC

WCYKOTC
OPIIU
    怎麼樣?如願已償!字串通過交換Long型數被實時交換了。
    通過這種方式來實現字串數組排序就非常快了,其效率上的提高是驚人的,對冒泡排序這樣交換字串次數很多的排序方式,其平均性能能提高一倍以上(要看我們字串平均長度,),對快速排序這樣交換次數較少的方法也能有不少性能上的提高,用這種技術實現的快速排序,可以看看本文的配套代碼中的QSortPointers。
    本道菜最關鍵的技術已經講了,至於怎麼做完這道菜,怎麼把這道菜做得更好,還需要大家自己來實踐。
    
    四、我們學到了什麼。
    僅就SafeArray來說,你可能已經發現我根本就沒有直接去用我定義了的SAFEARRAY結構,我也沒有展開講它,實際上對SafeArray我們還可以做很多工作,還有很多巧妙的應用。還有需要注意的,VarPtrArray不能用來返回字串數組和Variant數組的SafeArray結構的指針的指針,爲什麼會這樣和怎樣來解決這個問題?這些都需要我們瞭解BSTR,瞭解VARIANT,瞭解VARTYPE,這些也只是COM的冰山一角,要學好VB,乃至整個軟件開發技術,COM還有很多很多東西需要學習,我也還在學,在我開始COM的專題之前,大家也應該自學一下。
    COM的東西先放一放,下一篇文章,應朋友的要求,我準備來寫寫內存共享。

後記:
    又花了整整一天的時間,希望寫的東西有價值,覺得有用就來叫個好吧!

AdamBear
熊超
[email protected]
真想不到之五:高效字串指針類
關鍵字:VB、HCAK、字串指針、BSTR、效率、內存共享
難度:中級或高級
參考文章:
1、2000年7月VBPJ Black Belt專欄文章《Modify a Varialbe's Pointer》
   作者:Bill McCarthy
2、1998年4月VBPJ Black Belt專欄文章《Play VB's Strings》
   作者:Francesco Balena

引言:
    本想以內存共享做爲VB指針專題的最後一篇,寫着寫着發現字串的問題應該單獨談談。在內存共享的問題上,我尤其關心的是字串的共享,因爲在我一個多月前發佈的源碼裏用的是《HardCore VB》裏Bruce Mckinney提供的CShareStr類,它實現了字串的內存共享。但是Bruce也沒有突破侷限,對字串的處理依然是CopyMemory的乾坤大挪移,尤其是還要進行討厭的ANSI/DBCS和Unicode的轉換。我在readme裏說過它效率極低,應該採用Variant或Byte數組來實現,才能避免轉換。後來又想到可以用StrPtr來做,並在VC裏用DLL共享節實現了可以不進行轉換的字串內存共享。不過在VC裏我仍然需要用SysAllocString來建立VB能使用的BSTR。這都不是我想要的,我想要的東西要象VC裏的CString的一樣,只要字串夠大,對其賦值就不用重新分配內存,還要象VC裏CComBSTR類一樣可以Attach到一個特定BSTR。
    知道該怎麼做,是在看了VBPJ上Bill McCarthy和Francesco Balena的兩篇文章之後。Bill用修改SafeArray描述結構實現了數組的內存共享,而Francesco則對字串指針進行深入的探討。但是Bill和Francesco的東西都沒有實現我想要的字串類。
    方法知道了,實現並不難,所以我決定自己來包裝一個這樣的東西。
       


正文:
    使用VB裏的字串類型String有兩大不足:第一、它的分配是由VB運行時控制,我們不能將其分配在指定內存處;第二,任何一次對字串的賦值操作都要進行內存重新分配。要實現高效、靈活的字串處理,我們必須克服這兩大不足。
    對於第一個問題,通過修改String變量裏放着的BSTR描述符指針可以實現;對於第二個問題,可以用Mid語句(注意是語句而不是函數)來賦值。不詳細講了,直接看下面的這個類:

    Option Explicit
    
    '********************************************************
    'clsBSTR.cls
    '作者: 熊超        ID: AdamBear        2002年3月18日
    'http://www.csdn.net/Author/AdamBear
    '    你可以自由使用本類模塊,不過請保留本聲明
    '********************************************************
    
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
    
    '不要直接對sString賦值(可以用MID語句),將其設爲公有僅爲提高效率。
    Public sString As String               'BSTR描述符指針
    
    Private pStr As Long                    'BSTR地址
    Private nMaxLen As Long                 'BSTR最大字節數
    
    
    
    '讓本字串指向特定地址
    Public Sub Attach(Addr As Long, Optional nLen As Long)
        pStr = Addr
        '修改BSTR描述符指針,使其指向Addr
        CopyMemory ByVal VarPtr(sString), Addr, 4
       
        If IsMissing(nLen) Then Exit Sub
        '設定最大字串字節數
        nMaxLen = nLen
       
    End Sub
    
    '還原本字串原BSTR描述符
    Public Sub Detach()
        CopyMemory ByVal VarPtr(sString), 0&, 4
    End Sub
    
    '讓本字串指向源字串
    Public Sub AttachStr(sStr As String)
        Attach StrPtr(sStr), LenB(sStr)
    End Sub
    
    'data爲缺省屬性
    Public Property Let data(sVal As String)
        Dim c As Long
        c = LenB(sVal)
'超過最大字串數,拋出錯誤。
        If c > nMaxLen Then Err.Raise vbObjectError + 3000, _
                                  "CString::Let Data", "溢出"
        '寫字串長度
        CopyMemory ByVal (pStr - 4), c, 4
        '寫字串
        Mid(sString, 1) = sVal
    End Property
    
    '可以通過公有變量sString來讀字串,效率更高
    Public Property Get data() As String
        data = sString
    End Property

    Private Sub Class_Terminate()
        Call Detach
    End Sub
    
    用法如下,假設我們已通過VitualAlloc,HeapAlloc,MapViewOfFile這樣的內存管理API得到了一個4k個字節的可讀寫的內存地址baseAddr:
    Dim sShare As New clsBSTR
    '留下前4個字節用於BSTR保存字串字節數
    sShare.Attach(baseAddr+4, 4096-4)
    '下面的字串"Test"會直接寫到baseAddr+4字節處
    sShare = "Test"    
    Dim y As String
    '讀字串時可以用sString屬性或缺省屬性
    y = sShare.sString
    '用AttachStr方法Attach到一個字串。
    '必須要先Detach
    sShare.Detach
    sShare.AttachStr(y)
    sShare = "Hahaha"
    Debug.Print y
    '一旦AttachStr到字串y後,對sShare的修改就相當於對y的修改。
    '並且以後對y的修改也只能用Mid語句
    Mid(y, 1) = "xxxxx"
    '不能直接賦值,這樣VB會將原來y所指(也是sShare所指)內存釋放,
    '    重新分配y。這樣在訪問sShare時會出錯。
    'y = "Test"
       
    
    我也不在這裏講這個類的詳細原理,可以參考我前面說的兩篇文章。
    使用這個類有幾個需要注意的地方。
    1、讀字串時可以用sString屬性來讀,更快。
    讀sShare有兩種方法,一種是用缺省屬性Data來讀,一種是直接用sString屬性來讀。用sString屬性不重新分配內存,要快得多。
    2、不要直接給sString賦值,應使用缺省的data屬性來賦值。
    之所以把sString屬性暴露出來,是爲了效率和方便。我們可以用Mid語句對其進行修改,但不要直接用"="來賦值。
    3、注意Attach的第二個參數,表示字串的最大字節數,不要讓它超過已經分配的內存。
    4、用AttachStr將本字串對象Attach到某個字串(比如上面的y)上後,不能再對這個字串y重新賦值,也不能將其傳遞到會對其重新賦值的過程。
       
    哇,這麼多需要注意的問題,用起來豈不是更不方便。的確,用它的之前要考慮是不是必須的。因爲建立這個類也一樣有開銷。所以還有一個需要注意的問題:

    5、它主要的應用還是在於將字串安放在指定內存處。雖然它也可以讓同一個進程內幾個的字串達到共享的目的,但是如果只是兩三個很小的字串這樣時做反而慢了。
    

後計:
    數組指針和字串指針我們已經談過了,對於普通的數值類型變量的指針沒有什麼Hack的必要,但是它關係到一個有用的技術,下篇文章再談。
    本文和下篇文章的代碼,以及用這個類來實現的共享內存的代碼,我會發布到CSDN共享軟件上,名字是《內存共享和指針》。
真想不到之六:有用的技術和沒用的指針
關鍵字:VB、SafeArray、數值類型指針
難度:中級
參考文章:
1、2000年7月VBPJ Black Belt專欄文章《Modify a Varialbe's Pointer》
   作者:Bill McCarthy

引言:
    這真的是指針專題的最後一篇了(當然,以後肯定還會提到指針)。主要是來談談Bill McCarthy的文章《Modify a Varialbe's Pointer》的精華。關於這篇文章的東西,在我的《VB指針葵花寶典之SafeArray》裏曾談到過,但那篇文章實際上沒有寫出SafeArray的精華,用SafeArray最妙的地方在於可以將一個變量建在指定的內存處,就象上一篇文章給出的那個字串類一樣。

正文:   
    Bill McCarthy在那篇《Modify a Varialbe's Pointer》裏用SafeArray實現多進程的數組共享內存,他考慮了數組變量的類型,因此可以兼容大部分數值類型的數組,是一個非常不錯的東西。我這裏不講它實現的具體方法,只是想和大家一起看看SafeArray還能做什麼。
    修改SafeArray結構的pvData指針卻是一個非常有用的技術,通過修改pvData,就能夠通過數組直接訪問指定的內存。
    和上一篇文章包裝字串指針類一樣,通過修改pvData,我們也可以包裝一些普通數值類型變量的指針類。
    我在指針的第一篇文章裏說過,要想實現C語言裏'*'這個取指針所指變量值功能,必須要用CopyMemory。實際上,我說錯了,我們完全可以實現和C裏一樣的指針,如下:
//C語言
    Long L;
    Long* pL = &L;
    *pL = 12;
    printf("L = %d *pL = %d", l, *pl);

'VB裏
     Dim pL As New pLong, L As Long
     pL.Attach L
     '也可以 pL.Ptr = VarPtr(L)
     pL = 12
     Debug.Print "L ="; L; " *pL ="; pL
   
    結果都能夠通過修改pL指針,達到修改變量L的目的。
    上面VB代碼裏的pLong就是一個包裝好的Long型變量的指針類,下面看看如何來實現它:

    Option Explicit
    '********************************************************
    'pLong.cls
    '包裝一個Long型指針的類
    '作者: 熊超        ID: AdamBear        2002年3月18日
    'http://www.csdn.net/Author/AdamBear
    '    你可以自由使用本類模塊,不過請保留本聲明
    '********************************************************
    
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
    
    Private m_Arr(0) As Long
    
    
    '缺省屬性
    Public Property Get Data() As Long
        Data = m_Arr(0)
    End Property
    
    Public Property Let Data(ByVal Value As Long)
        m_Arr(0) = Value
    End Property
    
    Public Sub Attach(Target As Long)
        Ptr = VarPtr(Target)
    End Sub
    
    Public Property Let Ptr(ByVal Target As Long)
        Dim pSA As Long
       
        '得到SafeArray結構指針pSA
        CopyMemory pSA, ByVal VarPtrArray(m_Arr), 4
        '這個指針偏移12個字節後就是pvData指針
        CopyMemory ByVal (pSA + 12), Target, 4
       
    End Property
    
    Public Property Get Ptr() As Long
        Ptr = m_SA.pvData
    End Property
    
    
    Private Sub Class_Terminate()
        CopyMemory ByVal VarPtrArray(m_Arr), 0&, 4
    End Sub

    要將它改成Byte的指針類,只需要將上面的代碼中m_Arr數組的類型,Data屬性和Attach方法中的參數類型改爲Byte型即可。

    當我們這樣做出pLong、pByte、pInteger後,我們就能夠玩點和C裏一樣的花樣了。
    Sub Main()
        Dim pB As New pByte, B As Byte
        Dim pI As New pInteger, I As Integer
        Dim pL As New pLong, L As Long
    
        '用Attach方法將經過類型檢查,直接用Ptr屬性則可以繞過類型檢查
        pB.Attach B
        pI.Attach I
        pL.Attach L
       
        '試試指針
        B = 1
        Debug.Print "B ="; B; " *pB ="; pB
       
        pB = 1
        Debug.Print "B ="; B; " *pB ="; pB
       
        I = 1000
        Debug.Print "I ="; I; " *pI ="; pI
    
        pI = 2000
        Debug.Print "I ="; I; " *pI ="; pI
    
        L = 40000
        Debug.Print "L ="; L; " *pL ="; pL
       
        pL = 60000
        Debug.Print "L ="; L; " *pL ="; pL
       
        '試試C裏的類型轉換
        '用Integer指針訪問Long型變量
        pI.Ptr = VarPtr(L)
        Debug.Print "*pI ="; pI
       
    End Sub

    搞出這幾種普通數值類型的指針類有什麼用?基本上沒有什麼大用。不過是證明一種方法的可行性,和演示技術。這種技術還有什麼用,需要的時候還會再談。

後記:
    本文的東西,可見CSDN共享軟件上的《內存共享和指針》,
    指針的專題就到這兒了,下一篇準備開始着手寫VB和COM的一個系列文章,其間我準備翻譯一下《VB Design Patterns》,這是一本不錯的書。

轉載者後記:感謝作者將我從VB的噩夢中拯救出來,感謝他所以要轉載他的文章感化更多的VB用戶!!!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章