C++字符串完全指南(轉載)

C++字符串完全指南 - Win32字符編碼(一)

 

前言

字符串的表現形式各異,象TCHAR,std::string,BSTR等等,有時還會見到怪怪的用_tcs起頭的宏。這個指南的目的就是說明各種字符串類型及其用途,並說明如何在必要時進行類型的相互轉換。

在指南的第一部分,介紹三種字符編碼格式。理解編碼的工作原理是致爲重要的。即使你已經知道字符串是一個字符的數組這樣的概念,也請閱讀本文,它會讓你明白各種字符串類之間的關係。

指南的第二部分,將闡述各個字符串類,什麼時候使用哪種字符串類,及其相互轉換。

字符串基礎 - ASCII, DBCS, Unicode

所有的字符串類都起源於C語言的字符串,而C語言字符串則是字符的數組。首先了解一下字符類型。有三種編碼方式和三種字符類型。

第一種編碼方式是單字節字符集,稱之爲SBCS,它的所有字符都只有一個字節的長度。ASCII碼就是SBCS。SBCS字符串由一個零字節結尾。

第二種編碼方式是多字節字符集,稱之爲MBCS,它包含的字符中有單字節長的字符,也有多字節長的字符。Windows用到的MBCS只有二種字符類型,單字節字符和雙字節字符。因此Windows中用得最多的字符是雙字節字符集,即DBCS,通常用它來代替MBCS。

在DBCS編碼中,用一些保留值來指明該字符屬於雙字節字符。例如,Shift-JIS(通用日語)編碼中,值0x81-0x9F 和 0xE0-0xFC 的意思是:“這是一個雙字節字符,下一個字節是這個字符的一部分”。這樣的值通常稱爲前導字節(lead byte),總是大於0x7F。前導字節後面是跟隨字節(trail byte)。DBCS的跟隨字節可以是任何非零值。與SBCS一樣,DBCS字符串也由一個零字節結尾。

第三種編碼方式是Unicode。Unicode編碼標準中的所有字符都是雙字節長。有時也將Unicode稱爲寬字符集(wide characters),因爲它的字符比單字節字符更寬(使用更多內存)。注意,Unicode不是MBCS - 區別在於MBCS編碼中的字符長度是不同的。Unicode字符串用二個零字節字符結尾(一個寬字符的零值編碼)。

單字節字符集是拉丁字母,重音文字,用ASCII標準定義,用於DOS操作系統。雙字節字符集用於東亞和中東語言。Unicode用於COM和Windows NT內部。

讀者都很熟悉單字節字符集,它的數據類型是char。雙字節字符集也使用char數據類型(雙字節字符集中的許多古怪處之一)。Unicode字符集用wchar_t數據類型。Unicode字符串用L前綴起頭,如:

  wchar_t  wch = L'1';      // 2 個字節, 0x0031

  wchar_t* wsz = L"Hello";  // 12 個字節, 6 個寬字符

字符串的存儲

單字節字符串順序存放各個字符,並用零字節表示字符串結尾。例如,字符串"Bob"的存儲格式爲:

Unicode編碼中,L"Bob"的存儲格式爲:

用0x0000 (Unicode的零編碼)結束字符串。

DBCS 看上去有點象SBCS。以後我們會看到在串處理和指針使用上是有微妙差別的。字符串"日本語" (nihongo) 的存儲格式如下(用LB和TB分別表示前導字節和跟隨字節):

注意,"ni"的值不是WORD值0xFA93。值93和FA順序組合編碼爲字符"ni"。(在高位優先CPU中,存放順序正如上所述)。

字符串處理函數

C語言字符串處理函數,如strcpy(), sprintf(), atol()等只能用於單字節字符串。在標準庫中有隻用於Unicode字符串的函數,如wcscpy(), swprintf(), _wtol()。

微軟在C運行庫(CRT)中加入了對DBCS字符串的支持。對應於strxxx()函數,DBCS使用_mbsxxx()函數。在處理DBCS字符串(如日語,中文,或其它DBCS)時,就要用_mbsxxx()函數。這些函數也能用於處理SBCS字符串(因爲DBCS字符串可能就只含有單字節字符)。

現在用一個示例來說明字符串處理函數的不同。如有Unicode字符串L"Bob":

x86 CPU的排列順序是低位優先(little-endian)的,值0x0042的存儲順序爲42 00。這時如用strlen()函數求字符串的長度就發生問題。函數找到第一個字節42,然後是00,意味着字符串結尾,於是返回1。反之,用wcslen()函數求"Bob"的長度更糟糕。wcslen()首先找到0x6F42,然後是0x0062,以後就在內存緩衝內不斷地尋找00 00直至發生一般性保護錯(GPF)。

