搞明白BSTR和WCHAR *的區別是很重要的一件事
如果你曾經使用C/C++開發過涉及COM組件的應用,則下面的代碼你應該不會陌生:STDMETHODIMP CFo:Bar(BSTR bstrABC) { // …
這個BSTR是個什麼神仙玩意兒?它和WCHAR *之間有什麼區別嗎?
像C/C++這種低層語言給予了開發者十分大的自由度,開發者可以自己決定一串二進制位採用何種模式來抽象化一些具體的概念。Unicode字符串是一個很好的例子。在C++中,用來表達有N個字符的Unicode字符串是使用一個指向一塊包含有2*(N+1)的內存區的指針,在這個內存區裡,前2*N個字節都是用2個字節的UnsignedShort整數來代表每個字符,最後兩個字節的值為0,表示一個字符串的結束。
為了方便標識名稱,我們採用了匈牙利命名法。例如我們使用PWSZ來表明:”Pointer to Wide-character String, Zero-terminated”,如果考慮到C++裡的類型系統,則PWSZ是一個指向UnsignedShort的指針。
COM組件則使用到了一種不同的方法來存儲字符串數據,這種方法在期望獲取一個PWSZ作為參數的代碼和提供COM字符串的代碼之間提供了一種很好的互操作性。但是,如果你不小心或者不瞭解這裡面的不起眼的小區別,那麼可能會給代碼帶來嚴重的Bug。
從編譯器的角度看來,BSTR也是一個指向UnsignedShort類型的指針。編譯器不會在意你在需要傳入PWSZ的地方使用BSTR,反之亦然。但這並不意味著你可以隨意的這樣使用。如果同一個事物有兩個不同的名字,那麼在某種方面它們一定是不同的。確實,PWSZ和BSTR在一些方面存在不同點。
大多數情況下,BSTR可以被看作是PWSZ,但是很少情況下,一個PWSZ會被看作為一個BSTR。
下面,讓我們來看看到底有哪些不同點,然後我們對這些不同點進行逐個的解釋。
1) 一個BSTR必須對NULL和””有相同的語義。但是一個PWSZ通常對這兩者有不同的語義。
詳細解釋:如果你編寫一個需要傳入BSTR參數的函數,則你必須接受客戶傳入NULL並將它作為一個合法的BSTR,就如同客戶傳入一個空字符串一樣。COM組件一直都遵守這個慣例,就像Visual Basic和VBScript一樣,因此,如果你希望和其他語言的客戶端有良好的互操作性,則你必須遵守這樣的慣例。如果在VB中一個字符串變量時一個空字符串,則VB可能會將它看作NULL或者一個長度為0的緩衝區 — 這完全依賴VB程序的內部實現。
對於基於PWSZ的代碼就不能這樣轉換了。通常NULL被從來表示”這個字符串的值未定義”,不能作為空字符串的同義詞。
在COM組件開發中,如果你有一些數據可能為有效或者可能未定義,則你可以將它存儲在一個VARIANT中並使用VT_NULL來表達這個數據還未定義的語義,而不是將它解析為一個NULL字符串,因為它和空字符串語義上是不同的。
2) 一個BSTR必須使用SysAlloc*函數來進行分配和釋放。一個PWSZ則可以存儲在棧上的一個自動變量或者通過malloc, new, LocalAlloc或者其他任何一個內存分配器所開闢的堆上。
詳細解釋:BSTR變量總是使用類似於SysAllocString, SysAllocStringLen和SysFreeString來進行分配和釋放。操作系統會緩存底層的內存緩衝區,所有對一個BSTR進行free或者delete調用是非常嚴重的內存操作錯誤,會直接導致堆內存損壞。類似的,通過malloc或者new來開闢內存並將它轉換為一個BSTR也是錯誤的。操作系統內部會對BSTR的內存佈局進行某種方式的假定,而你不應當嘗試對這種假定進行模擬。PWSZ則可以被任何內存分配器分配,可以分配在堆上,也可以分配到棧上。
3) 一個BSTR是包含固定的長度的。一個PWSZ可以是任意的長度,其長度僅受限於系統可用內存或者一個字符串緩衝區的結束符。
詳細解釋:在BSTR中的字符數目是一個固定值。一個10字節的BSTR包含5個Unicode字符串,如此而已。即使這些字符都是0,它也是包含5個字符。一個PWSZ可以包含比緩衝區所允許的字符數更少的字符,例如:WCHAR pwszBuf[101];pwszBuf[0] = L’X’;pwszBuf[1] = L’\0′;在上面的代碼中,pwszBuf是一個包含一個字符的字符串,但是它的長度可以長達100個字符或者是一個空字符串。
4) 一個BSTR總是指向緩衝區中的第一個有效字符。一個PWSZ可能指向一個字符串的中間或者結尾。
詳細解釋:一個BSTR總是指向緩衝區中的第一個有效字符,以下代碼是錯誤的:BSTR bstrName = SysAllocString(L”John Doe”);BSTR bstrLast = &bstrName[5]; // ERROR
bstrLast不是一個合法的BSTR,但是對於PWSZ來說,下面的代碼就沒有這個問題:WCHAR * pwszName = L”John Doe”;WCHAR * pwszLast = &pwszName[5];
5) 當分配N個字節的BSTR時,這個BSTR將可以容納N/2個寬字節字符。當你為一個PWSZ分配N個字節時,你可以存儲N/2-1個字符,因為你需要為結尾的NULL保留空間。
6) 一個BSTR可以包含任意的Unicode數據,包括0字符。一個PWSZ從來都不會包含一個0字符,除非這個0字符作為一個字符串的結束符。BSTR和PWSZ都會在它們最後一個有效字符後跟有一個0字符,但是在BSTR中,有效字符可以是一個0字符。
詳細解釋:當你瞭解了BSTR的真實內存佈局後,你應該就會明白5)和6)所描述的限制。同時,這也解釋了為什麼一個N個字符的BSTR可以容納N個字符,而不像PWSZ那樣只能容納N-1個。
當你使用SysAllocString(L”ABCDE”)時,操作系統實際會分配16個字節。前面4個字節是一個32位的整數,它代表著這個BSTR中的有效字節數,在這個例子中,這個值是10。接下來的10個字節的內存空間屬於調用者,它會被調用者提供的數據填充並傳遞給內存分配器。最後2個字節會被填充為0。當函數返回時,你會得到一個指向數據區的指針,而不是指向頭部的指針。
以上的內存佈局立即解釋了BSTR的一些特性:> 字符串的長度可以即時的被確定。SysStringLeng不會像wcslen那樣在字符串中逐個檢查來尋找結束符。它只會返回數據指針前面的那個4字節整數給你。> 這也解釋了為什麼一個指向另一個BSTR中間字符位置的BSTR是無效的。因為BSTR的頭部並不在這個數據區指針的前面,甚至它會是一個非法的頭部。
一個BSTR可以被看作是PWSZ是因為,內存分配器總是會將一個0結束符放到BSTR數據區的結尾。調用者不用擔心是否為結束符分配了足夠空間的問題。如果你需要一個5個字符的字符串,則簡單地向內存分配器提出5個字符的分配需求即可。
這就是為什麼BSTR需要使用Sys*來分配和釋放的原因,因為這些函數清楚的知道所有這些隱藏在幕後的內存信息。
7) 一個BSTR實際上可以包含一個奇數長度的字節空間,一般它會用來對二進制數據進行移動。一個PWSZ則總是包含偶數個字節且只用來存儲Unicode字符串。
詳細解釋:因為一個BSTR有一個已知的長度字段,所有它不需要0結束符。因此,0字符在一個BSTR數據區中是一個合法字符。這意味著BSTR可以存儲二進制數據。因為這個原因,BSTR經常會被用來將二進制數據列集(Marshal)為字符串。在某些特殊的場景下,一個BSTR可以包含奇數字節的數據,這不十分常見,但是你應該知道有這種可能性。
總結
以上的內容應該可以解釋為什麼一個BSTR可以被看作是一個PWSZ,而一般PWSZ不能被看作是一個BSTR。唯一一個不能將BSTR看作是PWSZ的例外情況是:
1) BSTR是NULL。
2) BSTR含有內嵌的0字符,因為基於PWSZ的代碼會比它實際的會短。
3) BSTR實際不包含字符串,而是二進制數據。
能將一個PWSZ看作是BSTR的唯一情況是:這個PWSZ實際上就是一個BSTR,它使用了正確的內存分配器(Sys*函數)。
關於匈牙利命名法
在我自己的C++代碼中,我十分謹慎的使用匈牙利命名法來表示指針實際指向的類型。當需要追蹤變量的語義信息的時候,匈牙利命名法十分有效,因為變量的語義信息可能被它的類型簽名所掩蓋。下面是我經常會使用到的一些變量前綴:
bstr –> 一個真正的BSTR
pwsz –> 一個指向0結束符的寬字符串指針
psz –> 一個指向0結束符的窄字符串指針
ch –> 一個字符
pch –> 一個指向寬字符的指針
cch –> 字符的數目
b –> 一個字節
pb –> 一個指向一個字節的指針
cb –> 字節的數目
祝閱讀愉快。