1. 程式人生 > >VB中String的用法及原理

VB中String的用法及原理

    在各種不同開發語言中,字串型別顯然是最常見,也是最常用的。
    常用代表它最易用,是這樣嗎?未必,越簡單,越普通,你會忽視,內裡隱藏著的陷井更容易使你中招。它往往是絆腳石,或者程式中效能的瓶頸。
    本身,我對VB語言及相關應用並不太熟,只不過近期編碼用到,有些體會。

一: 先來總結一下,常用程式語言的字串表達方式:
C:      char(wchat_t) * 或 []:  字元陣列來表示字串,以0結尾,無長度標識。
          配一堆操作函式,不好記,不好用。
C++:  std::string  basic_string<>的特化版本. 注意:其內在的資料區由預設的記憶體管理器分配。
         物件哈,提供還算好用的方法。
MFC: CString 標準的C++類. 資料項:LPTSTR 一堆可用的方法。
Windows普通API DLL:       LPTSTR 採用C標準的0結束字串
Windows擴充套件COM中:         BSTR  OLE標準, 頭上帶長度標識的字串.
Java中:                            java.lang.string 
VB中:                              String   實際上就是OLE標準的BSTR
好了,我們關心的是BSTR。
看看BSTR到底是什麼東東。
1.0:看看在Windows SDK中的定義:
typedef OLECHAR* BSTR;
typedef WCHAR OLECHAR;
typedef wchar_t WCHAR;
所以,它實際上是寬字元指標。

1.1:它的記憶體結構呢?很容易查到資料。
前置四位元組,內建字串的長度,後面是字串內容,原則上並不以'/0'結尾,長度由前置值決定。
所以,它又不簡簡單單就是寬字元指標。但從基本型別定義上來看,它與寬字元指標是可以劃等號的。

1.2:那VB中的String也就明白了。實際上是地址,是字串的首地址(注意:不是長度的首地址)
另外,String是可以裝載中間帶'/0'字元的字串的,只不過,一些顯示函式可能將其省略。
如:dim str as string
     str = "ab" & chr(0) & "cd"
     MsgBox str   '輸出:ab
     Debug.Print str '輸出 ab cd


1.3:可以看出,它很特珠,Windows提供幾個函式,用來分配和釋放它。
分配:SysAllocStringLen      SysAllocString
釋放:SysFreeString
取長度:SysStringLen
注意:分配得到的BSTR,實際上仍然是以'/0'結尾。SysStringLen似乎有點兒英雄無用武之地,呵。
比如:SysAllocStringLen 的說明文件:
Allocates a new string, copies cch characters from the passed string into it, and then appends a null character.
比如:SysAllocString,我們可以通過例子:
 BSTR bstrVal = ::SysAllocString(L"abcd");
 for(int n=0;n<::SysStringLen(bstrVal)+1;n++)
 {
  TRACE(_T("/n%d"),*(bstrVal+n));
 }
 ::SysFreeString(bstrVal);
輸出:
97
98
99
100
0
仍然有0。

1.4:VC中的用法:
無論用MFC,還是ATL,實際上,由於BSTR並不是基本型別,而它的相關操作函式也是沿用的以'/0'結尾的函式,所以,雖然它在字串中可以帶'/0',但實際上,經過相關操作後,'/0'後的內容會丟掉,這要小心。比如,我們看看CString的構造:
CString::CString(LPCWSTR lpsz)
{
 Init();
 int nSrcLen = lpsz != NULL ? wcslen(lpsz) : 0;
 if (nSrcLen != 0)
 {
  AllocBuffer(nSrcLen*2);
  _wcstombsz(m_pchData, lpsz, (nSrcLen*2)+1);
  ReleaseBuffer();
 }
}

小結:BSTR有長度標識,但是MS在實現時,為了相容以前的字串操作,再加上BSTR並不是基本型別,所以,它的分配函式和一些操作函式都是把它當作'/0'結尾來處理。千萬要注意。最好也不要用它來裝載中間帶'/0'的字串,因為,說不定什麼時候,它就丟掉了。

二:  VBr的應用中將麻煩的地方,應該集中在以下幾個方面:
2.1: VB呼叫Windows API DLL

2.1.1: 字串作為輸入引數
   這還用說嗎?
   在API中如果是LPTSTR,在VB中直接轉為Byval s as String
   原因?  為什麼 BSTR可以直接轉為 LPTSTR ?
   前面其實已經講了,再說一次吧。
   BSTR實際上是指標,指向字串頭.所以,採用ByVal指向的剛好是字串的首地址,也就是LPSTR,嘿嘿,剛剛好,符合需要,真是巧啊。

2.1.2: 字串作為輸出引數
這個麻煩一點,舉個例子吧.
如: 
    UINT GetWindowsDirectory( 
      LPTSTR lpBuffer, 
      UINT uSize 
   );
使用API Viewer得到宣告 
   Public Declare Function GetWindowsDirectory Lib "kernel32" Alias "GetWindowsDirectoryA" _ 
      (ByVal lpBuffer As String, ByVal nSize As Long) As Long
程式碼: 
    Dim sTemp As String * MAX_PATH 
    Dim lsize As Long 
    Dim str As String 
    lsize = GetWindowsDirectory(sTemp, MAX_PATH) 
    str = Left(sTemp, lsize) 
    MsgBox "Windows路徑:" & str & "長度:" & Len(str)