strxxx()及其對應的_mbsxxx()究竟是如何運作的?二者之間的不同是非常重要的,直接影響到正確遍歷DBCS字符串的方法。下面先介紹字符串遍歷,然後再回來討論strxxx()和 _mbsxxx()。

 

 

字符串遍歷

我們中的大多數人都是從SBCS成長過來的,都習慣於用指針的 ++ 和 -- 操作符來遍歷字符串,有時也使用數組來處理字符串中的字符。這二種方法對於SBCS 和 Unicode 字符串的操作都是正確無誤的,因爲二者的字符都是等長的,編譯器能夠的正確返回我們尋求的字符位置。

但對於DBCS字符串就不能這樣了。用指針訪問DBCS字符串有二個原則,打破這二個原則就會造成錯誤。

1. 不可使用 ++ 算子,除非每次都檢查是否爲前導字節。

2. 絕不可使用 -- 算子來向後遍歷。

先說明原則2,因爲很容易找到一個非人爲的示例。假設,有一個配製文件,程序啓動時要從安裝路徑讀取該文件,如:C:/Program Files/MyCoolApp/config.bin。文件本身是正常的。

假設用以下代碼來配製文件名:

bool GetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];



char* pLastChar = strchr ( szConfigFilename, '/0' );

    pLastChar--;  
    if ( *pLastChar != '//' )
        strcat ( szConfigFilename, "//" );

    strcat ( szConfigFilename, "config.bin" );

    if ( strlen ( szConfigFilename ) >= nBuffSize )
        return false;
    else
        {
        strcpy ( pszName, szConfigFilename );
        return true;
        }
}

這段代碼的保護性是很強的,但用到DBCS字符串還是會出錯。假如文件的安裝路徑用日語表達:C:/ヨウユソ,該字符串的內存表達爲:

這時用上面的GetConfigFileName()函數來檢查文件路徑末尾是否含有反斜線就會出錯,得到錯誤的文件名。

錯在哪裏?注意上面的二個十六進制值0x5C(藍色)。前面的0x5C是字符"/",後面則是字符值83 5C,代表字符"ソ"。可是函數把它誤認爲反斜線了。

正確的方法是用DBCS函數將指針指向恰當的字符位置,如下所示:

bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];



char* pLastChar = _mbschr ( szConfigFilename, '/0' );

    pLastChar = CharPrev ( szConfigFilename, pLastChar );
    if ( *pLastChar != '//' )
        _mbscat ( szConfigFilename, "//" );

    _mbscat ( szConfigFilename, "config.bin" );

    if ( _mbslen ( szInstallDir ) >= nBuffSize )
        return false;
    else
        {
        _mbscpy ( pszName, szConfigFilename );
        return true;
        }
} 

這個改進的函數用CharPrev() API 函數將指針pLastChar向後移動一個字符。如果字符串末尾的字符是雙字節字符,就向後移動2個字節。這時返回的結果是正確的,因爲不會將字符誤判爲反斜線。

 

現在可以想像到第一原則了。例如,要遍歷字符串尋找字符":",如果不使用CharNext()函數而使用++算子,當跟隨字節值恰好也是":"時就會出錯。

與原則2相關的是數組下標的使用:

 2a. 絕不可在字符串數組中使用遞減下標。

出錯原因與原則2相同。例如,設置指針pLastChar爲:

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

結果與原則2的出錯一樣。下標減1就是指針向後移動一個字節,不符原則2。

再談strxxx() _mbsxxx()

現在可以清楚爲什麼要用 _mbsxxx() 函數了。strxxx() 函數不認識DBCS字符而 _mbsxxx()認識。如果調用strrchr("C://", '//')函數可能會出錯,但 _mbsrchr()認識雙字節字符,所以能返回指向最後出現反斜線字符的指針位置。

最後提一下strxxx() 和 _mbsxxx() 函數族中的字符串長度測量函數,它們都返回字符串的字節數。如果字符串含有3個雙字節字符,_mbslen()將返回6。而Unicode的函數返回的是wchar_ts的數量,如wcslen(L"Bob") 返回3

C++字符串完全指南 - Win32字符編碼(二)
翻譯:連波
15/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098306,00.htm

 

Win32 API中的MBCS Unicode

API的二個字符集

也許你沒有注意到,Win32的API和消息中的字符串處理函數有二種,一種爲MCBS字符串,另一種爲Unicode字符串。例如,Win32中沒有SetWindowText()這樣的接口,而是用SetWindowTextA()和 SetWindowTextW()函數。後綴A (表示ANSI)指明是MBCS函數,後綴W(表示寬字符)指明是Unicode函數。

編寫Windows程序時,可以選擇用MBCS或Unicode API接口函數。用VC AppWizards嚮導時,如果不修改預處理器設置,缺省使用的是MBCS函數。但是在API接口中沒有SetWindowText()函數,該如何調用呢?實際上,在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應用時,不必定義UNICODE,預處理爲:

#define SetWindowText  SetWindowTextA

