C++字符串完全指引之二 —— 字符串封裝類
C++字符串完全指引之二 —— 字符串封裝類
Rule #1 of string classes 使用cast來實現類型轉換是不好的做法,除非有文檔明確指出這種轉換可以使用。 void SomeFunc ( LPCWSTR widestr ); main() { SomeFunc ( (LPCWSTR) "C://foo.txt" ); // WRONG! }肯定會失敗。它可以被編譯,因為cast操作會撤消編譯器的類型檢查。但是,編譯可以通過並不能說明代碼是正確的。 在下麵的例子中,我將會指明cast在什麼時候使用是合法的。 C-style strings and typedefs 正如我在第一部分中提到的,windows APIs 是用TCHARs來定義的,在編譯時,它可以根據你是否定義_MBCS或者_UNICODE被編譯成MBCS或者Unicode字符。你可以參看第一部分中對TCHAR的完整描述,這裏為了方便,我列出了字符的typedefs
一個增加的字符類型是OLETYPE。它表示自動化接口(如word提供的可以使你操作文檔的接口)中使用的字符類型。這種類型一般被定義成wchar_t,然而如果你定義了OLE2ANSI預處理標記,OLECHAR將會被定義成char類型。我知道現在已經沒有理由定義OLE2ANSI(從MFC3以後,微軟已經不使用它了),所以從現在起我將把OLECHAR當作Unicode字符。
還有兩個用於包圍字符串和字符常量的宏定義,它們可以使同樣的代碼被用於MBCS和Unicode builds :
在文檔或例程中,你還會看到好多_T的變體。有四個等價的宏定義,它們是TEXT, _TEXT, __TEXT和__T,它們都起同樣的做用。
注意字符串的長度是如何被加到字符串數據中的。長度是DWORD類型的,保存了字符串中包含的字節數,但不包括結束標記。在這個例子中,"Bob"包含3個Unicode字符(不包括結束符),總共6個字節。字符串的長度被預先存儲好,以便當一個BSTR在進程或者計算機之間被傳遞時,COM庫知道多少數據需要傳送。(另一方麵,一個BSTR能夠存儲任意數據塊,而不僅僅是字符,它還可以包含嵌入在數據中的0字符。然而,由於這篇文章的目的,我將不考慮那些情況)。 BSTR bstr = NULL; bstr = SysAllocString ( L"Hi Bob!" ); if ( NULL == bstr ) // out of memory error // Use bstr here... SysFreeString ( bstr );自然的,各種各樣的BSTR封裝類為你實現內存管理。 另外一個用在自動化接口中的變量類型是VARIANT。它被用來在無類型(typeless)語言,如Jscript和VBScript,來傳遞數據。一個VARIANT可能含有很多不同類型的數據,例如long和IDispatch*。當一個VARIANT包含一個字符串,字符串被存成一個BSTR。當我後麵講到VARIANT封裝類時,我會對VARIANT多些介紹。 字符串封裝類 到目前為止,我已經介紹了各種各樣的字符串。下麵,我將說明封裝類。對於每個封裝類,我將展示怎樣創建一個對象及怎樣把它轉換成一個C語言風格的字符串指針。C語言風格的字符串指針對於API的調用,或者創建一個不同的字符串類對象經常是必需的。我不會介紹字符串類提供的其他操作,比如排序和比較。 重複一遍,除非你確切的明白結果代碼將會做什麼,否則不要盲目地使用cast來實現類型轉換。 CRT提供的類 _bstr_t _bstr_t是一個對BSTR的完整封裝類,實際上它隱藏了底層的BSTR。它提供各種構造函數和操作符來訪問底層的C語言風格的字符串。然而,_bstr_t卻沒有訪問BSTR本身的操作符,所以一個_bstr_t類型的字符串不能被作為輸出參數傳給一個COM方法。如果你需要一個BSTR*參數,使用ATL類CComBSTR是比較容易的方式。 一個_bstr_t字符串能夠傳給一個接收參數類型為BSTR的函數,隻是因為下列3個條件同時滿足。首先,_bstr_t有一個向wchar_t*轉換的轉換函數;其次,對編譯器而言,因為BSTR的定義,wchar_t*和BSTR有同樣的含義;第三,_bstr_t內部含有的wchar_t*指向一片按BSTR的形式存儲數據的內存。所以,即使沒有文檔說明,_bstr_t可以轉換成BSTR,這種轉換仍然可以正常進行。 // Constructing _bstr_t bs1 = "char string"; // construct from a LPCSTR _bstr_t bs2 = L"wide char string"; // construct from a LPCWSTR _bstr_t bs3 = bs1; // copy from another _bstr_t _variant_t v = "Bob"; _bstr_t bs4 = v; // construct from a _variant_t that has a string // Extracting data LPCSTR psz1 = bs1; // automatically converts to MBCS string LPCSTR psz2 = (LPCSTR) bs1; // cast OK, same as previous line LPCWSTR pwsz1 = bs1; // returns the internal Unicode string LPCWSTR pwsz2 = (LPCWSTR) bs1; // cast OK, same as previous line BSTR bstr = bs1.copy(); // copies bs1, returns it as a BSTR // ... SysFreeString ( bstr );注意_bstr_t也提供char*和wchar_t*之間的轉換操作符。這是一個值得懷疑的設計,因為即使它們是非常量字符串指針,你也一定不能使用這些指針去修改它們指向的緩衝區的內容,因為那將破壞內部的BSTR結構。 _variant_t _variant_t是一個對VARIANT的完整封裝,它提供很多構造函數和轉換函數來操作一個VARIANT可能包含的大量的數據類型。這裏,我將隻介紹與字符串有關的操作。 // Constructing _variant_t v1 = "char string"; // construct from a LPCSTR _variant_t v2 = L"wide char string"; // construct from a LPCWSTR _bstr_t bs1 = "Bob"; _variant_t v3 = bs1; // copy from a _bstr_t object // Extracting data _bstr_t bs2 = v1; // extract BSTR from the VARIANT _bstr_t bs3 = (_bstr_t) v1; // cast OK, same as previous line注意: 如果類型轉換不能被執行,_variant_t方法能夠拋出異常,所以應該準備捕獲_com_error異常。 還需要注意的是: 沒有從一個_variant_t變量到一個MBCS字符串的直接轉換。你需要創建一個臨時的_bstr_t變量,使用提供Unicode到MBCS轉換的另一個字符串類或者使用一個ATL轉換宏。 不像_bstr_t,一個_variant_t變量可以被直接作為參數傳遞給一個COM方法。_variant_t 繼承自VARIANT類型,所以傳遞一個_variant_t來代替VARIANT變量是C++語言所允許的。 STL 類 STL隻有一個字符串類,basic_string。一個basic_string管理一個以0做結束符的字符串數組。字符的類型是basic_string模般的參數。總的來說,一個basic_string類型的變量應該被當作不透明的對象。你可以得到一個指向內部緩衝區的隻讀指針,但是任何寫操作必須使用basic_string的操作符和方法。 basic_string有兩個預定義的類型:包含char的string類型和包含wchar_t的wstring類型。這裏沒有內置的包含TCHAR的類型,但是你可以使用下麵列出的代碼來實現。 // Specializations typedef basic_string tstring; // string of TCHARs // Constructing string str = "char string"; // construct from a LPCSTR wstring wstr = L"wide char string"; // construct from a LPCWSTR tstring tstr = _T("TCHAR string"); // construct from a LPCTSTR // Extracting data LPCSTR psz = str.c_str(); // read-only pointer to str''s buffer LPCWSTR pwsz = wstr.c_str(); // read-only pointer to wstr''s buffer LPCTSTR ptsz = tstr.c_str(); // read-only pointer to tstr''s buffer不像_bstr_t,一個basic_string變量不能在字符集之間直接轉換。然而,你可以傳遞由c_str()返回的指針給另外一個類的構造函數(如果這個類的構造函數接受這種字符類型)。例如: // Example, construct _bstr_t from basic_string _bstr_t bs1 = str.c_str(); // construct a _bstr_t from a LPCSTR _bstr_t bs2 = wstr.c_str(); // construct a _bstr_t from a LPCWSTRATL 類 CComBSTR CComBSTR 是 ATL 中的 BSTR 封裝類,它在某些情況下比_bstr_t有用的多。最引人注意的是CComBSTR允許訪問底層的BSTR,這意味著你可以傳遞一個CComBSTR對象給COM的方法。CComBSTR對象能夠替你自動的管理BSTR的內存。例如,假設你想調用下麵這個接口的方法: // Sample interface: struct IStuff : public IUnknown { // Boilerplate COM stuff omitted... STDMETHOD(SetText)(BSTR bsText); STDMETHOD(GetText)(BSTR* pbsText); };CComBSTR有一個操作符--BSTR方法,所以它能直接被傳給SetText()函數。還有另外一個操作--&,這個操作符返回一個BSTR*。所以,你可以對一個CComBSTR對象使用&操作符,然後把它傳給需要BSTR*參數的函數。 CComBSTR bs1; CComBSTR bs2 = "new text"; pStuff->GetText ( &bs1 ); // ok, takes address of internal BSTR pStuff->SetText ( bs2 ); // ok, calls BSTR converter pStuff->SetText ( (BSTR) bs2 ); // cast ok, same as previous lineCComBSTR有和_bstr_t相似的構造函數,然而卻沒有內置的向MBCS字符串轉換的函數。因此,你需要使用一個ATL轉換宏。 // Constructing CComBSTR bs1 = "char string"; // construct from a LPCSTR CComBSTR bs2 = L"wide char string"; // construct from a LPCWSTR CComBSTR bs3 = bs1; // copy from another CComBSTR CComBSTR bs4; bs4.LoadString ( IDS_SOME_STR ); // load string from string table // Extracting data BSTR bstr1 = bs1; // returns internal BSTR, but don''t modify it! BSTR bstr2 = (BSTR) bs1; // cast ok, same as previous line BSTR bstr3 = bs1.Copy(); // copies bs1, returns it as a BSTR BSTR bstr4; bstr4 = bs1.Detach(); // bs1 no longer manages its BSTR // ... SysFreeString ( bstr3 ); SysFreeString ( bstr4 );注意在上個例子中使用了Detach()方法。調用這個方法後,CComBSTR對象不再管理它的BSTR字符串或者說它對應的內存。這就是bstr4需要調用SysFreeString()的原因。 做一個補充說明:重載的&操作符意味著在一些STL容器中你不能直接使用CComBSTR變量,比如list。容器要求&操作符返回一個指向容器包含的類的指針,但是對CComBSTR變量使用&操作符返回的是BSTR*,而不是CComBSTR*。然而,有一個ATL類可以解決這個問題,這個類是CAdapt。例如,你可以這樣聲明一個CComBSTR的list: std::list< CAdapt<CComBSTR> > bstr_list; CAdapt提供容器所需要的操作符,但這些操作符對你的代碼是透明的。你可以把一個bstr_list當作一個CComBSTR的list來使用。 // Constructing CComVariant v1 = "char string"; // construct from a LPCSTR CComVariant v2 = L"wide char string"; // construct from a LPCWSTR CComBSTR bs1 = "BSTR bob"; CComVariant v3 = (BSTR) bs1; // copy from a BSTR // Extracting data CComBSTR bs2 = v1.bstrVal; // extract BSTR from the VARIANT不像_variant_t,這裏沒有提供針對VARIANT包含的各種類型的轉換操作符。正如上麵介紹的,你必須直接訪問VARIANT的成員並且確保這個VARIANT變量保存著你期望的類型。如果你需要把一個CComVariant類型的數據轉換成一個BSTR類型的數據,你可以調用ChangeType()方法。 CComVariant v4 = ... // Init v4 from somewhere CComBSTR bs3; if ( SUCCEEDED( v4.ChangeType ( VT_BSTR ) )) bs3 = v4.bstrVal;像_variant_t一樣,CComVariant也沒有提供向MBCS字符串轉換的轉換操作。你需要創建一個_bstr_t類型的中間變量,使用提供從Unicode到MBCS轉換的另一個字符串類,或者使用一個ATL的轉換宏。 ATL轉換宏 ATL:轉換宏是各種字符編碼之間進行轉換的一種很方便的方式,在函數調用時,它們顯得非常有用。ATL轉換宏的名稱是根據下麵的模式來命名的[源類型]2[新類型]或者[源類型]2C[新類型]。據有第二種形式的名字的宏的轉換結果是常量指針(對應名字中的"C")。各種類型的簡稱如下: A: MBCS string, char* (A for ANSI) W: Unicode string, wchar_t* (W for wide) T: TCHAR string, TCHAR* OLE: OLECHAR string, OLECHAR* (in practice, equivalent to W) BSTR: BSTR (used as the destination type only) 所以,W2A()宏把一個Unicode字符串轉換成一個MBCS字符串。T2CW()宏把一個TCHAR字符串轉轉成一個Unicode字符串常量。 // Functions taking various strings: void Foo ( LPCWSTR wstr ); void Bar ( BSTR bstr ); // Functions returning strings: void Baz ( BSTR* pbstr ); #include <atlconv.h> main() { using std::string; USES_CONVERSION; // declare locals used by the ATL macros // Example 1: Send an MBCS string to Foo() LPCSTR psz1 = "Bob"; string str1 = "Bob"; Foo ( A2CW(psz1) ); Foo ( A2CW(str1.c_str()) ); // Example 2: Send a MBCS and Unicode string to Bar() LPCSTR psz2 = "Bob"; LPCWSTR wsz = L"Bob"; BSTR bs1; CComBSTR bs2; bs1 = A2BSTR(psz2); // create a BSTR bs2.Attach ( W2BSTR(wsz) ); // ditto, assign to a CComBSTR Bar ( bs1 ); Bar ( bs2 ); SysFreeString ( bs1 ); // free bs1 memory // No need to free bs2 since CComBSTR will do it for us. // Example 3: Convert the BSTR returned by Baz() BSTR bs3 = NULL; string str2; Baz ( &bs3 ); // Baz() fills in bs3 str2 = W2CA(bs3); // convert to an MBCS string SysFreeString ( bs3 ); // free bs3 memory }正如你所看見的,當你有一個和函數所需的參數類型不同的字符串時,使用這些轉換宏是非常方便的。 MFC類 CString 因為一個MFC CString類的對象包含TCHAR類型的字符,所以確切的字符類型取決於你所定義的預處理符號。大體來說,CString 很像STL string,這意味著你必須把它當成不透明的對象,隻能使用CString提供的方法來修改CString對象。CString有一個string所不具備的優點:CString具有接收MBCS和Unicode兩種字符串的構造函數,它還有一個LPCTSTR轉換符,所以你可以把CString對象直接傳給一個接收LPCTSTR的函數而不需要調用c_str()函數。 // Constructing CString s1 = "char string"; // construct from a LPCSTR CString s2 = L"wide char string"; // construct from a LPCWSTR CString s3 ( '' '', 100 ); // pre-allocate a 100-byte buffer, fill with spaces CString s4 = "New window text"; // You can pass a CString in place of an LPCTSTR: SetWindowText ( hwndSomeWindow, s4 ); // Or, equivalently, explicitly cast the CString: SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );你可以從你的字符串表中裝載一個字符串,CString的一個構造函數和LoadString()函數可以完成它。Format()方法能夠從字符串表中隨意的讀取一個具有一定格式的字符串。 // Constructing/loading from string table CString s5 ( (LPCTSTR) IDS_SOME_STR ); // load from string table CString s6, s7; // Load from string table. s6.LoadString ( IDS_SOME_STR ); // Load printf-style format string from the string table: s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, ... );第一個構造函數看起來有點奇怪,但是這實際上是文檔說明的裝入一個字符串的方法。 注意,對一個CString變量,你可以使用的唯一合法轉換符是LPCTSTR。轉換成LPTSTR(非常量指針)是錯誤的。養成把一個CString變量轉換成LPTSTR的習慣將會給你帶來傷害,因為當你的程序後來崩潰時,你可能不知道為什麼,因為你到處都使用同樣的代碼而那時它們都恰巧正常工作。正確的得到一個指向緩衝區的非常量指針的方法是調用GetBuffer()方法。下麵是正確的用法的一個例子,這段代碼是給一個列表控件中的項設定文字: CString str = _T("new text"); LVITEM item = {0}; item.mask = LVIF_TEXT; item.iItem = 1; item.pszText = (LPTSTR)(LPCTSTR) str; // WRONG! item.pszText = str.GetBuffer(0); // correct ListView_SetItem ( &item ); str.ReleaseBuffer(); // return control of the buffer to strpszText成員是一個LPTSTR變量,一個非常量指針,因此你需要對str調用GetBuffer()。GetBuffer()的參數是你需要CString為緩衝區分配的最小長度。如果因為某些原因,你需要一個可修改的緩衝區來存放1K TCHARs,你需要調用GetBuffer(1024)。把0作為參數時,GetBuffer()返回的是指向字符串當前內容的指針。 上麵劃線的語句可以被編譯,在這種情況下,甚至可以正常起作用。但這並不意味著這行代碼是正確的。通過使用非常量轉換,你已經破壞了麵向對象的封裝,並對CString的內部實現作了某些假定。如果你有這樣的轉換習慣,你終將會陷入代碼崩潰的境地。你會想代碼為什麼不能正常工作了,因為你到處都使用同樣的代碼而那些代碼看起來是正確的。 你知道人們總是抱怨現在的軟件的bug是多麼的多嗎?軟件中的bug是因為程序員寫了不正確的代碼。難道你真的想寫一些你知道是錯誤的代碼來為所有的軟件都滿是bug這種認識做貢獻嗎?花些時間來學習使用CString的正確方法讓你的代碼在任何時間都正常工作把。 CString 有兩個函數來從一個 CString 創建一個 BSTR。它們是 AllocSysString() 和SetSysString()。 // Converting to BSTR CString s5 = "Bob!"; BSTR bs1 = NULL, bs2 = NULL; bs1 = s5.AllocSysString(); s5.SetSysString ( &bs2 ); SysFreeString ( bs1 ); SysFreeString ( bs2 );COleVariant COleVariant和CComVariant.很相似。COleVariant繼承自VARIANT,所以它可以傳給接收VARIANT的函數。然而,不像CComVariant,COleVariant隻有一個LPCTSTR構造函數。沒有對LPCSTR 和LPCWSTR的構造函數。在大多數情況下這不是一個問題,因為不管怎樣你的字符串很可能是LPCTSTRs,但這是一個需要意識到的問題。COleVariant還有一個接收CString參數的構造函數。 // Constructing CString s1 = _T("tchar string"); COleVariant v1 = _T("Bob"); // construct from an LPCTSTR COleVariant v2 = s1; // copy from a CString像CComVariant一樣,你必須直接訪問VARIANT的成員。如果需要把VARIANT轉換成一個字符串,你應該使用ChangeType()方法。然而,COleVariant::ChangeType()如果失敗會拋出異常,而不是返回一個表示失敗的HRESULT代碼。 // Extracting data COleVariant v3 = ...; // fill in v3 from somewhere BSTR bs = NULL; try { v3.ChangeType ( VT_BSTR ); bs = v3.bstrVal; } catch ( COleException* e ) { // error, couldn''t convert } SysFreeString ( bs ); WTL 類 CString WTL的CString的行為和MFC的 CString完全一樣,所以你可以參考上麵關於MFC的 CString的介紹。 CLR 和 VC 7 類 System::String是用來處理字符串的.NET類。在內部,一個String對象包含一個不可改變的字符串序列。任何對String對象的操作實際上都是返回了一個新的String對象,因為原始的對象是不可改變的。String的一個特性是如果你有不止一個String對象包含相同的字符序列,它們實際上是指向相同的對象的。相對於C++的使用擴展是增加了一個新的字符串常量前綴S,S用來代表一個受控的字符串常量(a managed string literal)。 // Constructing String* ms = S"This is a nice managed string";你可以傳遞一個非受控的字符串來創建一個String對象,但是樣會比使用受控字符串來創建String對象造成效率的微小損失。這是因為所有以S作為前綴的相同的字符串實例都代表同樣的對象,但這對非受控對象是不適用的。下麵的代碼清楚地闡明了這一點: String* ms1 = S"this is nice"; String* ms2 = S"this is nice"; String* ms3 = L"this is nice"; Console::WriteLine ( ms1 == ms2 ); // prints true Console::WriteLine ( ms1 == ms3); // prints false正確的比較可能沒有使用S前綴的字符串的方法是使用String::CompareTo() Console::WriteLine ( ms1->CompareTo(ms2) ); Console::WriteLine ( ms1->CompareTo(ms3) );上麵的兩行代碼都會打印0,0表示兩個字符串相等。 String和MFC 7 CString之間的轉換是很容易的。CString有一個向LPCTSTR的轉換操作,而String有兩個接收char* 和 wchar_t*的構造函數,因此你可以把一個CString變量直接傳給一個String的構造函數。 CString s1 ( "hello world" ); String* s2 ( s1 ); // copy from a CString反方向的轉換也很類似 String* s1 = S"Three cats"; CString s2 ( s1 );這也許會使你感到一點迷惑,但是它確實是起作用的。因為從VS.NET 開始,CString 有了一個接收String 對象的構造函數。 CStringT ( System::String* pString );對於一些快速操作,你可能想訪問底層的字符串: String* s1 = S"Three cats"; Console::WriteLine ( s1 ); const __wchar_t __pin* pstr = PtrToStringChars(s1); for ( int i = 0; i < wcslen(pstr); i++ ) (*const_cast<__wchar_t*>(pstr+i))++; Console::WriteLine ( s1 );PtrToStringChars()返回一個指向底層字符串的const __wchar_t* ,我們需要固定它,否則垃圾收集器或許會在我們正在管理它的內容的時候移動了它。 在 printf-style 格式函數中使用字符串類 當你在printf()或者類似的函數中使用字符串封裝類時你必須十分小心。這些函數包括sprintf()和它的變體,還有TRACE和ATLTRACE宏。因為這些函數沒有對添加的參數的類型檢查,你必須小心,隻能傳給它們C語言風格的字符串指針,而不是一個完整的字符串類。 例如,要把一個_bstr_t 字符串傳給ATLTRACE(),你必須使用顯式轉換(LPCSTR) 或者(LPCWSTR): _bstr_t bs = L"Bob!"; ATLTRACE("The string is: %s in line %d/n", (LPCSTR) bs, nLine); 如果你忘了使用轉換符而把整個_bstr_t對象傳給了函數,將會顯示一些毫無意義的輸出,因為_bstr_t保存的內部數據會全部被輸出。
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
作者簡介 Michael Dunn: Michael Dunn居住在陽光城市洛杉磯。他是如此的喜歡這裏的天氣以致於想一生都住在這裏。他在4年級時開始編程,那時用的電腦是Apple //e。1995年,在UCLA獲得數學學士學位,隨後在Symantec公司做QA工程師,在 Norton AntiVirus 組工作。他自學了 Windows 和 MFC 編程。1999-2000年,他設計並實現了 Norton AntiVirus的新界麵。 Michael 現在在 Napster(一個提供在線訂閱音樂服務的公司)做開發工作,他還開發了UltraBar,一個IE工具欄插件,它可以使網絡搜索更加容易,給了 googlebar 以沉重打擊;他還開發了 CodeProject SearchBar;與人共同創建了 Zabersoft 公司,該公司在洛杉磯和丹麥的 Odense 都設有辦事處。 他喜歡玩遊戲。愛玩的遊戲有 pinball, bike riding,偶爾還玩 PS, Dreamcasth 和 MAME 遊戲。他因忘了自己曾經學過的語言:法語、漢語、日語而感到悲哀。 Nishant S(Nish): Nish是來自印度 Trivandrum,的 Microsoft Visual C++ MVP。他從1990年開始編碼。現在,Nish為作為合同雇員在家裏為 CodeProject 工作。 他還寫了一部浪漫戲劇《Summer Love and Some more Cricket》和一本編程書籍《Extending MFC applications with the .NET Framework》。他還管理者MVP的一個網站https://www.voidnish.com/ 。在這個網站上,你可以看到他的很多關於編程方麵的思想和文章。 Nish 還計劃好了旅遊,他希望自一生中能夠到達地球上盡可能多的地方。 |
最後更新:2017-04-02 06:51:22