Win32编码

前言

Win32中有很多字符类型,char,wchar_t,TCHAR。本文总结各种类型字符串的目的和一些简单用法。

绝大多数转载自Win32 字符编码



字符基础——ASCII、DBSC、Unicode

先介绍字符类型。有三种编码模式对应三种字符类型。

  • 单字节字符集(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编码模式中,一些特定的值被保留用来表明他们是双字节字符的一部分。例如GBK编码中,0x81-0xEF之间的值标识这是一个双字节字符,下一个字节是这个字符的一部分,这样的值被称作”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时,我们处理的是单字节字符。双字节字符(DBCS)也用char类型类进行操作。Unicode字符用wchar_t来标识。Unicode字符和字符串常量用前缀L来表示,例如:

wchar_t wch = L”Hello”; //12 bytes,6个宽字节

wchar_t China = L”1”; // 两个字节 0x0031



字符在内存中是怎样存储的

  • 单字节字符串:每个字符占一个字节按照顺序依次存储,最后以单字节表示的0结束。例如,”Bob”的存储形式如下:
42 6F 62 00
B o b EOS
  • Unicode的存储形式,使用两个字节表示,L”Bob”
42 00 6F 00 62 00 00 00
B o b EOS
  • DBCS,字符串”中国话”在内存中的存储形式如下(LB和TB分别表示leading byte和trail byte):
    • 值得注意的是,”中”的值不能被理解称WORD型值0xD0D6,而应该看作两个值D6和D0以这种顺序被作为”中”的编码。
D6 D0 B9 Fa BB B0 00
LB TB LB TB LB TB EOS
EOS


使用字符串处理函数

我们都已经见过C语言中的字符串函数,strcpy(),sprintf(),atoll()等。这些字符串之应该用来处理单字节字符字符串。标准库也提供了仅适用于Unicode类型字符串的函数,比如wcscpy(),swprintf(),wtol()等

微软还在它的CRL(C runtime library)中增加了操作DBCS字符串的版本。str***()函数都有对应名字的DBCS版本_mbs***()。如果可能会遇到DBCS字符串,你应该使用_mbs***()函数,因为他们也可以处理SBCS字符串(一个DBCS字符串也可能含有单字节字符,这就是为什么_mbs***()函数也能处理SBCS字符串的原因)。

来看一个典型的字符串来阐明为什么需要不同版本的字符串处理函数。我们还是使用前面的Unicode字符串L”Bob”:

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

因为x86CPU是小端序,,值0x0042在内存中的存储形式是42 00。如果这个字符串被传给strlen(),它讲先看到第一个字节42,然后是00,而00是字符串结束的标志,于是strlen()返回1.如果把”Bob”传给wcslen(),将会得出更坏的结果,wcslen()将会先看到0x6F42,然后是0x0062,然后一直读到你的缓冲区末尾,直到发现00 00结束标志或者引起了GPF(一般保护性错误)。

那str***()和_mbs***()之间有没有区别呢?str***()函数根部不考虑DBCS字符,而_mbs***()考虑。当使用DBCS时,指针的前后移动应使用CharPrev()和CharNext()。



Win32 API中的MBCS和Unicode

Win32中每个与字符串相关的API和message都有两个版本。一个版本接收MBCS字符串,另一个接收Unicode字符串。例如,根本SetWindowText()这个API,相反,有SetWindowTextA()和SetWindowTextW()。后缀A表明这是MBCS函数,后缀W表示这是Unicode版本的函数。

当你build一个Windows程序,你可以选择是使用MBCS或者Unicode APIs。winuser.h头文件包含了一些宏,例如:

1
2
3
4
5
6
7
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没有被定义,所以预处理器看到:

1
#define SetWindowText SetWindowTextA

这个宏把所有对SetWindowText的调用都转换成真正地API函数SetWindowTextA

所以,如果你想把默认使用的API函数编程Unicode版的,你可以在预处理器设置中,把_MBCS从预定义的宏列表中删除,然后添加UNICODE和_UNICODE(两个都需要定义,因为不同的头文件可能使用不同的宏),在VS中简单一点,可以选择字符集为Unicode。然而,如果你用char来定义你的字符串,你将会陷入一个尴尬的境地。考虑下面的代码:

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

在预处理器把SetWindowTextSetWindowTextW来替换后,代码变成:

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

我们把单字节字符串传给了一个以Unicode字符串做参数的函数。解决这个问题的第一个方案是使用#ifdef来包含字符串变量的定义:

1
2
3
4
5
6
7
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来build程序时可以使用同样的代码,不需要使用繁琐的宏定义来包含你的代码。TCHAR定义如下:

1
2
3
4
5
#ifdef UNICODE
typedef wchar_t TCHAR;
#else
typedef char TCHAR;
#endif

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

1
2
3
4
5
6
#ifdef UNICODE
#define _T(x) L##x
#else
#define _T(x) x
#endif
//现在可以用TEXT()宏

##是一个预处理操作符,它可以把两个参数量子一起。如果你的代码中需要字符串常量,在它前面加上_T宏。如果你使用Unicode来build,他会在字符串常量前面加上L前缀。

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

像是用宏来隐藏SetWindowTextA/W的细节一样,还有很多可以供你使用的宏来实现str***()和_mbs***()等字符串函数。例如,你可以使用_tcsrchr宏来替换strchr()、_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 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 (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*)