然後將SetWindowText()處理爲真正的API接口函數SetWindowTextA() (如果願意的話,可以直接調用SetWindowTextA() 或SetWindowTextW()函數,不過很少有此需要)。

如果要將缺省應用接口改爲Unicode,就到預處理設置的預處理標記中去掉 _MBCS標記,加入UNICODE 和 _UNICODE (二個標記都要加入,不同的頭文件使用不同的標記)。不過,這時要處理普通字符串反而會遇到問題。如有代碼:

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字符串處理函數來處理單字節字符串。

第一種解決辦法是使用宏定義:
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 和 Unicode二種編碼。程序中也不必到處使用宏定義。

TCHAR的宏定義如下:

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

所以,TCHAR中在MBCS程序中是char類型,在Unicode中是 wchar_t 類型。

對於Unicode字符串,還有個 _T() 宏,用於解決 L 前綴:

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

## 是預處理算子,將二個變量粘貼在一起。不管什麼時候都對字符串用 _T 宏處理,這樣就可以在Unicode編碼中給字符串加上L前綴,如:

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

SetWindowTextA/W 函數族中還有其它隱藏的宏可以用來代替strxxx() 和 _mbsxxx() 字符串函數。例如,可以用 _tcsrchr 宏取代strrchr(),_mbsrchr(),或 wcsrchr()函數。_tcsrchr 根據編碼標記爲_MBCS 或 UNICODE,將右式函數做相應的擴展處理。宏定義方法類似於SetWindowText。

不止strxxx()函數族中有TCHAR宏定義,其它一些函數中也有。例如,_stprintf (取代sprintf()和swprintf()),和 _tfopen (取代fopen() 和 _wfopen())。MSDN的全部宏定義在"Generic-Text Routine Mappings"欄目下。

String 和 TCHAR 類型定義

Win32 API 文件中列出的函數名都是通用名(如"SetWindowText"),所有的字符串都按照TCHAR類型處理。(只有XP除外,XP只使用Unicode類型)。下面是MSDN給出的常用類型定義:

 

類型

MBCS 編碼中的意義

Unicode 編碼中的意義

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 (constchar*)

constant zero-terminated string of char (constchar*)

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?我一直用的都是普通字符串。”

在三種情況下要用到Unicode:

  1. 程序只運行於Windows NT。
  2. 處理的字符串長於MAX_PATH定義的字符數。
  3. 程序用於Windows XP中的新接口,那裏沒有A/W版本之分。

大部分Unicode API不可用於Windows 9x。所以如果程序要在Windows 9x上運行的話,要強制使用MBCS API (微軟推出一個可運行於Windows 9x的新庫,叫做Microsoft Layer for Unicode。但我沒有試用過,無法說明它的好壞)。相反,NT內部全部使用Unicode編碼,使用Unicode API可以加速程序運行。每當將字符串處理爲MBCS API時,操作系統都會將字符串轉換爲Unicode並調用相應的Unicode API 函數。對於返回的字符串,操作系統要做同樣的轉換。儘管這些轉換經過了高度優化,模塊儘可能地壓縮到最小,但畢竟會影響到程序的運行速度。

NT允許使用超長文件名(長於MAX_PATH 定義的260),但只限於Unicode API使用。Unicode API的另外一個優點是程序能夠自動處理輸入的文字語言。用戶可以混合輸入英文,中文和日文作爲文件名。不必使用其它代碼來處理,都按照Unicode編碼方式處理。

最後,作爲Windows 9x的結局,微軟似乎拋棄了MBCS API。例如,SetWindowTheme() 接口函數的二個參數只支持Unicode編碼。使用Unicode編碼省卻了MBCS與Unicode之間的轉換過程。

如果程序中還沒有使用到Unicode編碼,要堅持使用TCHAR和相應的宏。這樣不但可以長期保持程序中DBCS編碼的安全性,也利於將來擴展使用到Unicode編碼。那時只要改變預處理中的設置即可!

C++字符串完全指南(2) - 各種字符串類(一)
翻譯:連波
19/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098621,00.htm

 

前言

C語言的字符串容易出錯,難以管理,並且往往是黑客到處尋找的目標。於是,出現了許多字符串包裝類。可惜,人們並不很清楚什麼情況下該用哪個類,也不清楚如何將C語言字符串轉換到包裝類。

本文涉及到Win32 API,MFC,STL,WTL和Visual C++運行庫中使用到的所有的字符串類型。說明各個類的用法,如何構造對象,如何進行類轉換等等。Nish爲本文提供了Visual C++ 7的managed string 類的用法。

閱讀本文之前,應完全理解本指南第一部分中闡述的字符類型和編碼。

字符串類的首要原則:

不要隨便使用類型強制轉換,除非轉換的類型是明確由文檔規定的。

