收藏.C++.字符串封裝類

引言

  因爲C語言風格的字符串容易出錯且不易管理,黑客們甚至利用可能存在的緩衝區溢出bug把C語言風格的字符串作爲攻擊目標,所以出現了很多字符串封裝類。不幸的是,在某些場合下我們不知道該使用哪個字符串類,也不知道怎樣把一個C風格的字符串轉換成一個字符串封裝類。
  這篇文章將介紹所有在Win32 API, MFC, STL, WTL 和 Visual C++ 運行庫中出現的字符串類型。我將描述每一個類的用法,告訴大家怎樣創建每一個類的對象以及怎樣把一個類轉換成其他類。受控字符串和Visual C++ 7中的類兩部分是Nish完成的。
  爲了更好的從這篇文章中受益,你必須要明白不同的字符類型和編碼,這些內容我在第一部分中介紹過。

Rule #1 of string classes 

  使用cast來實現類型轉換是不好的做法,除非有文檔明確指出這種轉換可以使用。
促使我寫這兩篇文章的原因是字符串類型轉換中經常遇到的一些問題。當我們使用cast把字符串從類型X轉換到類型Z的時候,我們不知道爲什麼代碼不能正常工作。各種各樣的字符串類型,尤其是BSTR,幾乎沒有在任何一個地方的文檔中被明確的指出可以用cast來實現類型轉換。所以我想一些人可能會使用cast來實現類型轉換並希望這種轉換能夠正常工作。
  除非源字符串是一個被明確指明支持轉換操作符的字符串包裝類,否則cast不對字符串做任何轉換。對常量字符串使用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

Type Meaning
WCHAR Unicode character (wchar_t)
TCHAR MBCS or Unicode character, depending on preprocessor settings
LPSTR  string of char (char*)
LPCSTR constant string of char (const char*)
LPWSTR  string of WCHAR (WCHAR*)
LPCWSTR  constant string of WCHAR (const WCHAR*)
LPTSTR  string of TCHAR (TCHAR*)
LPCTSTR  constant string of TCHAR (const TCHAR*)

  一個增加的字符類型是OLETYPE。它表示自動化接口(如word提供的可以使你操作文檔的接口)中使用的字符類型。這種類型一般被定義成wchar_t,然而如果你定義了OLE2ANSI預處理標記,OLECHAR將會被定義成char類型。我知道現在已經沒有理由定義OLE2ANSI(從MFC3以後,微軟已經不使用它了),所以從現在起我將把OLECHAR當作Unicode字符。
這裏給出你將會看到的一些OLECHAR相關的typedefs:

Type Meaning
OLECHAR Unicode character (wchar_t)
LPOLESTR  string of OLECHAR (OLECHAR*)
LPCOLESTR constant string of OLECHAR (const OLECHAR*)

  還有兩個用於包圍字符串和字符常量的宏定義,它們可以使同樣的代碼被用於MBCS和Unicode builds :

Type Meaning
_T(x) Prepends L to the literal in Unicode builds.
OLESTR(x) Prepends L to the literal to make it an LPCOLESTR.

  在文檔或例程中,你還會看到好多_T的變體。有四個等價的宏定義,它們是TEXT, _TEXT, __TEXT和__T,它們都起同樣的做用。

COM 中的字符串 —— BSTR 和 VARIANT

  很多自動化和COM接口使用BSTR來定義字符串。BSTRs中有幾個"陷阱",所以這裏我用單獨的部分來說明它。
  BSTR 是 Pascal-style 字符串(字符串長度被明確指出)和C-style字符串(字符串的長度要通過尋找結束符來計算)的混合產物。一個BSTR是一個Unicode字符串,它的長度是預先考慮的,並且它還有一個0字符作爲結束標記。下面是一個BSTR的示例:

