C++字符串完全指引之一 —— Win32 字符编码
翻译:Chengjie Sun 原文出处:CodeProject:The Complete Guide to C++ Strings, Part I 引言 毫无疑问,我们都看到过像 TCHAR, std::string, BSTR 等各种各样的字符串类型,还有那些以 _tcs 开头的奇怪的宏。你也许正在盯着显示器发愁。本指引将总结引进各种字符类型的目的,展示一些简单的用法,并告诉您在必要时,如何实现各种字符串类型之间的转换。 字符基础 -- ASCII, DBCS, Unicode 所有的 string 类都是以C-style字符串为基础的。C-style 字符串是字符数组。所以我们先介绍字符类型。这里有3种编码模式对应3种字符类型。第一种编码类型是单子节字符集(single-byte character set or SBCS)。在这种编码模式下,所有的字符都只用一个字节表示。ASCII是SBCS。一个字节表示的0用来标志SBCS字符串的结束。 wchar_t wch = L''1''; // 2 bytes, 0x0031 wchar_t* wsz = L"Hello"; // 12 bytes, 6 wide characters 字符在内存中是怎样存储的 单字节字符串:每个字符占一个字节按顺序依次存储,最后以单字节表示的0结束。例如。"Bob"的存贮形式如下:
Unicode的存储形式,L"Bob"
使用两个字节表示的0来做结束标志。 一眼看上去,DBCS 字符串很像 SBCS 字符串,但是我们一会儿将看到 DBCS 字符串的微妙之处,它使得使用字符串操作函数和永字符指针遍历一个字符串时会产生预料之外的结果。字符串" " ("nihongo")在内存中的存储形式如下(LB和TB分别用来表示 leading byte 和 trail byte)
值得注意的是,"ni"的值不能被解释成WORD型值0xfa93,而应该看作两个值93和fa以这种顺序被作为"ni"的编码。 使用字符串处理函数 我们都已经见过C语言中的字符串函数,strcpy(), sprintf(), atoll()等。这些字符串只应该用来处理单字节字符字符串。标准库也提供了仅适用于Unicode类型字符串的函数,比如wcscpy(), swprintf(), wtol()等。
因为x86CPU是little-endian,值0x0042在内存中的存储形式是42 00。你能看出如果这个字符串被传给strlen()函数会出现什么问题吗?它将先看到第一个字节42,然后是00,而00是字符串结束的标志,于是strlen()将会返回1。如果把"Bob"传给wcslen(),将会得出更坏的结果。wcslen()将会先看到0x6f42,然后是0x0062,然后一直读到你的缓冲区的末尾,直到发现00 00结束标志或者引起了GPF。 正确的遍历和索引字符串 因为我们中大多数人都是用着SBCS字符串成长的,所以我们在遍历字符串时,常常使用指针的++-和-操作。我们也使用数组下标的表示形式来操作字符串中的字符。这两种方式是用于SBCS和Unicode字符串,因为它们中的字符有着相同的宽度,编译器能正确的返回我们需要的字符。 我们先来阐述规则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, '''' ); // 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:/。下面是这个名字在内存中的存储形式:
当使用 GetConfigFileName() 检查尾部的''//''时,它寻找安装目录名中最后的非0字节,看它是等于''//''的,所以没有重新增加一个''//''。结果是代码返回了错误的文件名。 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, '''' ); // 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。 2a. 永远不要使用减法去得到一个字符串的索引。 违背这条规则的代码和违背规则2的代码很相似。例如, char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1]; 这和向后移动一个指针是同样的效果。 回到关于str***()和_mbs***()的区别 现在,我们应该很清楚为什么_mbs***()函数是必需的。Str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:// ", ''//''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''//''的指针。 Win32 API中的MBCS和Unicode 两组 APIs: 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(),虽然你不必那么做。) 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所作的一样。 字符串和TCHAR typedefs 由于Win32 API文档的函数列表使用函数的常用名字(例如,"SetWindowText"),所有的字符串都是用TCHAR来定义的。(除了XP中引入的只适用于Unicode的API)。下面列出一些常用的typedefs,你可以在msdn中看到他们。
何时使用 TCHAR 和 Unicode 到现在,你可能会问,我们为什么要使用Unicode。我已经用了很多年的char。下列3种情况下,使用Unicode将会使你受益: Windows 9x 中大多数的 API 没有实现 Unicode 版本。所以,如果你的程序要在windows 9x中运行,你必须使用MBCS APIs。然而,由于NT系统内部都使用Unicode,所以使用Unicode APIs将会加快你的程序的运行速度。每次,你传递一个字符串调用MBCS API,操作系统会把这个字符串转换成Unicode字符串,然后调用对应的Unicode API。如果一个字符串被返回,操作系统还要把它转变回去。尽管这个转换过程被高度优化了,但它对速度造成的损失是无法避免的。 |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
作者简介 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 游戏。他因忘了自己曾经学过的语言:法语、汉语、日语而感到悲哀。 |