之所以撰寫字符串指南這二篇文章,是因爲常有人問到如何將X類型的字符串轉換到Z類型。提問者使用了強制類型轉換(cast),但不知道爲什麼不能轉換成功。各種各樣的字符串類型,特別是BSTR,在任何場合都不是三言二語可以講清的。因此,我以爲這些提問者是想讓強制類型轉換來處理一切。

除非明確規定了轉換算子,不要將任何其它類型數據強制轉換爲string。一個字符串不能用強制類型轉換到string類。例如:

void SomeFunc ( LPCWSTR widestr );
main()
{
  SomeFunc ( (LPCWSTR) "C://foo.txt" ); 
}

這段代碼100%錯誤。它可以通過編譯,因爲類型強制轉換超越了編譯器的類型檢驗。但是,能夠通過編譯,並不證明代碼是正確的。

下面,我將指出什麼時候用類型強制轉換是合理的。
C語言字符串與類型定義

如指南的第一部分所述,Windows API定義了TCHAR術語。它可用於MBCS或Unicode編碼字符,取決於預處理設置爲_MBCS 或 _UNICODE標記。關於TCHAR的詳細說明請閱指南的第一部分。爲便於敘述,下面給出字符類型定義:

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 (constchar*)

LPWSTR

string of WCHAR (WCHAR*)

LPCWSTR

constant string of WCHAR (const WCHAR*)

LPTSTR

string of TCHAR (TCHAR*)

LPCTSTR

constant string of TCHAR (const TCHAR*)

另外還有一個字符類型OLECHAR。這是一種對象鏈接與嵌入的數據類型(比如嵌入Word文檔)。這個類型通常定義爲wchar_t。如果將預處理設置定義爲OLE2ANSI,OLECHAR將被定義爲char類型。現在已經不再定義OLE2ANSI(它只在MFC 3以前版本中使用),所以我將OLECHAR作爲Unicode字符處理。

下面是與OLECHAR相關的類型定義:

Type

Meaning

OLECHAR

Unicode character (wchar_t)

LPOLESTR

string of OLECHAR (OLECHAR*)

LPCOLESTR

constant string of OLECHAR (const OLECHAR*)

還有以下二個宏讓相同的代碼能夠適用於MBCS和Unicode編碼:

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聲明字符串。BSTR有一些缺陷,所以我在這裏讓它獨立成章。

BSTR是Pascal類型字符串(字符串長度值顯式地與數據存放在一起)和C類型字符串(字符串長度必須通過尋找到結尾零字符來計算)的混合型字符串。BSTR屬於Unicode字符串,字符串中預置了字符串長度值,並且用一個零字符來結尾。下面是一個"Bob"的BSTR字符串:

注意,字符串長度值是一個DWORD類型值,給出字符串的字節長度,但不包括結尾零。在上例,"Bob"含有3個Unicode字符(不計結尾零),6個字節長。因爲明確給出了字符串長度,所以當BSTR數據在不同的處理器和計算機之間傳送時,COM庫能夠知道應該傳送的數據量。

附帶說一下,BSTR可以包含任何數據塊,不單是字符。它甚至可以包容內嵌零字符數據。這些不在本文討論範圍。

C++中的BSTR變量其實就是指向字符串首字符的指針。BSTR是這樣定義的:

typedef OLECHAR* BSTR;

這個定義很糟糕,因爲事實上BSTR與Unicode字符串不一樣。有了這個類型定義,就越過了類型檢查,可以混合使用LPOLESTR和BSTR。向一個需要LPCOLESTR (或 LPCWSTR)類型數據的函數傳遞BSTR數據是安全的,反之則不然。所以要清楚瞭解函數所需的字符串類型,並向函數傳遞正確類型的字符串。

要知道爲什麼向一個需要BSTR類型數據的函數傳遞LPCWSTR類型數據是不安全的,就別忘了BSTR必須在字符串開頭的四個字節保留字符串長度值。但LPCWSTR字符串中沒有這個值。當其它的處理過程(如Word)要尋找BSTR的長度值時就會找到一堆垃圾或堆棧中的其它數據或其它隨機數據。這就導致方法失效,當長度值太大時將導致崩潰。

許多應用接口都使用BSTR,但都用到二個最重要的函數來構造和析構BSTR。就是SysAllocString()和SysFreeString()函數。SysAllocString()將Unicode字符串拷貝到BSTR,SysFreeString()釋放BSTR。示例如下:

BSTR bstr = NULL;
bstr = SysAllocString ( L"Hi Bob!" );
if ( NULL == bstr )
   
SysFreeString ( bstr );

當然,各種BSTR包裝類都會小心地管理內存。