06 00 00 00 42 00 6F 00 62 00 00 00
--length-- B o b EOS

  注意字符串的長度是如何被加到字符串數據中的。長度是DWORD類型的,保存了字符串中包含的字節數,但不包括結束標記。在這個例子中,"Bob"包含3個Unicode字符(不包括結束符),總共6個字節。字符串的長度被預先存儲好,以便當一個BSTR在進程或者計算機之間被傳遞時,COM庫知道多少數據需要傳送。(另一方面,一個BSTR能夠存儲任意數據塊,而不僅僅是字符,它還可以包含嵌入在數據中的0字符。然而,由於這篇文章的目的,我將不考慮那些情況)。
  在 C++ 中,一個 BSTR 實際上就是一個指向字符串中第一個字符的指針。它的定義如下:

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<tchar> 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
</tchar>

  不像_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 LPCWSTR       

ATL 類

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 line       

  CComBSTR有和_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;</ccombstr>

  CAdapt提供容器所需要的操作符,但這些操作符對你的代碼是透明的。你可以把一個bstr_list當作一個CComBSTR的list來使用。

CComVariant
  CComVariant是VARIANT的封裝類。然而,不像_variant_t,在CComVariant中VARIANT沒有被隱藏。事實上你需要直接訪問VARIANT的成員。CComVariant提供了很多構造函數來對VARIANT能夠包含的多種類型進行處理。這裏,我將只介紹和字符串相關的操作。

// 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字符串常量。
  爲了使用這些宏,需要先包含atlconv.h頭文件。你甚至可以在非ATL工程中包含這個頭文件來使用其中定義的宏,因爲這個頭文件獨立於ATL中的其他部分,不需要一個_Module全局變量。當你在一個函數中使用轉換宏時,需要把USES_CONVERSION宏放在函數的開頭。它定義了轉換宏所需的一些局部變量。
  當轉換的目的類型是除了BSTR以外的其他類型時,被轉換的字符串是存在棧中的。所以,如果你想讓字符串的生命週期比當前的函數長,你需要把這個字符串拷貝到其他的字符串類中。當目的類型是BSTR時,內存不會自動被釋放,你必須把返回值賦給一個BSTR變量或者一個BSTR封裝類以避免內存泄漏。
  下面是一些各種轉換宏的使用例子:

// 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
}      </atlconv.h>

  正如你所看見的,當你有一個和函數所需的參數類型不同的字符串時,使用這些轉換宏是非常方便的。

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 str       

  pszText成員是一個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保存的內部數據會全部被輸出。

所有類的總結

  兩個字符串類之間進行轉換的常用方式是:先把源字符串轉換成一個C語言風格的字符串指針,然後把這個指針傳遞給目的類型的構造函數。下面這張表顯示了怎樣把一個字符串轉換成一個C語言風格的字符串指針以及哪些類具有接收C語言風格的字符串指針的構造函數。

Class  string type convert to char*? convert to const char*? convert to wchar_t*? convert to const wchar_t*? convert to BSTR? construct from char*? construct from wchar_t*?
_bstr_t BSTR yes cast1 yes cast yes cast1 yes cast yes2 yes yes
_variant_t BSTR no no no cast to
_bstr_t3
cast to
_bstr_t3
yes yes
string MBCS no yes c_str() method no no no yes no
wstring Unicode no no no yes c_str() method no no yes
CComBSTR BSTR no no no yes cast to BSTR yes cast yes yes
CComVariant BSTR no no no yes4 yes4 yes yes
CString TCHAR no6 in MBCS
builds, cast
no6 in Unicode
builds, cast
no5 yes yes
COleVariant BSTR no no no yes4 yes4 in MBCS
builds
in Unicode
builds
即使 _bstr_t 提供了向非常量指針的轉換操作符,修改底層的緩衝區也會已引起GPF如果你溢出了緩衝區或者造成內存泄漏。 _bstr_t 在內部用一個 wchar_t* 來保存 BSTR,所以你可以使用 const wchar_t* 來訪問BSTR。這是一個實現細節,你可以小心的使用它,將來這個細節也許會改變。 如果數據不能轉換成BSTR會拋出一個異常。 使用 ChangeType(),然後訪問 VARIANT 的 bstrVal 成員。在MFC中,如果數據轉換不成功將會拋出異常。 這裏沒有轉換 BSTR 函數,然而 AllocSysString() 返回一個新的BSTR。 使用 GetBuffer() 方法,你可以暫時地得到一個非常量的TCHAR指針。

