C++字符串完全指引之一 —— Win32 字符编码

C++ 字符串完全指引之一 —— Win32 字符编码
原著: Michael Dunn 翻译: Chengjie Sun
引言
  毫无疑问,我们都看到过像 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
·  1 .在前向遍历时,不要使用 ++ 操作,除非你每次都检查 lead byte
·  2 .永远不要使用 - 操作进行后向遍历。
  我们先来阐述规则 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, ''/0'' );
    // 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, ''/0'' );
    // 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 将会使你受益:
·  1 .你的程序只运行在 Windows NT 系统中。
·  2 你的程序需要处理超过 MAX_PATH 个字符长的文件名。
·  3 你的程序需要使用 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 游戏。他因忘了自己曾经学过的语言:法语、汉语、日语而感到悲哀。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值