自動接口中的另一個數據類型是VARIANT。它用於在無類型語言,諸如JScript,VBScript,以及Visual Basic,之間傳遞數據。VARIANT可以包容許多不用類型的數據,如long和IDispatch*。如果VARIANT包含一個字符串,這個字符串是BSTR類型。在下文的VARIANT包裝類中我還會談及更多的VARIANT。
C++字符串完全指南(2) - 各種字符串類- CRT類
翻譯:連波
20/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098682,00.htm

_bstr_t

 

字符串包裝類

我已經說明了字符串的各種類型,現在討論包裝類。對於每個包裝類,我都會說明它的對象構造過程和如何轉換成C類型字符串指針。應用接口的調用,或構造另一個不同類型的字符串類,大多都要用到C類型指針。本文不涉及類的其它操作,如排序和比較等。

再強調一下,在完全瞭解轉換結果之前不要隨意使用強制類型轉換。

CRT類

_bstr_t

_bstr_t 是BSTR的完全包裝類。實際上,它隱含了BSTR。它提供多種構造函數,能夠處理隱含的C類型字符串。但它本身卻不提供BSTR的處理機制,所以不能作爲COM方法的輸出參數[out]。如果要用到BSTR* 類型數據,用ATL的CComBSTR類更爲方便。

_bstr_t 數據可以傳遞給需要BSTR數據的函數,但必須滿足以下三個條件:

首先,_bstr_t 具有能夠轉換爲wchar_t*類型數據的函數。

其次,根據BSTR定義,使得wchar_t* 和BSTR對於編譯器來說是相同的。

第三,_bstr_t內部保留的指向內存數據塊的指針 wchar_t* 要遵循BSTR格式。

滿足這些條件,即使沒有相應的BSTR轉換文檔,_bstr_t 也能正常工作。示例如下:

_bstr_t bs1 = "char string";        
_bstr_t bs2 = L"wide char string"; 
_bstr_t bs3 = bs1;             
_variant_t v = "Bob";
_bstr_t bs4 = v;             

LPCSTR psz1 = bs1;             
LPCSTR psz2 = (LPCSTR) bs1;    
LPCWSTR pwsz1 = bs1;           
LPCWSTR pwsz2 = (LPCWSTR) bs1; 
BSTR    bstr = bs1.copy();     

  SysFreeString ( bstr );

注意,_bstr_t 也可以轉換爲char* 和 wchar_t*。這是個設計問題。雖然char* 和 wchar_t*不是常量指針,但不能用於修改字符串,因爲可能會打破內部BSTR結構。

_variant_t
_variant_t

_variant_t 是VARIANT的完全包裝類。它提供多種構造函數和數據轉換函數。本文僅討論與字符串有關的操作。

_variant_t v1 = "char string";
_variant_t v2 = L"wide char string"; 
_bstr_t bs1 = "Bob";
_variant_t v3 = bs1; 

_bstr_t bs2 = v1; 
_bstr_t bs3 = (_bstr_t) v1; 

注意,_variant_t 方法在轉換失敗時會拋出異常,所以要準備用catch 捕捉_com_error異常。

另外要注意 _variant_t 不能直接轉換成MBCS字符串。要建立一個過渡的_bstr_t 變量,用其它提供轉換Unicode到MBCS的類函數,或ATL轉換宏來轉換。

與_bstr_t 不同,_variant_t 數據可以作爲參數直接傳送給COM方法。_variant_t 繼承了VARIANT類型,所以在需要使用VARIANT的地方使用_variant_t 是C++語言規則允許的。
C++字符串完全指南(2) - STL和ATL類
翻譯:連波
21/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098845,00.htm

STL類

 

STL類

STL只有一個字符串類,即basic_string。basic_string管理一個零結尾的字符數組。字符類型由模板參數決定。通常,basic_string被處理爲不透明對象。可以獲得一個只讀指針來訪問緩衝區,但寫操作都是由basic_string的成員函數進行的。

basic_string預定義了二個特例:string,含有char類型字符;which,含有wchar_t類型字符。沒有內建的TCHAR特例,可用下面的代碼實現:

typedef basic_string tstring; 

string str = "char string"; 
wstring wstr = L"wide char string"; 
tstring tstr = _T("TCHAR string"); 

LPCSTR psz = str.c_str(); 
LPCWSTR pwsz = wstr.c_str(); 
LPCTSTR ptsz = tstr.c_str(); 

與_bstr_t 不同,basic_string不能在字符集之間進行轉換。但是如果一個構造函數接受相應的字符類型,可以將由c_str()返回的指針傳遞給這個構造函數。例如:

 
_bstr_t bs1 = str.c_str();  
_bstr_t bs2 = wstr.c_str(); 
ATL類
CComBSTR

CComBSTR 是ATL的BSTR包裝類。某些情況下比_bstr_t 更有用。最主要的是,CComBSTR允許操作隱含BSTR。就是說,傳遞一個CComBSTR對象給COM方法時,CComBSTR對象會自動管理BSTR內存。例如,要調用下面的接口函數:

struct IStuff : public IUnknown
{
  
  STDMETHOD(SetText)(BSTR bsText);
  STDMETHOD(GetText)(BSTR* pbsText);
};

CComBSTR 有一個BSTR操作方法,能將BSTR直接傳遞給SetText()。還有一個引用操作(operator &)方法,返回BSTR*,將BSTR*傳遞給需要它的有關函數。

CComBSTR bs1;
CComBSTR bs2 = "new text";
pStuff->GetText ( &bs1 );       
  pStuff->SetText ( bs2 );        
  pStuff->SetText ( (BSTR) bs2 ); 

CComVariant
CComBSTR有類似於 _bstr_t 的構造函數。但沒有內建MBCS字符串的轉換函數。可以調用ATL宏進行轉換。

CComBSTR bs1 = "char string"; 
CComBSTR bs2 = L"wide char string"; 
CComBSTR bs3 = bs1; 
CComBSTR bs4;
bs4.LoadString ( IDS_SOME_STR ); 

BSTR bstr1 = bs1; 
BSTR bstr2 = (BSTR) bs1; 
BSTR bstr3 = bs1.Copy(); 
BSTR bstr4;
bstr4 = bs1.Detach(); 

SysFreeString ( bstr3 );
SysFreeString ( bstr4 );

上面的最後一個示例用到了Detach()方法。該方法調用後,CComBSTR對象就不再管理它的BSTR或其相應內存。所以bstr4就必須調用SysFreeString()。

最後討論一下引用操作符(operator &)。它的超越使得有些STL集合(如list)不能直接使用CComBSTR。在集合上使用引用操作返回指向包容類的指針。但是在CComBSTR上使用引用操作,返回的是BSTR*,不是CComBSTR*。不過可以用ATL的CAdapt類來解決這個問題。例如,要建立一個CComBSTR的隊列,可以聲明爲:

  std::list< CAdapt> bstr_list;

CAdapt 提供集合所需的操作,是隱含於代碼的。這時使用bstr_list 就象在操作一個CComBSTR隊列。

CComVariant

CComVariant 是VARIANT的包裝類。但與 _variant_t 不同,它的VARIANT不是隱含的,可以直接操作類裏的VARIANT成員。CComVariant 提供多種構造函數和多類型操作。這裏只介紹與字符串有關的操作。

CComVariant v1 = "char string";      
CComVariant v2 = L"wide char string"; 
CComBSTR bs1 = "BSTR bob";
CComVariant v3 = (BSTR) bs1;          

CComBSTR bs2 = v1.bstrVal;           

跟_variant_t 不同,CComVariant沒有不同VARIANT類型之間的轉換操作。必須直接操作VARIANT成員,並確定該VARIANT的類型無誤。調用ChangeType()方法可將CComVariant數據轉換爲BSTR。

CComVariant v4 = ... 
CComBSTR bs3;
if ( SUCCEEDED( v4.ChangeType ( VT_BSTR ) ))
    bs3 = v4.bstrVal;

跟 _variant_t 一樣,CComVariant不能直接轉換爲MBCS字符串。要建立一個過渡的_bstr_t 變量,用其它提供轉換Unicode到MBCS的類函數,或ATL轉換宏來轉換。

ATL轉換宏

ATL轉換宏

ATL的字符串轉換宏可以方便地轉換不同編碼的字符,用在函數中很有效。宏按照[source type]2[new type] 或 [source type]2C[new type]格式命名。後者轉換爲一個常量指針 (名字內含"C")。類型縮寫如下:


 A:MBCS字符串,char* (A for ANSI)
 W:Unicode字符串,wchar_t* (W for wide)
 T:TCHAR字符串,TCHAR*
 OLE:OLECHAR字符串,OLECHAR* (實際等於W)
 BSTR:BSTR (只用於目的類型)

例如,W2A() 將Unicode字符串轉換爲MBCS字符串,T2CW()將TCHAR字符串轉換爲Unicode字符串常量。

要使用宏轉換,程序中要包含atlconv.h頭文件。可以在非ATL程序中使用宏轉換,因爲頭文件不依賴其它的ATL,也不需要 _Module全局變量。如在函數中使用轉換宏,在函數起始處先寫上USES_CONVERSION宏。它表明某些局部變量由宏控制使用。

轉換得到的結果字符串,只要不是BSTR,都存儲在堆棧中。如果要在函數外使用這些字符串,就要將這些字符串拷貝到其它的字符串類。如果結果是BSTR,內存不會自動釋放,因此必須將返回值分配給一個BSTR變量或BSTR的包裝類,以避免內存泄露。

下面是若干宏轉換示例:

void Foo ( LPCWSTR wstr );
void Bar ( BSTR bstr );