注意一下:
A: 引用的Windows API是A版,而不是W版,如果是W版會有錯誤.
原因,我的猜測是:   VB的API呼叫機制中,字串強制做了UNICODE到ANSI的轉換.API的DLL沒法告知調
用端,它是Unicode版還是ANSI版.如果是OCX或COM就沒問題了,因為它們有標識。沒辦法,只能用一種了, MS選了A版。
B: 引數是ByVal
道理同作為輸入時,實際上傳的指標,所以,指向的內容是可以修改地,也就可以返回了。 
C: 根據API的使用說明,可得到
[out] Pointer to the buffer to receive the null-terminated string containing the path.
需要呼叫方分配空間,因此, sTemp需要給足夠長度的空間. 
D: 返回的0結尾的字串,可以用left達到目的.
因為:VB的String對應的是BSTR型別,長度由前置位元組決定,而不是0字元決定. 

2.1.3: 字串作為Return值
這種API是不是存在呢?系統級的沒看見,一般不會這樣用,因為這存在記憶體分配的問題.
但我自已好像寫過,處理方式是:
VB宣告中可以使用long返回,然後用lstrcpy進行轉換,
說起lStrCpy就不得不說說它的作用了,它實在是很關鍵,它很神奇,它可以把
地址指向的字串拿出來。
我們來看看原理:
lStrcpy本身只是簡單的複製字串的函式:
Public Declare Function lstrcpy Lib "kernel32" Alias "lstrcpyA" (ByVal lpString1 As String, ByVal lpString2 As String) As Long 如果按照API View中的宣告,是不行的.它確實就是字串拷字串,沒辦法把字址指向的字串拷到另一串.
   好了,需要小小改動一下:
   首先:目的地沒錯,確實應該是指向字串的首地址.用Byval String就可以.第一個引數OK。
   源串呢? 因為你傳入的實際上不是String,而是long啊(上例返回的是long噢),那當然應該將宣告改成long,否則,傳入的String會變成啥,啥都不是呢?就這麼簡單。 

2.1.4: LPTSTR 都可以用VB的byval String替代嗎?
如果在結構體裡,可就不是這樣了.
如:EnumForms  列舉某個印表機的所有列印紙型。
Public Declare Function EnumForms Lib "winspool.drv" Alias "EnumFormsA" (ByVal hPrinter As Long, _ ByVal Level As Long, ByRef pForm As FORM_INFO_1, ByVal cbBuf As Long, ByRef pcbNeeded As Long, _ 
 ByRef pcReturned As Long) As Long 
對於 FORM_INFO_1的定義:
Public Type FORM_INFO_1 
        Flags As Long 
        pName As String 
        Size As SIZEL 
        ImageableArea As RECTL
End Type 
    如果按上述的方法呼叫,會出現異常.估計是在拷貝資料時,String實際上是不同於LPTSTR的,因為它的前面還有四位元組是標識長度的,如果定義為String,那在構造pName之前的長度,必將動到它不應該動到的地方,這樣,就錯了.想來不會異常退出的,但卻退出了,不知原因(我沒弄明白)

  所以,此種情況,應該將pName變為long,然後再用2.1.3提到的方法,將字串拷貝出來使用. 
         這樣,就OK了。 

引申一下:
    對於一些API中大的結構體中的指標型別,我們完全可以用long來取代它,然後傳遞0&,因為,大多數情況,我們是不需要傳入任何引數的,避免定義很多我們並不需要的型別.這樣就降低了API使用的複雜性。

2.2 : VB呼叫COM元件(或OCX控制元件)
這似乎沒啥可說的,COM元件的介面一般都是標準的BSTR,與VB的String完全相容.
VB呼叫端沒啥問題,倒是書寫COM的程式需要注意:
2.2.1: 應用端可能會傳入空指標.比如傳入vbNullString,這需要留意.
2.2.2: BSTR的釋放與普通LPTSTR可不同,使用前需要初始化,使用後要釋放,當然,VB無此煩惱.

3: VB字串的連線操作.
    當我們要序列化生成文字檔案時,大都喜歡先將內容寫入字串,然後再一次寫了檔案.否則多次寫入檔案,似乎有效率低之嫌,但如果字串為多次累加而成,那麼使用VB字串的連線操作,將是效率極差的做法,這也變成效能低下的重要原因.
    分析一下原因: 
    對於String的連線,系統的做法一定是重分配空間,搬移到新空間,拷貝進新內容.如果連線次數較多,且較細,那將是災難性的。(類似MFC的CString的做法,假設我們要寫一個100K長度的字串,但每次新增一個字元,那你可以試試它的速度)
   如何解決? 
   很簡單,採用預分配記憶體塊, 當記憶體塊不夠用時,自動增加指定塊的增量(當然每次增長的記憶體塊這個閥值需要慎重考慮) 
   這樣,減少搬移操作,效率自然提升。 
   具體實現:可以使用記憶體操作API實現.VB會麻煩一點(需要用API生成全域性記憶體,自行完成搬移,拷貝),
VC可以直接使用CMemFile簡單搞定.

4: VB字串多語言化
4.1: 一般的做法是: 將原生串與多語言串做成Key.Value Pair結構。多語言資訊為Value,然後做一個簡單的Hashc ode來索引,或者乾脆用Hash Map來實現 (為了更高效,更容易使用已有容器,最好藉助於C++編寫相關支援)。
4.2: 為了使UI上的字串也能多語言化,需要保證字串資訊為動態載入。
4.3: 對於控制元件的字型編碼字符集也需要處理,以保證正確顯示。