C++字符串系列2:字符编码进阶

来源:互联网 发布:淘宝网店宣传推广 编辑:程序博客网 时间:2024/05/16 07:53

From: http://blog.sina.com.cn/s/blog_4b7e71290100b1ah.html

 

一.字符基础 -- SBCS,MBCS, Unicode
  所有的 string 类都是以C-style字符串为基础的。C-style字符串是字符数组。所以我们先介绍字符类型。这里有3种编码模式对应3种字符类型。

   第一种编码类型是单子节字符集(single-byte character set orSBCS)。在这种编码模式下,所有的字符都只用一个字节表示。ASCII是SBCS。一个字节表示的0用来标志SBCS字符串的结束。

  第二种编码模式是多字节字符集(multi-byte character set orMBCS)。一个MBCS编码包含一些一个字节长的字符,而另一些字符大于一个字节的长度。用在Windows里的MBCS包含两种字符类型,单字节字符(single-bytecharacters)和双字节字符(double-bytecharacters)。由于Windows里使用的多字节字符绝大部分是两个字节长,所以MBCS常被用DBCS代替。

  在DBCS编码模式中,一些特定的值被保留用来表明他们是双字节字符的一部分。例如,在Shift-JIS编码中(一个常用的日文编码模式),0x81-0x9f之间和0xe0-oxfc之间的值表示"这是一个双字节字符,下一个子节是这个字符的一部分。"这样的值被称作"leadingbytes",他们都大于0x7f。跟随在一个leading byte子节后面的字节被称作"trailbyte"。在DBCS中,trailbyte可以是任意非0值。像SBCS一样,DBCS字符串的结束标志也是一个单字节表示的0。

  第三种编码模式是Unicode。Unicode是一种所有的字符都使用两个字节编码的编码模式。Unicode字符有时也被称作宽字符,因为它比单子节字符宽(使用了更多的存储空间)。注意,Unicode不能被看作MBCS。MBCS的独特之处在于它的字符使用不同长度的字节编码。Unicode字符串使用两个字节表示的0作为它的结束标志。

  单字节字符包含拉丁文字母表,accentedcharacters及ASCII标准和DOS操作系统定义的图形字符。双字节字符被用来表示东亚及中东的语言。Unicode被用在COM及WindowsNT操作系统内部。

  你一定已经很熟悉单字节字符。当你使用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字符串,但是它使得使用字符串操作函数和永字符指针遍历一个字符串时会产生预料之外的结果。

    字符串"日本語"("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 runtimelibrary)中增加了操作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在内存中的存储形式是4200。你能看出如果这个字符串被传给strlen()函数会出现什么问题吗?它将先看到第一个字节42,然后是00,而00是字符串结束的标志,于是strlen()将会返回1。如果把"Bob"传给wcslen(),将会得出更坏的结果。wcslen()将会先看到0x6f42,然后是0x0062,然后一直读到你的缓冲区的末尾,直到发现0000结束标志或者引起了GPF。

  str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:// ",''//''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''//''的指针。

  关于字符串函数的最后一点:str***()和_mbs***()函数认为字符串的长度都是以char来计算的。所以,如果一个字符串包含3个双字节字符,_mbslen()将会返回6。Unicode函数返回的长度是按wchar_t来计算的。例如,wcslen(L"Bob")返回3。

Win32API中的MBCSUnicode

  尽管你也许从来没有注意过,Win32中的每个与字符串相关的API和message都有两个版本。一个版本接受MBCS字符串,另一个接受Unicode字符串。例如,根本没有SetWindowText()这个API,相反,有SetWindowTextA()和SetWindowTextW()。后缀A表明这是MBCS函数,后缀W表示这是Unicode版本的函数。

  当你 build 一个 Windows 程序,你可以选择是用 MBCS 或者 UnicodeAPIs。如果,你曾经用过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     

    当使用MBCSAPIs来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的定义如下:

    #ifdefUNICODE

       typedef wchar_t TCHAR;

    #else

       typedef char TCHAR;

    #endif

   所以用MBCS来build时,TCHAR是char,使用UNICODE时,TCHAR是wchar_t。还有一个宏来处理定义Unicode字符串常量时所需的L前缀。

    #ifdefUNICODE

       #define _T(x) L##x

    #else

       #define _T(x) x

    #endif

 

   ##是一个预处理操作符,它可以把两个参数在一起。如果你的代码中需要字符串常量,在它前面加上_T宏。如果你使用Unicode来build,它会在字符串常量前加上L前缀。像是用宏来隐藏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."标题下有完整的宏列表。

.字符串宏定义

由于Win32API文档的函数列表使用函数的常用名字(例如,"SetWindowText"),所有的字符串都是用TCHAR来定义的。(除了XP中引入的只适用于Unicode的API)。下面列出一些常用的typedefs,你可以在MSDN中看到他们。

type

Meaning in MBCS builds

Meaning in Unicode builds

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

WCHAR

wchar_t

wchar_t

LPWSTR

zero-terminated Unicode string (wchar_t*)

zero-terminated Unicode string (wchar_t*)

LPCWSTR

constant zero-terminated Unicode string (constwchar_t*)

constant zero-terminated Unicode string (constwchar_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 (constTCHAR*)

constant zero-terminated string of TCHAR (constTCHAR*)

   一个增加的字符类型是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 anLPCOLESTR.

   在文档或例程中,你还会看到好多_T的变体。有四个等价的宏定义,它们是TEXT, _TEXT,__TEXT和__T,它们都起同样的做用。

七.字符串扩展类型及封装类

   字符串的种类多种多样,有些可以应用在很多场合(比如C字符串的基础char类型),也有一些可能只是在特定情况下才会使用(比如使用MFC一般会用到CString,而开发COM组件一般会涉及到BSTR或CComBSTR)。希望通过下面的总结能让大家清楚地了解到各种不同类型字符串的出处和通常情况下的使用场合。

封装类

出处

功能简介

使用

BSTR

COM

特殊的数据结构,可以保存字符串的长度

需要调用专门的API函数,容易造成内存泄漏。建议使用封装类。比如,CComBSTR。

VARIANT

COM

特殊结构,被设计用来实现跨语言的特性

用来在无类型(typeless)语言(如Jscript和VBScript)来传递数据。

CString

MFC

封装TCHAR类型的字符串

MFC中使用

COleVariant

MFC

继承自VARIANT

很少用

_bstr_t

CRT

是一对BSTR的完整封装类,隐藏了底层的BSTR。

 

_variant_t

CRT

是一个对VARIANT的完整封装,隐藏了底层的VARIANT。

 

basic_string

STL

类模版

string

wstring

CComBSTR

ATL

BSTR 封装类,允许直接访问底层BSTR。

它在某些情况下比_bstr_t有用的多。

CComVariant

ATL

VARIANT的封装类,但是VARIANT没有被隐藏。

 

CString

WTL

行为和MFC的 CString完全一样。

 

System::String

CLR 和 VC 7 类

一个String对象包含一个不可改变的字符串序列。

 

 

常用字符串转换

1、函数 WideCharToMultiByte(),转换 UNICODE 到MBCS。使用范例:

    LPCOLESTRlpw = L"Hello,你好";

    size_t wLen= wcslen( lpw ) + 1;  // 宽字符字符长度,+1表示包含字符串结束符

    intaLen=WideCharToMultiByte(  // 第一次调用,计算所需 MBCS字符串字节长度

                         CP_ACP,

                         0,

                         lpw,  // 宽字符串指针

                         wLen, // 字符长度

                         NULL,

                         0,  // 参数0表示计算转换后的字符空间

                          NULL,

                         NULL);

 

    LPSTRlpa = new char [aLen];

 

   WideCharToMultiByte(

                          CP_ACP,

                         0,

                          lpw,

                         wLen,

                         lpa,  // 转换后的字符串指针

                         aLen, // 给出空间大小

                          NULL,

                          NULL);

 

    // 此时,lpa中保存着转换后的 MBCS 字符串

    ... ... ......

    delete []lpa;

 

2、函数 MultiByteToWideChar(),转换 MBCS 到UNICODE。使用范例:

 

    LPCSTR lpa ="Hello,你好";

    size_t aLen= strlen( lpa ) + 1;

    int wLen =MultiByteToWideChar(

                                  CP_ACP,

                                   0,

                                  lpa,

                                  aLen,

                                   NULL,

                                   0);

 

    LPOLESTR lpw= new WCHAR [wLen];

   MultiByteToWideChar(

                                  CP_ACP,

                                   0,

                                   lpa,

                                  aLen,

                                  lpw,

                                  wLen);

    ... ... ......

    delete []lpw;

3、使用 ATL 提供的转换宏。

A2BSTR

OLE2A

T2A

W2A

A2COLE

OLE2BSTR

T2BSTR

W2BSTR

A2CT

OLE2CA

T2CA

W2CA

A2CW

OLE2CT

T2COLE

W2COLE

A2OLE

OLE2CW

T2CW

W2CT

A2T

OLE2T

T2OLE

W2OLE

A2W

OLE2W

T2W

W2T

   上表中的宏函数,其实非常容易记忆:

2

好搞笑的缩写,to 的发音和 2 一样,所以借用来表示“转换为、转换到”的含义。

A

ANSI 字符串,也就是 MBCS。

W、OLE

宽字符串,也就是 UNICODE。

T

中间类型T。如果定义了 _UNICODE,则T表示W;如果定义了 _MBCS,则T表示A

C

const 的缩写

 

使用范例:

     #include <atlconv.h>

     void fun()

     {

         USES_CONVERSION;  // 只需要调用一次,就可以在函数中进行多次转换

         LPCTSTR lp = OLE2CT( L"Hello,你好") );

         ... ... ... ...

         // 不用显式释放 lp 的内存,因为

         // 由于 ATL 转换宏使用栈作为临时空间,函数结束后会自动释放栈空间。

     }

  使用 ATL 转换宏,由于不用释放临时空间,所以使用起来非常方便。但是考虑到栈空间的尺寸(VC默认2M),使用时要注意几点:

   1、只适合于进行短字符串的转换;
   2、不要试图在一个次数比较多的循环体内进行转换;
   3、不要试图对字符型文件内容进行转换,因为文件尺寸一般情况下是比较大的;
    4、对情况 2 和3,要使用 MultiByteToWideChar() 和 WideCharToMultiByte();