void Baz ( BSTR* pbstr );
#include 
main()
{
using std::string;
USES_CONVERSION;    

LPCSTR psz1 = "Bob";
string str1 = "Bob";
Foo ( A2CW(psz1) );
  Foo ( A2CW(str1.c_str()) );

LPCSTR psz2 = "Bob";
LPCWSTR wsz = L"Bob";
BSTR bs1;
CComBSTR bs2;
bs1 = A2BSTR(psz2);        
  bs2.Attach ( W2BSTR(wsz) ); 
Bar ( bs1 );
  Bar ( bs2 );
SysFreeString ( bs1 );     


BSTR bs3 = NULL;
string str2;
Baz ( &bs3 );         
str2 = W2CA(bs3);     
  SysFreeString ( bs3 ); 
}

可以看到,向一個需要某種類型參數的函數傳遞另一種類型的參數,用宏轉換是非常方便的。
C++字符串完全指南(2) - MFC類
翻譯:連波
22/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098983,00.htm

MFC類

 

MFC類

CString

MFC的CString含有TCHAR,它的實際字符類型取決於預處理標記的設置。通常,CString象STL字符串一樣是不透明對象,只能用CString的方法來修改。CString比STL字符串更優越的是它的構造函數接受MBCS和Unicode字符串。並且可以轉換爲LPCTSTR,因此可以向接受LPCTSTR的函數直接傳遞CString對象,不必調用c_str()方法。

// 構造
CString s1 = "char string"; // 從LPCSTR構造
CString s2 = L"wide char string"; // 從LPCWSTR構造
CString s3 ( ' ', 100 ); // 預分配100字節,填充空格
CString s4 = "New window text";
// 可以在LPCTSTR處使用CString:
SetWindowText ( hwndSomeWindow, s4 );
// 或者,顯式地做強制類型轉換:
SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );

也可以從字符串表加載字符串。CString通過LoadString()來構造對象。用Format()方法可有選擇地從字符串表讀取一定格式的字符串。

// 從字符串表構造/加載
CString s5 ( (LPCTSTR) IDS_SOME_STR );  // 從字符串表加載
CString s6, s7;
// 從字符串表加載
  s6.LoadString ( IDS_SOME_STR );
// 從字符串表加載打印格式的字符串
  s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, ... );

第一個構造函數看上去有點怪,但它的確是文檔標定的字符串加載方式。

注意,CString只允許一種強制類型轉換,即強制轉換爲LPCTSTR。強制轉換爲LPTSTR (非常量指針)是錯誤的。按照老習慣,將CString強制轉換爲LPTSTR只能傷害自己。有時在程序中沒有發現出錯,那只是碰巧。轉換到非常量指針的正確方法是調用GetBuffer()方法。

下面以往隊列加入元素爲例說明如何正確地使用CString:

CString str = _T("new text");
LVITEM item = {0};
item.mask = LVIF_TEXT;
  item.iItem = 1;
  item.pszText = (LPTSTR)(LPCTSTR) str; // 錯!
  item.pszText = str.GetBuffer(0);      // 正確
ListView_SetItem ( &item );
  str.ReleaseBuffer();  // 將隊列返回給str

pszText成員是LPTSTR,一個非常量指針,因此要用str的GetBuffer()。GetBuffer()的參數是CString分配的最小緩衝區。如果要分配一個1K的TCHAR,調用GetBuffer(1024)。參數爲0,只返回指向字符串的指針。

上面示例的出錯語句可以通過編譯,甚至可以正常工作,如果恰好就是這個類型。但這不證明語法正確。進行非常量的強制類型轉換,打破了面向對象的封裝原則,並逾越了CString的內部操作。如果你習慣進行這樣的強制類型轉換,終會遇到出錯,可你未必知道錯在何處,因爲你到處都在做這樣的轉換,而代碼也都能運行。

 

知道爲什麼人們總在抱怨有缺陷的軟件嗎?不正確的代碼就臭蟲的滋生地。然道你願意編寫明知有錯的代碼讓臭蟲有機可乘?還是花些時間學習CString的正確用法讓你的代碼能夠100%的正確吧。

CString還有二個函數能夠從CString中得到BSTR,並在必要時轉換成Unicode。那就是AllocSysString()和SetSysString()。除了SetSysString()使用BSTR*參數外,二者一樣。

// 轉換成BSTR
CString s5 = "Bob!";
BSTR bs1 = NULL, bs2 = NULL;
bs1 = s5.AllocSysString();
  s5.SetSysString ( &bs2 );
// ...
  SysFreeString ( bs1 );
  SysFreeString ( bs2 );

COleVariant 與CComVariant 非常相似。COleVariant 繼承於VARIANT,可以傳遞給需要VARIANT的函數。但又與CComVariant 不同,COleVariant 只有一個LPCTSTR的構造函數,不提供單獨的LPCSTR和LPCWSTR的構造函數。在大多情況下,沒有問題,因爲總是願意把字符串處理爲LPCTSTR。但你必須知道這點。COleVariant 也有接受CString的構造函數。