作者簡介

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的一個網站http://www.voidnish.com/ 。在這個網站上,你可以看到他的很多關於編程方面的思想和文章。
Nish 還計劃好了旅遊,他希望自一生中能夠到達地球上儘可能多的地方。

添加評論
單擊隱藏此項的評論。

C++字符串完全指引之一 —— Win32 字符編碼

原著:Michael Dunn

翻譯:Chengjie Sun



原文出處:CodeProject:The Complete Guide to C++ Strings, Part I

引言

  毫無疑問,我們都看到過像 TCHAR, std::string, BSTR 等各種各樣的字符串類型,還有那些以 _tcs 開頭的奇怪的宏。你也許正在盯着顯示器發愁。本指引將總結引進各種字符類型的目的,展示一些簡單的用法,並告訴您在必要時,如何實現各種字符串類型之間的轉換。
  在第一部分,我們將介紹3種字符編碼類型。瞭解各種編碼模式的工作方式是很重要的事情。即使你已經知道一個字符串是一個字符數組,你也應該閱讀本部分。一旦你瞭解了這些,你將對各種字符串類型之間的關係有一個清楚地瞭解。
  在第二部分,我們將單獨講述string類,怎樣使用它及實現他們相互之間的轉換。
字符基礎 -- ASCII, DBCS, Unicode

  所有的 string 類都是以C-style字符串爲基礎的。C-style 字符串是字符數組。所以我們先介紹字符類型。這裏有3種編碼模式對應3種字符類型。第一種編碼類型是單子節字符集(single-byte character set or SBCS)。在這種編碼模式下,所有的字符都只用一個字節表示。ASCII是SBCS。一個字節表示的0用來標誌SBCS字符串的結束。
  第二種編碼模式是多字節字符集(multi-byte character set or MBCS)。一個MBCS編碼包含一些一個字節長的字符,而另一些字符大於一個字節的長度。用在Windows裏的MBCS包含兩種字符類型,單字節字符(single-byte characters)和雙字節字符(double-byte characters)。由於Windows裏使用的多字節字符絕大部分是兩個字節長,所以MBCS常被用DBCS代替。
  在DBCS編碼模式中,一些特定的值被保留用來表明他們是雙字節字符的一部分。例如,在Shift-JIS編碼中(一個常用的日文編碼模式),0x81-0x9f之間和 0xe0-oxfc之間的值表示"這是一個雙字節字符,下一 個子節是這個字符的一部分。"這樣的值被稱作"leading bytes",他們都大於0x7f。跟隨在一個leading byte子節後面的字節被稱作"trail byte"。在DBCS中,trail byte可以是任意非0值。像SBCS一樣,DBCS字符串的結束標誌也是一個單字節表示的0。
  第三種編碼模式是Unicode。Unicode是一種所有的字符都使用兩個字節編碼的編碼模式。Unicode字符有時也被稱作寬字符,因爲它比單子節字符寬(使用了更多的存儲空間)。注意,Unicode不能被看作MBCS。MBCS的獨特之處在於它的字符使用不同長度的字節編碼。Unicode字符串使用兩個字節表示的0作爲它的結束標誌。
  單字節字符包含拉丁文字母表,accented characters及ASCII標準和DOS操作系統定義的圖形字符。雙字節字符被用來表示東亞及中東的語言。Unicode被用在COM及Windows NT操作系統內部。
  你一定已經很熟悉單字節字符。當你使用char時,你處理的是單字節字符。雙字節字符也用char類型來進行操作(這是我們將會看到的關於雙子節字符的很多奇怪的地方之一)。Unicode字符用wchar_t來表示。Unicode字符和字符串常量用前綴L來表示。例如:

