VB進(jìn)階 : VB真是想不到系列之二:VB《葵花寶典》--指針技術(shù)
關(guān)鍵字:VB,、指針,、動(dòng)態(tài)內(nèi)存分配,、效率,、安全
難度:中級(jí)至高級(jí)
要求:熟悉VB,,掌握基本的C,,了解匯編,,了解內(nèi)存分配原理。
想當(dāng)年?yáng)|方不敗,,黑木崖密室一戰(zhàn),,僅憑一根繡花針獨(dú)戰(zhàn)四大高手,神出鬼沒(méi),,堪稱天下武林第一高手,。若想成為VB里的東方不敗,熟習(xí)VB《葵花寶典》,,掌握VB指針技術(shù),,乃是不二的法門。
欲練神功,,引刀……,,其實(shí)掌握VB指針技術(shù),并不需要那么痛苦,。因?yàn)檎f(shuō)穿了,,也就那么幾招,再勤加練習(xí),,終可至神出鬼沒(méi)之境,。廢話少說(shuō),讓我們先從指針的定義說(shuō)起,。
一,、指針是什么?
不需要去找什么標(biāo)準(zhǔn)的定義,,它就是一個(gè)32位整數(shù),,在C語(yǔ)言和在VB里都可以用Long類型來(lái)表示。在32位Windows平臺(tái)下它和普通的32位長(zhǎng)整型 數(shù)沒(méi)有什么不同,,只不過(guò)它的值是一個(gè)內(nèi)存地址,,正是因?yàn)檫@個(gè)整數(shù)象針一樣指向一個(gè)內(nèi)存地址,所以就有了指針的概念,。
有統(tǒng)計(jì)表明,,很大一部分程序缺陷和內(nèi)存的錯(cuò)誤訪問(wèn)有關(guān)。正是因?yàn)橹羔樦苯雍蛢?nèi)存打交道,,所以指針一直以來(lái)被看成一個(gè)危險(xiǎn)的東西,。以至于不少語(yǔ)言,如著名的 JAVA,,都不提供對(duì)指針操作的支持,,所有的內(nèi)存訪問(wèn)方面的處理都由編譯器來(lái)完成。而象C和C++,,指針的使用則是基本功,,指針給了程序員極大的自由去隨 心所欲地處理內(nèi)存訪問(wèn),,很多非常巧妙的東西都要依靠指針技術(shù)來(lái)完成。
關(guān)于一門高級(jí)的程序設(shè)計(jì)語(yǔ)言是不是應(yīng)該取消指針操作,,關(guān)于沒(méi)有指針操作算不算一門語(yǔ)言的優(yōu)點(diǎn),,我在這里不討論,因?yàn)榛ヂ?lián)網(wǎng)上關(guān)于這方面的沒(méi)有結(jié)果的討論,,已經(jīng)造成了占用幾個(gè)GB的資源,。無(wú)論最終你是不是要下定決心修習(xí)指針技術(shù)《葵花寶典》,了解這門功夫總是有益處的,。
注意:在VB里,,官方是不鼓勵(lì)使用什么指針的,本文所講的任何東西你都別指望取得官方的技術(shù)支持,,一切都要靠我們自己的努力,,一切都更刺激!
讓我們開始神奇的VB指針探險(xiǎn)吧,!
二,、來(lái)看看指針能做什么?有什么用,?
先來(lái)看兩個(gè)程序,,程序的功能都是交換兩個(gè)字串:
【程序一】:注釋:標(biāo)準(zhǔn)的做法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
你是不是以為第一個(gè)程序要快,因?yàn)樗粗?jiǎn)單而且不用調(diào)用API(調(diào)用API需要額外的處理,,VB文檔明確指出大量調(diào)用API將降低程序性能),。但事實(shí) 上,在VB集成環(huán)境中運(yùn)行,,程序二要比程序一快四分之一;而編譯成本機(jī)代碼或p-code,,程序二基本上要比程序一快一倍,。下面是兩個(gè)函數(shù)在編譯成本機(jī)代碼后,運(yùn)行不同次數(shù)所花時(shí)間的比較:
運(yùn)行100000次,,SwapStr需要170毫秒,,SwapPtr需要90毫秒。
運(yùn)行200000次,,SwapStr需要340毫秒,,SwapPtr需要170毫秒。
運(yùn)行2000000次,,SwapStr需要3300毫秒,,SwapPtr需要1500毫秒。
的確,,調(diào)用API是需要額外指令來(lái)處理,,但是由于使用了指針技術(shù),,它沒(méi)有進(jìn)行臨時(shí)字串的分配和拷貝,因此速度提高了不少,。
怎么樣,,想不到吧!C/C++程序員那么依賴指針,,無(wú)非也是因?yàn)槭褂弥羔樛芨苯拥娜ヌ幚韱?wèn)題的根源,,更有駕馭一切的快感。他們不是不知道使用指針的危險(xiǎn),,他們不是不愿意開衛(wèi)星定位無(wú)級(jí)變速的汽車,,只是騎摩托更有快感,而有些地方只有摩托才走得過(guò)去,。
和在C里類似,,在VB里我們使用指針也不過(guò)三個(gè)理由:
一是效率,這是一種態(tài)度一種追求,,在VB里也一樣,;
二是不能不用,因?yàn)椴僮飨到y(tǒng)是C寫的,,它時(shí)刻都在提醒我們它需要指針,;
三是突破限制,VB想照料我們的一切,,VB給了我們很強(qiáng)的類型檢查,,VB象我們老媽一樣,對(duì)我們關(guān)心到有時(shí)我們會(huì)受不了,,想偶爾不聽媽媽的話嗎,?你需要指針!
但由于缺少官方的技術(shù)支持,,在VB里,,指針變得很神秘。因此在C里一些基本的技術(shù),,在VB里就變得比較困難,。本文的目的就是要提供給大家一種簡(jiǎn)單的方法, 來(lái)將C處理指針的技術(shù)拿到VB里來(lái),,并告訴你什么是可行的,,什么可行但必須要小心的,什么是可能但不可行的,,什么是根本就不可能的,。
三、 程咬金的三板斧
是的,,程序二基本上就已經(jīng)讓我們看到VB指針技術(shù)的模樣了,??偨Y(jié)一下,在VB里用指針技術(shù)我們需要掌握三樣?xùn)|西:CopyMemory,,VarPtr/StrPtr/ObjPtr, AdressOf. 三把斧頭,,程咬金的三板斧,在VB里Hack的工具,。
1,、CopyMemory
關(guān)于CopyMemory和Bruce McKinney大師的傳奇,MSDN的Knowledge Base中就有文章介紹,,你可以搜索"ID: Q129947"的文章,。正是這位大師給32位的VB帶來(lái)了這個(gè)可以移動(dòng)內(nèi)存的API,也正是有了這個(gè)API,,我們才能利用指針完成我們?cè)瓉?lái)想都不敢想的 一些工作,,感謝Bruce McKinney為我們帶來(lái)了VB的指針革命。
如CopyMemory的聲明,,它是定義在Kernel32.dll中的RtlMoveMemory這個(gè)API,,32位C函數(shù)庫(kù)中的memcpy就是這個(gè) API的包裝,如MSDN文檔中所言,,它的功能是將從Source指針?biāo)柑庨_始的長(zhǎng)度為Length的內(nèi)存拷貝到Destination所指的內(nèi)存處,。 它不會(huì)管我們的程序有沒(méi)有讀寫該內(nèi)存所應(yīng)有的權(quán)限,一但它想讀寫被系統(tǒng)所保護(hù)的內(nèi)存時(shí),,我們就會(huì)得到著名的Access Violation Fault(內(nèi)存越權(quán)訪問(wèn)錯(cuò)誤),,甚至?xí)鸶?span lang=EN-US>general protection (GP) fault(通用保護(hù)錯(cuò)誤) 。所以,,在進(jìn)行本系列文章里的實(shí)驗(yàn)時(shí),,請(qǐng)注意隨時(shí)保存你的程序文件,在VB集成環(huán)境中將"工具"->"選項(xiàng)"中的"環(huán)境"選項(xiàng)卡里的"啟動(dòng)程序時(shí)" 設(shè)為"保存改變",,并記住在"立即"窗口中執(zhí)行危險(xiǎn)代碼之前一定要保存我們的工作成果,。
2、VatPtr/StrPtr/ObjPtr
它們是VB提供給我們的好寶貝,,它們是VBA函數(shù)庫(kù)中的隱藏函數(shù),。為什么要隱藏,?因?yàn)?span lang=EN-US>VB開發(fā)小組,,不鼓勵(lì)我們用指針嘛。
實(shí)際上這三個(gè)函數(shù)在VB運(yùn)行時(shí)庫(kù)MSVBVM60.DLL(或MSVBVM50.DLL)中是同一個(gè)函數(shù)VarPtr(可參見我在本系列第一篇文章里介紹的方法),。
其庫(kù)型庫(kù)定義如下:
[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運(yùn)行時(shí)庫(kù)中的同一個(gè)函數(shù),,我們也可以在VB里用API方式重新聲明這幾個(gè)函數(shù),如下:
Private Declare Function ObjPtr Lib "MSVBVM60" Alias "VarPtr" _
(var As Object) As Long
Private Declare Function VarPtr Lib "MSVBVM60" _
(var As Any) As Long
(沒(méi)有StrPtr,,是因?yàn)?span lang=EN-US>VB對(duì)字符串處理方式有點(diǎn)不同,,這方面的問(wèn)題太多,,在本系列中另用一篇《VB字符串全攻略》來(lái)詳談。
順便提一下,,聽說(shuō)VB.NET里沒(méi)有這幾個(gè)函數(shù),,但只要還能調(diào)用API,我們就可以試試上面的幾個(gè)聲明,,這樣在VB.NET里我們一樣可以進(jìn)行指針操作,。
但是請(qǐng)注意,如果通過(guò)API調(diào)用來(lái)使用VarPtr,,整個(gè)程序二SwapPtr將比原來(lái)使用內(nèi)置VarPtr函數(shù)時(shí)慢6倍,。)
如果你喜歡刨根問(wèn)底,那么下面就是VarPtr函數(shù)在C和匯編語(yǔ)言里的樣子:
在C里樣子是這樣的:
long VarPtr(void* pv){
return (long)pv;
}
所對(duì)就的匯編代碼就兩行:
mov eax,dword ptr [esp+4]
ret 4 注釋:彈出棧里參數(shù)的值并返回,。
之所以讓大家了解VarPtr的具體實(shí)現(xiàn),,是想告訴大家它的開銷并不大,因?yàn)樗鼈儾贿^(guò)兩條指令,,即使加上參數(shù)賦值,、壓棧和調(diào)用指令,整個(gè)獲取指針的過(guò)程也就六條指令,。當(dāng)然,,同樣的功能在C語(yǔ)言里,由于語(yǔ)言的直接支持,,僅需要一條指令即可,。但在VB里,它已經(jīng)算是最快的函數(shù)了,,所以我們完全不用擔(dān)心使用 VarPtr會(huì)讓我們失去效率,!速度是使用指針技術(shù)的根本要求。
一句話,,VarPtr返回的是變量所在處的內(nèi)存地址,,也可以說(shuō)返回了指向變量?jī)?nèi)存位置的指針,它是我們?cè)?span lang=EN-US>VB里處理指針最重要的武器之一,。
3,、ByVal和ByRef
ByVal傳遞的參數(shù)值,而ByRef傳遞的參數(shù)的地址,。在這里,,我們不用去區(qū)別傳指針/傳地址/傳引用的不同,在VB里,,它們根本就是一個(gè)東西的三種不同說(shuō)法,,即使VB的文檔里也有地方在混用這些術(shù)語(yǔ)(但在C++里的確要區(qū)分指針和引用)
初次接觸上面的程序二SwapPtr的朋友,一定要搞清在里面的CopyMemory調(diào)用中,在什么地方要加ByVal,,什么地方不加(不加ByVal就是使用VB缺省的ByRef)
準(zhǔn)確的理解傳值和傳地址(指針)的區(qū)別,,是在VB里正確使用指針的基礎(chǔ)。
現(xiàn)在一個(gè)最簡(jiǎn)單的實(shí)驗(yàn)來(lái)看這個(gè)問(wèn)題,,如下面的程序三:
【程序三】:注釋:體會(huì)ByVal和ByRef
Sub TestCopyMemory()
Dim k As Long
k = 5
Note: CopyMemory ByVal VarPtr(k), 40000, 4
Debug.Print k
End Sub
上面標(biāo)號(hào)Note處的語(yǔ)句的目的,,是將k賦值為40000,等同于語(yǔ)句k=40000,,你可以在"立即"窗口試驗(yàn)一下,,會(huì)發(fā)現(xiàn)k的值的確成了40000。
實(shí)際上上面這個(gè)語(yǔ)句,,翻譯成白話,,就是從保存常數(shù)40000的臨時(shí)變量處拷貝4個(gè)字節(jié)到變量k所在的內(nèi)存中。
現(xiàn)在我們來(lái)改變一個(gè)Note處的語(yǔ)句,,若改成下面的語(yǔ)句:
Note2: CopyMemory ByVal VarPtr(k), ByVal 40000, 4
這句話的意思就成了,,從地址40000拷貝4個(gè)字節(jié)到變量k所在的內(nèi)存中。由于地址40000所在的內(nèi)存我們無(wú)權(quán)訪問(wèn),,操作系統(tǒng)會(huì)給我們一個(gè)Access Violation內(nèi)存越權(quán)訪問(wèn)錯(cuò)誤,,告訴我們"試圖讀取位置0x00009c40處內(nèi)存時(shí)出錯(cuò),該內(nèi)存不能為注釋:Read注釋:",。
我們?cè)俑某扇缦碌恼Z(yǔ)句看看,。
Note3: CopyMemory VarPtr(k), 40000, 4
這句話的意思就成了,從保存常數(shù)40000的臨時(shí)變量處拷貝4個(gè)字節(jié)到到保存變量k所在內(nèi)存地址值的臨時(shí)變量處,。這不會(huì)出出內(nèi)存越權(quán)訪問(wèn)錯(cuò)誤,,但k的值并沒(méi)有變。
我們可以把程序改改以更清楚的休現(xiàn)這種區(qū)別,,如下面的程序四:
【程序四】:注釋:看看我們的東西被拷貝到哪兒去了
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 的值卻沒(méi)有變化。但是,,在NOTE4前有:i=VarPtr(k),,本意是要把i本身做為一個(gè)指針來(lái)使用。這時(shí),,我們必須如NOTE5那樣用ByVal來(lái) 傳遞指針i,,由于i是指向變量k的指針,所以最后常量40000被拷貝了變量k里,。
希望你已經(jīng)理解了這種區(qū)別,,在后面問(wèn)題的討論中,我還會(huì)再談到它,。
4,、AddressOf
它用來(lái)得到一個(gè)指向VB函數(shù)入口地址的指針,不過(guò)這個(gè)指針只能傳遞給API使用,,以使得API能回調(diào)VB函數(shù),。
本文不準(zhǔn)備詳細(xì)討論函數(shù)指針,關(guān)于它的使用請(qǐng)參考VB文檔,。
5,、拿來(lái)主義。
實(shí)際上,,有了CopyMemory,,VarPtr,AddressOf這三把斧頭,,我們已經(jīng)可以將C里基本的指針操作拿過(guò)來(lái)了,。
如下面的C程序包括了大部分基本的指針指針操作:
struct POINT{
int x; int y;
};
int Compare(void* elem1, void* elem2){}
void PtrDemo(){
//指針聲明:
char c = 注釋:X注釋:; //聲明一個(gè)char型變量
char* pc; long* pl; //聲明普通指針
POINT* pPt; //聲明結(jié)構(gòu)指針
void* pv; //聲明無(wú)類型指針
int (*pfnCastToInt)(void *, void*);//聲明函數(shù)指針:
//指針賦值:
pc = &c; //將變量c的地址值賦給指針pc
pfnCompare = Compare; //函數(shù)指針賦值。
//指針取值:
c = *pc; //將指針pc所指處的內(nèi)存值賦給變量c
//用指針賦值:
*pc = 注釋:Y注釋: //將注釋:Y注釋:賦給指針pc所指內(nèi)存變量里,。
//指針移動(dòng):
pc++; pl--;
}
這些對(duì)指針操作在VB里都有等同的東西,,
前面討論ByVal和ByRef時(shí)曾說(shuō)過(guò)傳指針和傳地址是一回事,實(shí)際上當(dāng)我們?cè)?span lang=EN-US>VB里用缺省的ByRef聲明函數(shù)參數(shù)時(shí),,我們已經(jīng)就聲明了指針,。
如一個(gè)C聲明的函數(shù):long Func(char* pc)
其對(duì)應(yīng)的VB聲明是:Function Func(pc As Byte) As Long
這時(shí)參數(shù)pc使用缺省的ByRef傳地址方式來(lái)傳遞,這和C里用指針來(lái)傳遞參數(shù)是一樣,。
那么怎么才能象C里那樣明確地聲明一個(gè)指針呢,?
很簡(jiǎn)單,如前所說(shuō),,用一個(gè)32位長(zhǎng)整數(shù)來(lái)表達(dá)指針就行,。在VB里就是用Long型來(lái)明確地聲明指針,我們不用區(qū)分是普通指針,、無(wú)類型指針還是函數(shù)指針,,通 通都可用Long來(lái)聲明。而給一個(gè)指針賦值,,就是賦給它用VarPar得到的另一個(gè)變量的地址,。具體見程序五。
【程序五】:同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) 注釋:對(duì)應(yīng)C里的long、int型指針
pc = VarPtr(c) 注釋:對(duì)應(yīng)char,、short型指針
pPt = VarPtr(Pt) 注釋:結(jié)構(gòu)指針
pv = VarPtr(ca(0)) 注釋:字節(jié)數(shù)組指針,,可對(duì)應(yīng)任何類型,,也就是void*
pfnCompare = FnPtrToLong(AddressOf Compare) 注釋:函數(shù)指針
CopyMemory c, ByVal pc, LenB(c) 注釋:用指針取值
CopyMemory ByVal pc, AscB("Y"), LenB(c) 注釋:用指針賦值
pc = pc + LenB(c) : pl = pl - LenB(l) 注釋:指針移動(dòng)
End Sub
我們看到,由于VB不直接支持指針操作,,在VB里用指針取值和用指針賦值都必須用CopyMemory這個(gè)API,,而調(diào)用API的代價(jià)是比較高的,這就決定了我們?cè)?span lang=EN-US>VB里使用指針不能象在C里那樣自由和頻繁,,我們必須要考慮指針操作的代價(jià),,在后面的"指針應(yīng)用"我們會(huì)再變談這個(gè)問(wèn)題。
程序五中關(guān)于函數(shù)指針的問(wèn)題請(qǐng)參考VB文檔,,無(wú)類型指針void*會(huì)在下面"關(guān)于Any的問(wèn)題"里說(shuō),。
程序五基本上已經(jīng)包括了我們能在VB里進(jìn)行的所有指針操作,僅此而已,。
下面有一個(gè)小測(cè)試題,,如果現(xiàn)在你就弄懂了上面程咬金的三板斧,你就應(yīng)該能做得出來(lái),。
上面提到過(guò),,VB.NET中沒(méi)有VarPtr,我們可以用聲明API的方式來(lái)引入MSVBVM60.DLL中的VarPtr?,F(xiàn)在的問(wèn)題如果不用VB的運(yùn)行時(shí)DLL文件,,你能不能自己實(shí)現(xiàn)一個(gè)ObjPtr。答案在下一節(jié)后給出,。
四,、指針使用中應(yīng)注意的問(wèn)題
1、關(guān)于ANY的問(wèn)題
如果以一個(gè)老師的身份來(lái)說(shuō)話,,我會(huì)說(shuō):最好永遠(yuǎn)也不要用Any,!是的,我沒(méi)說(shuō)錯(cuò),,是永遠(yuǎn),!所以我沒(méi)有把它放在程咬金的三板斧里。當(dāng)然,,這個(gè)問(wèn)題和是不是應(yīng)該使用指針這個(gè)問(wèn)題一樣會(huì)引發(fā)一場(chǎng)沒(méi)有結(jié)果的討論,,我告訴你的只是一個(gè)觀點(diǎn),因?yàn)橛袝r(shí)我們會(huì)為了效率上的一點(diǎn)點(diǎn)提高或想偷一點(diǎn)點(diǎn)懶而去用Any,,但這樣做 需要要承擔(dān)風(fēng)險(xiǎn),。
Any不是一個(gè)真正的類型,它只是告訴VB編譯器放棄對(duì)參數(shù)類型的檢查,,這樣,,理論上,我們可以將任何類型傳遞給API,。
Any在什么地方用呢,?讓我們來(lái)看看,,在VB文檔里的是怎么說(shuō)的,現(xiàn)在就請(qǐng)打開MSDN(Visual Studio 6自帶的版本),,翻到"Visual Basic文檔"->"使用Visual Basic"->"部件工具指南"->"訪問(wèn)DLL和Windows API"部分,,再看看"將 C 語(yǔ)言聲明轉(zhuǎn)換為 Visual Basic 聲明"這一節(jié)。文檔里告訴我們,,只有C的聲明為LPVOID和NULL時(shí),我們才用Any,。實(shí)際上如果你愿意承擔(dān)風(fēng)險(xiǎn),,所有的類型你都可以用Any。當(dāng) 然,,也可以如我所說(shuō),,永遠(yuǎn)不要用Any。
為什么要這樣,?那為什么VB官方還要提供Any,?是信我的,還是信VB官方的,?有什么道理不用Any,?
如前面所說(shuō),VB官方不鼓勵(lì)我們使用指針,。因?yàn)?span lang=EN-US>VB所標(biāo)榜的優(yōu)點(diǎn)之一,,就是沒(méi)有危險(xiǎn)的指針操作,所以的內(nèi)存訪問(wèn)都是受VB運(yùn)行時(shí)庫(kù)控制的,。在這一點(diǎn)上,, JAVA語(yǔ)言也有著同樣的標(biāo)榜。但是,,同JAVA一樣,,VB要避免使用指針而得到更高的安全性,就必須要克服沒(méi)有指針而帶來(lái)的問(wèn)題,。VB已經(jīng)盡最大的努力 來(lái)使我們遠(yuǎn)離指針的同時(shí)擁有強(qiáng)類型檢查帶來(lái)的安全性,。但是操作系統(tǒng)是C寫的,里面到處都需要指針,,有些指針是沒(méi)有類型的,,就是C程序員常說(shuō)的可怕的 void*無(wú)類型指針。它沒(méi)有類型,,因此它可以表示所有類型,。如CopyMemory所對(duì)應(yīng)的是C語(yǔ)言的memcpy,它的聲明如下:
void *memcpy( void *dest, const void *src, size_t count );
因memcpy前兩個(gè)參數(shù)用的是void*,,因此任何類型的參數(shù)都可以傳遞給他,。
一個(gè)用C的程序員,,應(yīng)該知道在C函數(shù)庫(kù)里這樣的void*并不少見,也應(yīng)該知道它有多危險(xiǎn),。無(wú)論傳遞什么類型的變量指針給上面memcpy的void*,,C編譯器都不會(huì)報(bào)錯(cuò)或給任何警告。
在VB里大多數(shù)時(shí)候,,我們使用Any就是為了使用void*,,和在C里一樣,VB也不對(duì)Any進(jìn)行類型檢查,,我們也可以傳遞任何類型給Any,,VB編譯器也都不會(huì)報(bào)錯(cuò)或給任何警告。
但程序運(yùn)行時(shí)會(huì)不會(huì)出錯(cuò),,就要看使用它時(shí)是不是小心了,。正因?yàn)樵?span lang=EN-US>C里很多錯(cuò)誤是和void*相關(guān)的,所以,,C++鼓勵(lì)我們使用satic_cast<void*>來(lái)明確指出這種不安全的類型的轉(zhuǎn)換,,已利于發(fā)現(xiàn)錯(cuò)誤。
說(shuō)了這么多C/C++,,其實(shí)我是想告訴所有VB的程序員,,在使用Any時(shí),我們必須和C/C++程序員使用void*一樣要高度小心,。
VB里沒(méi)有satic_cast這種東西,,但我們可以在傳遞指針時(shí)明確的使用long類型,并且用VarPtr來(lái)取得參數(shù)的指針,,這樣至少已經(jīng)明確地指出我們?cè)谑褂梦kU(xiǎn)的指針,。如程序二經(jīng)過(guò)這樣的處理就成了下面的程序:
【程序五】:注釋:使用更安全的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位的地址值,,當(dāng)我們將一個(gè)別的類型傳遞給這個(gè)API時(shí),編譯器會(huì)報(bào)錯(cuò),,比如現(xiàn)在我們用下面的語(yǔ)句:
【程序六】:注釋:有點(diǎn)象【程序四】,,但將常量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)
注釋:下面的語(yǔ)句會(huì)引起類型不符的編譯錯(cuò)誤,,這是好事,!
注釋:CopyMemory i, z, 4
注釋:應(yīng)該用下面的
CopyMemory i, ByVal VarPtr(z), 2
Debug.Print k
End Sub
編譯會(huì)出錯(cuò)!是好事,!這總比運(yùn)行時(shí)不知道錯(cuò)在哪兒好,!
象程序四那樣使用Any類型來(lái)聲明CopyMemory的參數(shù),,VB雖然不會(huì)報(bào)錯(cuò),但運(yùn)行時(shí)結(jié)果卻是錯(cuò)的,。不信,,你試試將程序四中的40000改為1,結(jié) 果i的值不是我們想要的1,,而是327681,。為什么在程序四中,常量為1時(shí)結(jié)果會(huì)出錯(cuò),,而常量為40000時(shí)結(jié)果就不錯(cuò),?
原因是VB對(duì)函數(shù)參數(shù)中的常量按Variant的方式處理。是1時(shí),,由于1小于Integer型的最大值32767,,VB會(huì)生成一個(gè)存儲(chǔ)值1的 Integer型的臨時(shí)變量,,也就是說(shuō),,當(dāng)我們想將1用CopyMemroy拷貝到Long型的變量i時(shí),這個(gè)常量1是實(shí)際上是Integer型臨時(shí)變量,!VB里Integer類型只有兩個(gè)字節(jié),,而我們實(shí)際上拷貝了四個(gè)字節(jié)。知道有多危險(xiǎn)了吧,!沒(méi)有出內(nèi)存保護(hù)錯(cuò)誤那只是我們的幸運(yùn),!
如果一定要解釋一下為什么i最后變成了327681,這是因?yàn)槲覀儗?span lang=EN-US>k的低16位的值5也拷貝到了i值的高16位中去了,,因此有5*65536+1= 327681,。詳談這個(gè)問(wèn)題涉及到VB局部變量聲明順序,CopyMemory參數(shù)的壓棧順序,,long型的低位在前高位在后等問(wèn)題,。如果你對(duì)這些問(wèn)題感 興趣,可以用本系列第一篇文章所提供的方法(DebugBreak這個(gè)API和VC調(diào)試器)來(lái)跟蹤一下,,可以加深你對(duì)VB內(nèi)部處理方式的認(rèn)識(shí),,由于這和本 文討論的問(wèn)題無(wú)關(guān),所以就不詳談了,。到這里,,大家應(yīng)該明白,程序三和程序四實(shí)際上有錯(cuò)誤?。,。∥以谏厦嬗贸A?span lang=EN-US>40000而不用1,,不是為了在文章中湊字 數(shù),,而是因?yàn)?span lang=EN-US>40000這個(gè)常量大于32767,,會(huì)被VB解釋成我們需要的Long型的臨時(shí)變量,只有這樣程序三和程序四才能正常工作,。對(duì)不起,,我這樣有 意的隱藏錯(cuò)誤只是想加深你對(duì)Any危害的認(rèn)識(shí)。
總之,,我們要認(rèn)識(shí)到,,編譯時(shí)就找到錯(cuò)誤是非常重要的,因?yàn)槟泷R上就知道錯(cuò)誤的所在,。所以我們應(yīng)該象程序五和程序六那樣明確地用long型的ByVal的指針,,而不要用Any的ByRef的指針。
但用Any已經(jīng)如此的流行,,以至很多大師們也用它,。它唯一的魅力就是不象用Long型指針那樣,需要我們自己調(diào)用VarPtr來(lái)得到指針,,所有處理指針的工作由VB編譯器來(lái)完成,。所以在參數(shù)的處理上,只用一條匯編指令:push [i],,而用VarPtr時(shí),,由于需要函數(shù)調(diào)用,因此要多用五條匯編指令,。五條多余的匯編指令有時(shí)的確能我們冒著風(fēng)險(xiǎn)去用Any,。
VB開發(fā)小組提供Any,就是想用ByRef xxx As Any來(lái)表達(dá)void* xxx,。我們也完全可以使用VarPtr和Long型的指針來(lái)處理,。我想,VB開發(fā)小組也曾猶豫過(guò)是公布VarPtr,,還是提供Any,,最后他們決定還是 提供Any,而繼續(xù)隱瞞VarPtr,。的確,,這是個(gè)兩難的決定。但是經(jīng)過(guò)我上面的分析,,我們應(yīng)該知道,,這個(gè)決定并不符合VB所追求的"更安全"的初衷。因?yàn)樗赡軙?huì)隱藏類型不符的錯(cuò)誤,,調(diào)試和找到這種運(yùn)行時(shí)才產(chǎn)生的錯(cuò)誤將花貴更多的時(shí)間和精力,。
所以我有了"最好永遠(yuǎn)不要用Any"這個(gè)"驚人"的結(jié)論。
不用Any的另一個(gè)好處是,簡(jiǎn)化了我們將C聲明的API轉(zhuǎn)換成VB聲明的方式,,現(xiàn)在它變成了一句話:除了VB內(nèi)置的可以進(jìn)行類型檢查的類型外,,所以其它的類型我們都應(yīng)該聲明成Long型。
2,、關(guān)于NULL的容易混淆的問(wèn)題
有很多文章講過(guò),,一定要記在心里:
VbNullChar 相當(dāng)于C里的注釋:\0注釋:,在用字節(jié)數(shù)組構(gòu)造C字串時(shí)常用它來(lái)做最后1個(gè)元素,。
vbNullString 這才是真正的NULL,,就是0,在VB6中直接用0也可以,。
只有上面的兩個(gè)是API調(diào)用中會(huì)用的,。還有Empty、Null是Variant,,而Nothing只和類對(duì)象有關(guān),,一般API調(diào)用中都不會(huì)用到它們。
另:本文第三節(jié)曾提出一個(gè)小測(cè)驗(yàn)題,,做出來(lái)了嗎,?現(xiàn)在公布正確答案:
【測(cè)驗(yàn)題答案】
Function ObjPtr(obj as Object) as long
Dim lpObj As Long
CopyMemory lpObj, Obj, 4
ObjectPtr = lpObj
End Function
五、VB指針應(yīng)用
如前面所說(shuō)VB里使用指針不象C里那樣靈活,,用指針處理數(shù)據(jù)時(shí)都需要用CopyMemory將數(shù)據(jù)在指針和VB能夠處理的變量之間來(lái)回拷貝,,這需要很大的額外開銷,。因此不是所有C里的指針操作都可以移值到VB里來(lái),,我們只應(yīng)在需要的時(shí)候才在VB里使用指針。
1,、動(dòng)態(tài)內(nèi)存分配:完全不可能,、可能但不可行,VB標(biāo)準(zhǔn)
在C和C++里頻繁使用指針的一個(gè)重要原因是需要使用動(dòng)態(tài)內(nèi)存分配,,用Malloc或New來(lái)從堆棧里動(dòng)態(tài)分配內(nèi)存,,并得到指向這個(gè)內(nèi)存的指針。在VB里我們也可以自己
用API來(lái)實(shí)現(xiàn)動(dòng)態(tài)分配內(nèi)存,,并且實(shí)現(xiàn)象C里的指針鏈表,。
但我們不可能象C那樣直接用指針來(lái)訪問(wèn)這樣動(dòng)態(tài)分配的內(nèi)存,訪問(wèn)時(shí)我們必須用CopyMemory將數(shù)據(jù)拷貝到VB的變量?jī)?nèi),,大量的使用這種技術(shù)必然會(huì)降 低效率,,以至于要象C那樣用指針來(lái)使用動(dòng)態(tài)內(nèi)存根本就沒(méi)有可行性。要象C,、PASCAL那樣實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu),,在VB里還是應(yīng)該老老實(shí)實(shí)用對(duì)象技術(shù)來(lái)實(shí) 現(xiàn)。
本文配套代碼中的LinkedList里有完全用指針實(shí)現(xiàn)的鏈表,,它是使用HeapAlloc從堆棧中動(dòng)態(tài)分配內(nèi)存,,另有一個(gè)調(diào)用 FindFirstUrlCacheEntry這個(gè)API來(lái)操作IE的Cache的小程序IECache,,它使用了VirtualAlloc來(lái)動(dòng)態(tài)分配內(nèi) 存。但實(shí)際上這都不是必須的,,VB已經(jīng)為我們提供了標(biāo)準(zhǔn)的動(dòng)態(tài)內(nèi)存分配的方法,,那就是:
對(duì)象、字符串和字節(jié)數(shù)組
限于篇幅,,關(guān)于對(duì)象的技術(shù)這里不講,,LinkedList的源代碼里有用對(duì)象實(shí)現(xiàn)的鏈表,你可以參考,。
字符串可以用Space$函數(shù)來(lái)動(dòng)態(tài)分配,,VB的文檔里就有詳細(xì)的說(shuō)明。
關(guān)于字節(jié)數(shù)組,,這里要講講,,它非常有用。我們可用Redim來(lái)動(dòng)態(tài)改變它的大小,,并將指向它第一個(gè)元素的指針傳給需要指針的API,,如下:
dim ab() As Byte , ret As long
注釋:傳遞Null值API會(huì)返回它所需要的緩沖區(qū)的長(zhǎng)度。
ret = SomeApiNeedsBuffer(vbNullString)
注釋:動(dòng)態(tài)分配足夠大小的內(nèi)存緩沖區(qū)
ReDim ab(ret) As Byte
注釋:再次把指針傳給API,,此時(shí)傳字節(jié)數(shù)組第一個(gè)元素的指針,。
SomeApiNeedsBuffer(ByVal VarPtr(ab(1)))
在本文配套程序中的IECache中,我也提供了用字節(jié)數(shù)組來(lái)實(shí)現(xiàn)動(dòng)態(tài)分配緩沖區(qū)的版本,,比用VirtualAlloc來(lái)實(shí)現(xiàn)更安全更簡(jiǎn)單,。
2、突破限制
下面是一個(gè)突破VB類型檢查來(lái)實(shí)現(xiàn)特殊功能的經(jīng)典應(yīng)用,,出自Bruce Mckinney的《HardCore Visual Basic》一書,。
將一個(gè)Long長(zhǎng)整數(shù)的低16位作為Interger型提取出來(lái),
【程序七】 注釋:標(biāo)準(zhǔn)的方法,,也是高效的方法,,但不容易理解。
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
【程序八】 注釋:用指針來(lái)做效率雖不高,,但思想清楚,。
Function LoWord(ByVal dw As Long) As Integer
CopyMemory ByVal VarPtr(LoWord), ByVal VarPtr(dw), 2
End Function
3、對(duì)數(shù)組進(jìn)行批量操作
用指針進(jìn)行大批量數(shù)組數(shù)據(jù)的移動(dòng),,從效率上考慮是很有必要的,,看下面的兩個(gè)程序,它們功能都是將數(shù)組的前一半數(shù)據(jù)移到后一半中:
【程序九】:注釋:標(biāo)準(zhǔn)的移動(dòng)數(shù)組的做法
Private Sub ShitArray(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 ShitArrayByPtr(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
當(dāng)數(shù)組較大,,移動(dòng)操作較多(比如用數(shù)組實(shí)現(xiàn)HashTable)時(shí)程序十比程序九性能上要好得多,。
程序十中又介紹兩個(gè)在指針操作中會(huì)用到的API: ZeroMemory是用來(lái)將內(nèi)存清零;FillMemory用同一個(gè)字節(jié)來(lái)填充內(nèi)存。當(dāng)然,,這兩個(gè)API的功能,,也完全可以用CopyMemory來(lái)完 成。象在C里一樣,,作為一個(gè)好習(xí)慣,,在VB里我們也可以明確的用ZeroMemory來(lái)對(duì)數(shù)組進(jìn)行初始化,用FillMemory在不立即使用的內(nèi)存中填 入怪值,,這有利于調(diào)試,。
4、最后的一點(diǎn)
當(dāng)然,,VB指針的應(yīng)用決不止這些,,還有什么應(yīng)用就要靠自己去摸索了。對(duì)于對(duì)象指針和字符串指針的應(yīng)用我會(huì)另寫文章來(lái)談,,做為本文的結(jié)束和下一篇文章《VB字符串全攻略》的開始,,我在這里給出交換兩個(gè)字符串的最快的方法:
【程序十一】注釋:交換兩個(gè)字符串最快的方法
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
對(duì)不起,為了一點(diǎn)點(diǎn)效率,,又用了Any,!關(guān)于StrPtr,下一篇文章我會(huì)來(lái)談,。
自己來(lái)試試吧,!欲練神功,趕快行動(dòng),!