《C++字符串完全指南--第一部分:win32 字符编码》
原作者:Michael Dun
译 者:Dingqiao Wang
引言
毫无疑问,你肯定见过像TCHAR, std::string, BSTR等等这类字符串类型.也包括一些以_tcs开头的奇怪的宏。也许你正盯着屏幕"哇哇"的发愁,然而阅读完本文情况将会改观。这篇指南概述了引入各种类型字符串的目的,展示了它们的一些简单用法,同时描述了在必要的时候在它们之间如何进行转换。
在第一部分,将会涉及三种类型的字符编码。理解编码方案的工作原理对你至关重要。即使你现在已经知道字符串是一个字符数组,还是看看这部分内容。一旦你阅读了这些内容,你就清楚了这么多字符串类之间的联系。
在第二部分,将描述字符串类本身,包括什么时候使用哪种类以及如何在他们之间进行转换。
基本字符-----ASCII,DBCS,Unicode
所有的字符串类最终都归结为C风格的字符串,而C风格的字符串就是字符数组,因此我首先介绍下字符类型。有三种编码方案和三种字符类型。第一种方案是单字节编码(single-byte character set, or SBCS).这种方案里,所有字符都正好是一个字节长。ASCII码就是单字节编码的例子。单字节字符串以一个字节的0做结束标志。
第二种编码方案是多字节编码(multi-byte character set, or MBCS).在多字节编码中包含一些单字节长的字符,也包含其它的多余一个字节长度的字符。在Windows中使用的多字节编码方案中包含两种类型,单字节和双字节类型。由于在Windows中使用到的最长的多字节字符也就是2个字节长,因此常常用双字节字符集(double-byte character set, or DBCS)来代替MBCS.
在双字节编码方式中,一些值被保留来指示他们是双字节的一部分。举个例子,在Shift-JIS编码(一种常用的日文编码方案)中,介于0x81-0x9F and 0xE0-0xFC之间的值就用来说明这是双字节字符,它的下一个字节是其一部分。这些值被称作"头部字节"(lead bytes),他们总是比0X7F大。紧跟在头部字节后的下一字节被称作"后随字节"(trail bytes)。在双字节编码中,后随字节可以为任意非零值。和单字节编码一样,双字节编码使用单字节的0值作为结束符。
第三种方案是Unicode。Unicode 是一种所有字符均采用二个字节的编码标准。Unicode字符有时也被称作宽字节(wide characters),因为他们比单字节占用更多存贮。注意,Unicode并不是一种多字节编码——多字节编码的显著特点是字符是不同长度的。一个Unicode字符串以两个0值字节作为结束标志(0值的宽字符形式)。
单字节字符包括拉丁文字母,带重音的字符(accented characters),ASCII标准和DOS系统中定义的图形符号。双字节字符在东亚和中东地区的语言中使用。Unicode在COM和Windows NT 内部使用。
你肯定已经很熟悉单字节字符了。当你在使用char类型时,处理的就是单字节字符。双字节字符也用过char类型来操作(这也是我们使用双字节时遇到的第一个怪现象)。wchar_t类型代表着Unicode字符。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 | EOS |
Unicode 版本的,L"Bob",是这样存储的:
42 00 | 6F 00 | 62 00 | 00 00 |
B | o | b | EOS |
以0x0000(0的Unicode编码形式)作为结束标记.
双字节字符串初看起来像单字节字符串,但是当我们以后使用字符串操作函数和利用指针遍历字符串时将看到他们的细微区别。字符串("nihongo")采用以下形式存贮(下面表中的LB代表 lead bytes,TB代表trail bytes):
记住,"ni"值并不是被解释为0xFA93这一值。而是93和FA两个值以那种字节序,在一起而被编码为"ni".(因此在一个大端格式(Big-endian)的CPU上,这些字节仍然按上述顺序)
字符串处理函数的使用
我们已经见过C风格字符串函数像strcpy(), sprintf(), atol()等等。这些函数只能用于处理单字节的字符串。标准库中有他们的只能用于处理Unicode字符串的版本,诸如wcscpy(), swprintf(), _wtol().
微软也在C运行库(C runtime library)中增加了这些函数处理多字节字符串的版本。strxxx()这类函数对应的DBCS版本取名为_mbsxxx().如果你遇到了DBCS字符串(如果你的软件是安装在日文、中文或者其他使用DBCS的语言情况下你会遇到的),你应该总是使用_mbsxxx()函数,因为他们接受SBCS字符串(一个DBCS字符串可能仅仅包含单字节字符,这就是_mbsxxx()函数可以处理SBCS字符串的缘故)。
让我们来看一个典型的字符串来解释字符串处理函数不同版本的必要性。回到上文讲到的Unicode字符串L"Bob":
42 00 | 6F 00 | 62 00 | 00 00 |
B | o | b | EOS |
因为x86系列CPU是小端格式(little-endian),值0x0042在内存中形式为42 00.你预见到了把这个字符串传递给函数strlen()的问题了吗?函数将看到头字节42,然后00,而00恰好是字符串结束标志,函数将返回1.相反,将"Bob"传递给函数wcslen(),将变得更糟。wcslen()会首先看到0x6F42,然后是0x0062,继而一直读下去直到碰到了00 00序列或者引起了GPF.
这里我们涉及到了strxxx()和wcsxxx()的对比。他们的区别又是什么呢?他们的区别至关重要,与在DBCS字符串中的合理的遍历密切相关。下文将讲述字符串的遍历,然后再回到二者的对比上来。
字符串中合理的遍历和索引
我们之中的大部分人都是伴着SBCS字符串而成长起来的,我们习惯了利用指针通过++和--操作符来遍历一个字符串。我们也习惯于用数组来获取字符串中的字符。这两种方式在SBCS和Unicode字符串下用起来十分完美,因为字符都是相同长度的,编译器会成功返回我们想要的字符。
但是,当你遇到了DBCS字符串时,为了代码的正常运行,你必须改掉这种习惯。
这里有两条利用指针遍历DBCS字符串的原则。破坏了这些原则将导致你大部分与DBCS相关的漏洞(bugs)。
1.不要使用++操作符来向前遍历,除非你一直检查字符串的头字节。
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字符时,仍将会出错。来分析下为什么会这样,假设一个日本用户将你的安装目录改为.以下是目录名在内存中的存贮形式:
当GetConfigFileName()检查反斜杠时,它会检查安装目录的最后一个非0字节,来判断是否等于"\\",如果没有则添加上去。运行的结果是返回错误的文件名。哪儿出错呢?看看以蓝色高亮显示的反斜杠。反斜杠字符的值是0x5C.的值是83 5C,而上述代码误将它的后随字节当做了一个独立字符。正确的向后遍历方法是使用注意到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向后移动一个字符,这样就可能移动两个字节如果字符串以双字节字符结尾。在这个版本中,假设的情况会运行正常,因为头部字节将永远不等于0x5C。
你可以合理想象下破坏原则1的方式。举个例子,你通过判断字符':'出现的次数验证用户输入的一个文件名是否合法。如果你使用++而不是CharNext()来遍历,你可能会产生错误如果碰巧遇到后随字节等于':'的字符。
和原则2相关的使用数组索引的原则:
2a.永远不要使用减法来计算字符串的索引。
破坏这个原则的代码和破坏原则2的代码很相似。例如,pLastChar像下面这样使用时:
- char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];
这同样的破坏了原则,因为计算索引时使用减1这等于指针向后移动一个字节,这破坏了原则2.
再谈strxxx()和_mbsxxx()的对比
现在应该明白_mbsxxx()这类函数的必要性了。Strxxx()不知道DBCS字符而_mbsxxx()函数了解.如果你调用将返回错误结果 ,但是_mbsxxx()将在末尾识别出双字节字符,返回实际上指向反斜杠的指针。 关于字符串函数的最后一点,strxxx()和_mbsxxx()函数取或者返回长度均以char为单位。 因此对于一个包含3个双字节字符的字符串,_mbslen()将返回6.Unicode函数以wchar_t为单位返回长度,例如wcslen(L"Bob")返回3.
Win32 API中的MBCS和Unicode
两套API
即使你从没有注意到,但是Win32中每一个处理字符串的API和消息都有两个版本.
一个接受MBCS字符串,另一个接受Unicode字符串。举个例子,并没有SetWindowText这个API,相反,有SetWindowTextA()和SetWindowTextW().后缀A(对于ANSI)指示MBCS函数,后缀W(对于Wide)指示Unicode版本。
当你建立一个Windows应用程序,你可以选择使用MBCS或者Unicode版本的API.如果你使用VC应用程序向导并且从未接触过编译器设置的话,你使用的一直是MBCS版本。那么为什么我们写下"SetWindowText"而事实上又没有这个名字对应的API呢?在winuser.h头文件中包含了一些#define开头的宏,如下:
- 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 API建立时,UNICODE就没有定义,因此编译器看到:
- #define SetWindowText SetWindowTextA
并将所有调用SetWindowText()的地方用真正的API,SetWindowTextA来替换掉。(注意你可以直接调用函
数SetWindowTextA和SetWindowTextW,尽管你很少需要这样做.)
因此,如果你想要把Unicode API设定为默认的话,你可转到编译器设置项,从预定义符号表中移除_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和Unicode应用使用同一分代码的字符类型,它不需要在你整个代码中写这些零乱的#define宏。TCHAR的一种定义如下:
- #ifdef UNICODE
- typedef wchar_t TCHAR;
- #else
- typedef char TCHAR;
- #endif
因此一个TCHAR在MBCS工程中是char类型,在Unicode工程中是wchar_t类型。这里还有一个_T()宏,来处理Unicode字符串字面值所需的L前缀。
- <pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"><pre><pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"><pre>
- <span style="color:#000000;"></span><pre name="code" class="cpp">#ifdef UNICODE
- #define _T(x) L##x
- #else
- #define _T(x) x
- #endif
##是用来连接两个参数的预编译操作符。无论何时,在你代码中有字符串字面值时,使用_T宏,那么在Unicode工程中就会添加上L前缀。
- <p></p><pre name="code" class="cpp"><pre name="code" class="cpp">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 typedef
由于Win32 API文档以函数名列举函数(l例如,"SetWindowText"),所有的字符串均以TCHAR形式给定。(例外之处是xp系统中的仅适用于Unicode的API)
下列是你可在MSDN中看到的常见typedef:
何时使用TCHAR和Unicode
那么讲了这么多,你可能会想"为什么我要使用Unicode?我已经单单使用char好多年了"
下面三种情况使用Unicode将会颇有益处:
1.你的程序仅仅在Windows NT系统上运行。
2. 你的程序要处理长度超过MAX_PATH的文件名。
3.你的程序使用了Windows XP中新的API,而这些APi没有区分的A/W版本。
大部分的Unicode API都没有在Windows 9x上执行,所以如果你只想你的程序在9x上运行,那你就要坚持使用MBCS API.(微软公司一些新的叫做MicroSoft Layer的库,允许在9X上使用Unicode API,但是我没有使用过,我不知道执行情况如何.)但是,既然NT系统内部所有的都采用Unicode,使用Unicode API可以提高你程序运行的速度。每次你向MBCS API传递一个字符串时,系统将字符串转换为Unicode型,同时调用相应的Unicode API。如果一个字符串返回了,那么操作系统将其转换后再返回。尽管这些转换操作都做了很大程度的优化来尽可能减小影响,但是鉴于其影响运行速度还是应该避免。
NT 只有在你使用Unicode API时才允许使用超过MAX_PATH长度的文件名。使用Unicode API 的好处一方面就是你的程序将自动处理不同用户键入的任意语言。那么,当一个用户可同时键入一个英文的、中文的、日文的文件名,而你可以不用编写任何特别处理的代码,因为它们对你而言都是Unicode字符。
最后,随着Windows 9x的下线,微软好像已经废除了MBCS API。例如,SetWindowTheme() API,有两个字符串参数,但是只有Unicode版本。使用Unicode工程将简化你的字符串处理,因为你也不想在MBCS和Unicode之间来回转换。
而且即使你现在没有建立Unicode工程,你也应该一直使用TCHAR和相关的宏。
不仅仅因为这样可以保证你代码的DBCS安全性,同时当未来某个时候你想建立Unicode工程时,你只需改动一下你编译器的设置!
原文地址:http://www.codeproject.com/Articles/2995/The-Complete-Guide-to-C-Strings-Part-I-Win32-Chara
下一部分原文地址:http://www.codeproject.com/Articles/3004/The-Complete-Guide-to-C-Strings-Part-II-String-Wra
下一部分译文还在翻译中...