wchar_t wch = L''1''; // 2 bytes, 0x0031
wchar_t* wsz = L"Hello"; // 12 bytes, 6 wide characters 

字符在內存中是怎樣存儲的

  單字節字符串:每個字符佔一個字節按順序依次存儲,最後以單字節表示的0結束。例如。"Bob"的存貯形式如下:

42 6F 62 00
B o b BOS

Unicode的存儲形式,L"Bob"

42 00 6F 00 62 00 00 00
B o b BOS

使用兩個字節表示的0來做結束標誌。

  一眼看上去,DBCS 字符串很像 SBCS 字符串,但是我們一會兒將看到 DBCS 字符串的微妙之處,它使得使用字符串操作函數和永字符指針遍歷一個字符串時會產生預料之外的結果。字符串" " ("nihongo")在內存中的存儲形式如下(LB和TB分別用來表示 leading byte 和 trail byte)

93 FA 96 7B 8C EA 00
LB TB LB TB LB TB EOS
EOS

值得注意的是,"ni"的值不能被解釋成WORD型值0xfa93,而應該看作兩個值93和fa以這種順序被作爲"ni"的編碼。

使用字符串處理函數

  我們都已經見過C語言中的字符串函數,strcpy(), sprintf(), atoll()等。這些字符串只應該用來處理單字節字符字符串。標準庫也提供了僅適用於Unicode類型字符串的函數,比如wcscpy(), swprintf(), wtol()等。
  微軟還在它的CRT(C runtime library)中增加了操作DBCS字符串的版本。Str***()函數都有對應名字的DBCS版本_mbs***()。如果你料到可能會遇到DBCS字符串(如果你的軟件會被安裝在使用DBCS編碼的國家,如中國,日本等,你就可能會),你應該使用_mbs***()函數,因爲他們也可以處理SBCS字符串。(一個DBCS字符串也可能含有單字節字符,這就是爲什麼_mbs***()函數也能處理SBCS字符串的原因)
  讓我們來看一個典型的字符串來闡明爲什麼需要不同版本的字符串處理函數。我們還是使用前面的Unicode字符串 L"Bob":

42 00 6F 00 62 00 00 00
B o b BOS

  因爲x86CPU是little-endian,值0x0042在內存中的存儲形式是42 00。你能看出如果這個字符串被傳給strlen()函數會出現什麼問題嗎?它將先看到第一個字節42,然後是00,而00是字符串結束的標誌,於是strlen()將會返回1。如果把"Bob"傳給wcslen(),將會得出更壞的結果。wcslen()將會先看到0x6f42,然後是0x0062,然後一直讀到你的緩衝區的末尾,直到發現00 00結束標誌或者引起了GPF。
  到目前爲止,我們已經討論了str***()和wcs***()的用法及它們之間的區別。Str***()和_mbs**()之間的有區別區別呢?明白他們之間的區別,對於採用正確的方法來遍歷DBCS字符串是很重要的。下面,我們將先介紹字符串的遍歷,然後回到str***()與_mbs***()之間的區別這個問題上來。

正確的遍歷和索引字符串

  因爲我們中大多數人都是用着SBCS字符串成長的,所以我們在遍歷字符串時,常常使用指針的++-和-操作。我們也使用數組下標的表示形式來操作字符串中的字符。這兩種方式是用於SBCS和Unicode字符串,因爲它們中的字符有着相同的寬度,編譯器能正確的返回我們需要的字符。
  然而,當碰到DBCS字符串時,我們必須拋棄這些習慣。這裏有使用指針遍歷DBCS字符串時的兩條規則。違背了這兩條規則,你的程序就會存在DBCS有關的bugs。