// 構造
CString s1 = _T("tchar string");
COleVariant v1 = _T("Bob"); // 從LPCTSTR構造
COleVariant v2 = s1; // 從CString拷貝

對於CComVariant,必須直接處理VARIANT成員,用ChangeType()方法在必要時將其轉換爲字符串。但是,COleVariant::ChangeType() 在轉換失敗時會拋出異常,而不是返回HRESULT的出錯碼。

// 數據萃取
COleVariant v3 = ...; // 從某種類型構造v3
BSTR bs = NULL;
try
    {
    v3.ChangeType ( VT_BSTR );
    bs = v3.bstrVal;
    }
  catch ( COleException* e )
    {
    // 出錯,無法轉換
    }
SysFreeString ( bs );

WTL類

 

WTL類

CString

WTL的CString與MFC的CString的行爲完全相同,參閱上面關於MFC CString的說明即可。

CLR 及 VC 7 類

System::String 是.NET的字符串類。在其內部,String對象是一個不變的字符序列。任何操作String對象的String方法都返回一個新的String對象,因爲原有的String對象要保持不變。String類有一個特性,當多個String都指向同一組字符集時,它們其實是指向同一個對象。Managed Extensions C++ 的字符串有一個新的前綴S,用來表明是一個managed string字符串。

// 構造
String* ms = S"This is a nice managed string";

可以用unmanaged string字符串來構造String對象,但不如用managed string構造String對象有效。原因是所有相同的具有S前綴的字符串都指向同一個對象,而unmanaged string沒有這個特點。下面的例子可以說明得更清楚些:

String* ms1 = S"this is nice";
String* ms2 = S"this is nice";
String* ms3 = L"this is nice";
Console::WriteLine ( ms1 == ms2 ); // 輸出true
Console::WriteLine ( ms1 == ms3);  // 輸出false

要與沒有S前綴的字符串做比較,用String::CompareTo()方法來實現,如:

  Console::WriteLine ( ms1->CompareTo(ms2) );
  Console::WriteLine ( ms1->CompareTo(ms3) );

二者都輸出0,說明字符串相等。

在String和MFC 7的CString之間轉換很容易。CString可以轉換爲LPCTSTR,String有接受char* 和 wchar_t* 的二種構造函數。因此可以直接把CString傳遞給String的構造函數:

  CString s1 ( "hello world" );
  String* s2 ( s1 );  // 從CString拷貝

反向轉換的方法也類似:

  String* s1 = S"Three cats";
  CString s2 ( s1 );

可能有點迷惑。從VS.NET開始,CString有一個接受String對象的構造函數,所以是正確的。

  CStringT ( System::String* pString );

爲了加速操作,有時可以用基礎字符串(underlying string):

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* 指針,可以防止在操作字符串時,垃圾收集器去除該字符串。
C++字符串完全指南(2) - 總結
翻譯:連波
23/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39099061,00.htm

 

字符串類的打印格式函數

對字符串包裝類使用printf()或其它類似功能的函數時要特別小心。包括sprintf()函數及其變種,以及TRACE 和ATLTRACE 宏。它們的參數都不做類型檢驗,一定要給它們傳遞C語言字符串,而不是整個string對象。

例如,要向ATLTRACE()傳遞一個_bstr_t 裏的字符串,必須顯式用(LPCSTR)或 (LPCWSTR)進行強制類型轉換:


  _bstr_t bs = L"Bob!";
  ATLTRACE("The string is: %s in line %d/n", (LPCSTR) bs, nLine);

如果忘了用強制類型轉換,直接把整個 _bstr_t 對象傳遞給ATLTRACE,跟蹤消息將輸出無意義的東西,因爲_bstr_t 變量內的所有數據都進棧了。

所有類的總結

常用的字符串類之間的轉換方法是:將源字符串轉換爲C類型字符串指針,然後將該指針傳遞給目標類的構造函數。下面列出將字符串轉換爲C類型指針的方法,以及哪些類的構造函數接受C類型指針。

Class

string
type

convert to char*?

convert to constchar*?

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

附註:

  1. 雖然 _bstr_t 可以轉換爲非常量指針,但對內部緩衝區的修改可能導致內存溢出,或在釋放BSTR時導致內存泄露。
  2. bstr_t 的BSTR內含 wchar_t* 變量,所以可將const wchar_t* 轉換到BSTR。但這個用法將來可能會改變,使用時要小心。
  3. 如果轉換到BSTR失敗,將拋出異常。
  4. 用ChangeType()處理VARIANT的bstrVal。在MFC,轉換失敗將拋出異常。
  5. 雖然沒有BSTR的轉換函數,但AllocSysString()可返回一個新的BSTR。
  6. 用GetBuffer()方法可臨時得到一個非常量TCHAR指針。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章