在前向遍歷時,不要使用++操作,除非你每次都檢查lead byte; 永遠不要使用-操作進行後向遍歷。

  我們先來闡述規則2,因爲找到一個違背它的真實的實例代碼是很容易的。假設你有一個程序在你自己的目錄裏保存了一個設置文件,你把安裝目錄保存在註冊表中。在運行時,你從註冊表中讀取安裝目錄,然後合成配置文件名,接着讀取該文件。假設,你的安裝目錄是C:/Program Files/MyCoolApp,那麼你合成的文件名應該是C:/Program Files/MyCoolApp/config.bin。當你進行測試時,你發現程序運行正常。
  現在,想象你合成文件名的代碼可能是這樣的:

bool GetConfigFileName ( char* pszName, size_t nBuffSize )
{
    char szConfigFilename[MAX_PATH];
 
    // Read install dir from registry... we''ll assume it succeeds.
 
    // Add on a backslash if it wasn''t present in the registry value.
    // First, get a pointer to the terminating zero.
    char* pLastChar = strchr ( szConfigFilename, ''<!-- ~ Value_txtContent ~ -->'' );
 
    // Now move it back one character.
    pLastChar--;  
 
    if ( *pLastChar != ''//'' )
        strcat ( szConfigFilename, "//" );
 
    // Add on the name of the config file.
    strcat ( szConfigFilename, "config.bin" );
 
    // If the caller''s buffer is big enough, return the filename.
    if ( strlen ( szConfigFilename ) >= nBuffSize )
        return false;
    else
        {
        strcpy ( pszName, szConfigFilename );
        return true;
        }
}
這是一段很健壯的代碼,然而在遇到 DBCS 字符時它將會出錯。讓我們來看看爲什麼。假設一個日本用戶使用了你的程序,把它安裝在 C:/。下面是這個名字在內存中的存儲形式:
 
43 3A 5C 83 88 83 45 83 52 83 5C 00
      LB TB LB TB LB TB LB TB  
C : / EOS

  當使用 GetConfigFileName() 檢查尾部的''//''時,它尋找安裝目錄名中最後的非0字節,看它是等於''//''的,所以沒有重新增加一個''//''。結果是代碼返回了錯誤的文件名。
  哪裏出錯了呢?看看上面兩個被用藍色高量顯示的字節。斜槓''//''的值是0x5c。'' ''的值是83 5c。上面的代碼錯誤的讀取了一個 trail byte,把它當作了一個字符。
  正確的後向遍歷方法是使用能夠識別DBCS字符的函數,使指針移動正確的字節數。下面是正確的代碼。

bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )
{
    char szConfigFilename[MAX_PATH];
 
    // Read install dir from registry... we''ll assume it succeeds.
 
    // Add on a backslash if it wasn''t present in the registry value.
    // First, get a pointer to the terminating zero.
    char* pLastChar = _mbschr ( szConfigFilename, ''<!-- ~ Value_txtContent ~ -->'' );
 
    // Now move it back one double-byte character.
    //指針移動的地方
    pLastChar = CharPrev ( szConfigFilename, pLastChar );
 
    if ( *pLastChar != ''//'' )
        _mbscat ( szConfigFilename, "//" );
 
    // Add on the name of the config file.
    _mbscat ( szConfigFilename, "config.bin" );
 
     // If the caller''s buffer is big enough, return the filename.
    if ( _mbslen ( szInstallDir ) >= nBuffSize )
        return false;
    else
        {
        _mbscpy ( pszName, szConfigFilename );
        return true;
        }
}
上面的函數使用CharPrev() API使pLastChar向後移動一個字符,這個字符可能是兩個字節長。在這個版本里,if條件正常工作,因爲lead byte永遠不會等於0x5c。
  讓我們來想象一個違背規則1的場合。例如,你可能要檢測一個用戶輸入的文件名是否多次出現了'':''。如果,你使用++操作來遍歷字符串,而不是使用CharNext(),你可能會發出不正確的錯誤警告如果恰巧有一個trail byte它的值的等於'':''的值。
與規則2相關的關於字符串索引的規則:
2a. 永遠不要使用減法去得到一個字符串的索引。

違背這條規則的代碼和違背規則2的代碼很相似。例如,

char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];

這和向後移動一個指針是同樣的效果。

回到關於str***()和_mbs***()的區別

  現在,我們應該很清楚爲什麼_mbs***()函數是必需的。Str***()函數根本不考慮DBCS字符,而_mbs***()考慮。如果,你調用strrchr("C:// ", ''//''),返回結果可能是錯誤的,然而_mbsrchr()將會認出最後的雙字節字符,返回一個指向真的''//''的指針。
  關於字符串函數的最後一點:str***()和_mbs***()函數認爲字符串的長度都是以char來計算的。所以,如果一個字符串包含3個雙字節字符,_mbslen()將會返回6。Unicode函數返回的長度是按wchar_t來計算的。例如,wcslen(L"Bob")返回3。

Win32 API中的MBCS和Unicode

兩組 APIs:
  儘管你也許從來沒有注意過,Win32中的每個與字符串相關的API和message都有兩個版本。一個版本接受MBCS字符串,另一個接受Unicode字符串。例如,根本沒有SetWindowText()這個API,相反,有SetWindowTextA()和SetWindowTextW()。後綴A表明這是MBCS函數,後綴W表示這是Unicode版本的函數。
  當你 build 一個 Windows 程序,你可以選擇是用 MBCS 或者 Unicode APIs。如果,你曾經用過VC嚮導並且沒有改過預處理的設置,那表明你用的是MBCS版本。那麼,既然沒有 SetWindowText() API,我們爲什麼可以使用它呢?winuser.h頭文件包含了一些宏,例如:

BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString );
BOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );
 
#ifdef UNICODE
#define SetWindowText  SetWindowTextW
#else
#define SetWindowText  SetWindowTextA
#endif       
當使用MBCS APIs來build程序時,UNICODE沒有被定義,所以預處理器看到:
#define SetWindowText SetWindowTextA 

  這個宏定義把所有對SetWindowText的調用都轉換成真正的API函數SetWindowTextA。(當然,你可以直接調用SetWindowTextA() 或者 SetWindowTextW(),雖然你不必那麼做。)
  所以,如果你想把默認使用的API函數變成Unicode版的,你可以在預處理器設置中,把_MBCS從預定義的宏列表中刪除,然後添加UNICODE和_UNICODE。(你需要兩個都定義,因爲不同的頭文件可能使用不同的宏。) 然而,如果你用char來定義你的字符串,你將會陷入一個尷尬的境地。考慮下面的代碼:

HWND hwnd = GetSomeWindowHandle();
char szNewText[] = "we love Bob!";
SetWindowText ( hwnd, szNewText );

在預處理器把SetWindowText用SetWindowTextW來替換後,代碼變成:

HWND hwnd = GetSomeWindowHandle();
char szNewText[] = "we love Bob!";
SetWindowTextW ( hwnd, szNewText );

  看到問題了嗎?我們把單字節字符串傳給了一個以Unicode字符串做參數的函數。解決這個問題的第一個方案是使用 #ifdef 來包含字符串變量的定義:

HWND hwnd = GetSomeWindowHandle();
#ifdef UNICODE
wchar_t szNewText[] = L"we love Bob!";
#else
char szNewText[] = "we love Bob!";
#endif
SetWindowText ( hwnd, szNewText );

你可能已經感受到了這樣做將會使你多麼的頭疼。完美的解決方案是使用TCHAR.

使用TCHAR

  TCHAR是一種字符串類型,它讓你在以MBCS和UNNICODE來build程序時可以使用同樣的代碼,不需要使用繁瑣的宏定義來包含你的代碼。TCHAR的定義如下:

#ifdef UNICODE
typedef wchar_t TCHAR;
#else
typedef char TCHAR;
#endif 

所以用MBCS來build時,TCHAR是char,使用UNICODE時,TCHAR是wchar_t。還有一個宏來處理定義Unicode字符串常量時所需的L前綴。

#ifdef UNICODE
#define _T(x) L##x
#else
#define _T(x) x
#endif 

  ##是一個預處理操作符,它可以把兩個參數連在一起。如果你的代碼中需要字符串常量,在它前面加上_T宏。如果你使用Unicode來build,它會在字符串常量前加上L前綴。

TCHAR szNewText[] = _T("we love Bob!");

  像是用宏來隱藏SetWindowTextA/W的細節一樣,還有很多可以供你使用的宏來實現str***()和_mbs***()等字符串函數。例如,你可以使用_tcsrchr宏來替換strrchr()、_mbsrchr()和wcsrchr()。_tcsrchr根據你預定義的宏是_MBCS還是UNICODE來擴展成正確的函數,就像SetWindowText所作的一樣。
  不僅str***()函數有TCHAR宏。其他的函數如, _stprintf(代替sprinft()和swprintf()),_tfopen(代替fopen()和_wfopen())。 MSDN中"Generic-Text Routine Mappings."標題下有完整的宏列表。

字符串和TCHAR typedefs

  由於Win32 API文檔的函數列表使用函數的常用名字(例如,"SetWindowText"),所有的字符串都是用TCHAR來定義的。(除了XP中引入的只適用於Unicode的API)。下面列出一些常用的typedefs,你可以在msdn中看到他們。

type Meaning in MBCS builds Meaning in Unicode builds
WCHAR wchar_t wchar_t
LPSTR zero-terminated string of char (char*) zero-terminated string of char (char*)
LPCSTR constant zero-terminated string of char (const char*) constant zero-terminated string of char (const char*)
LPWSTR zero-terminated Unicode string (wchar_t*) zero-terminated Unicode string (wchar_t*)
LPCWSTR constant zero-terminated Unicode string (const wchar_t*) constant zero-terminated Unicode string (const wchar_t*)
TCHAR char wchar_t
LPTSTR zero-terminated string of TCHAR (TCHAR*) zero-terminated string of TCHAR (TCHAR*)
LPCTSTR constant zero-terminated string of TCHAR (const TCHAR*) constant zero-terminated string of TCHAR (const TCHAR*)

何時使用 TCHAR 和 Unicode

  到現在,你可能會問,我們爲什麼要使用Unicode。我已經用了很多年的char。下列3種情況下,使用Unicode將會使你受益:

你的程序只運行在Windows NT系統中。 你的程序需要處理超過MAX_PATH個字符長的文件名。 你的程序需要使用XP中引入的只有Unicode版本的API.

  Windows 9x 中大多數的 API 沒有實現 Unicode 版本。所以,如果你的程序要在windows 9x中運行,你必須使用MBCS APIs。然而,由於NT系統內部都使用Unicode,所以使用Unicode APIs將會加快你的程序的運行速度。每次,你傳遞一個字符串調用MBCS API,操作系統會把這個字符串轉換成Unicode字符串,然後調用對應的Unicode API。如果一個字符串被返回,操作系統還要把它轉變回去。儘管這個轉換過程被高度優化了,但它對速度造成的損失是無法避免的。
  只要你使用Unicode API,NT系統允許使用非常長的文件名(突破了MAX_PATH的限制,MAX_PATH=260)。使用Unicode API的另一個優點是你的程序會自動處理用戶輸入的各種語言。所以一個用戶可以輸入英文,中文或者日文,而你不需要額外編寫代碼去處理它們。
  最後,隨着windows 9x產品的淡出,微軟似乎正在拋棄MBCS APIs。例如,包含兩個字符串參數的SetWindowTheme() API只有Unicode版本的。使用Unicode來build你的程序將會簡化字符串的處理,你不必在MBCS和Unicdoe之間相互轉換。
  即使你現在不使用Unicode來build你的程序,你也應該使用TCHAR及其相關的宏。這樣做不僅可以的代碼可以很好地處理DBCS,而且如果將來你想用Unicode來build你的程序,你只需要改變一下預處理器中的設置就可以實現了。

作者簡介
  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 遊戲。他因忘了自己曾經學過的語言:法語、漢語、日語而感到悲哀。  
發佈了7 篇原創文章 · 獲贊 1 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章