C++字符串完全指南

Win32字符编码(一)

http://www.zdnet.com.cn/developer/tech/story/0,3800067013,39098124-1,00.htm
前言

字符串的表现形式各异,象TCHAR,std::string,BSTR等等,有时还会见到怪怪的用_tcs起头的宏。这个指南的目的就是说明各种字符串类型及其用途,并说明如何在必要时进行类型的相互转换。

在指南的第一部分,介绍三种字符编码格式。理解编码的工作原理是致为重要的。即使你已经知道字符串是一个字符的数组这样的概念,也请阅读本文,它会让你明白各种字符串类之间的关系。

指南的第二部分,将阐述各个字符串类,什么时候使用哪种字符串类,及其相互转换。

字符串基础 - ASCII, DBCS, Unicode
所有的字符串类都起源于C语言的字符串,而C语言字符串则是字符的数组。首先了解一下字符类型。有三种编码方式和三种字符类型。

第一种编码方式是单字节字符集,称之为SBCS,它的所有字符都只有一个字节的长度。ASCII码就是SBCS。SBCS字符串由一个零字节结尾。

第二种编码方式是多字节字符集,称之为MBCS,它包含的字符中有单字节长的字符,也有多字节长的字符。Windows用到的MBCS只有二种字符类型,单字节字符和双字节字符。因此Windows中用得最多的字符是双字节字符集,即DBCS,通常用它来代替MBCS。

在DBCS编码中,用一些保留值来指明该字符属于双字节字符。例如,Shift-JIS(通用日语)编码中,值0x81-0x9F 和 0xE0-0xFC 的意思是:“这是一个双字节字符,下一个字节是这个字符的一部分”。这样的值通常称为前导字节(lead byte),总是大于0x7F。前导字节后面是跟随字节(trail byte)。DBCS的跟随字节可以是任何非零值。与SBCS一样,DBCS字符串也由一个零字节结尾。

第三种编码方式是Unicode。Unicode编码标准中的所有字符都是双字节长。有时也将Unicode称为宽字符集(wide characters),因为它的字符比单字节字符更宽(使用更多内存)。注意,Unicode不是MBCS - 区别在于MBCS编码中的字符长度是不同的。Unicode字符串用二个零字节字符结尾(一个宽字符的零值编码)。

单字节字符集是拉丁字母,重音文字,用ASCII标准定义,用于DOS操作系统。双字节字符集用于东亚和中东语言。Unicode用于COM和Windows NT内部。

读者都很熟悉单字节字符集,它的数据类型是char。双字节字符集也使用char数据类型(双字节字符集中的许多古怪处之一)。Unicode字符集用wchar_t数据类型。Unicode字符串用L前缀起头,如:

  wchar_t  wch = L'1';      // 2 个字节, 0x0031

  wchar_t* wsz = L"Hello";  // 12 个字节, 6 个宽字符

字符串的存储
单字节字符串顺序存放各个字符,并用零字节表示字符串结尾。例如,字符串"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 (Unicode的零编码)结束字符串。

DBCS 看上去有点象SBCS。以后我们会看到在串处理和指针使用上是有微妙差别的。字符串"日本语" (nihongo) 的存储格式如下(用LB和TB分别表示前导字节和跟随字节):
93 FA 96 7B 8C EA 00
LB TB LB TB LB TB EOS
日    本    语    EOS

注意,"ni"的值不是WORD值0xFA93。值93和FA顺序组合编码为字符"ni"。(在高位优先CPU中,存放顺序正如上所述)。

字符串处理函数
C语言字符串处理函数,如strcpy(), sprintf(), atol()等只能用于单字节字符串。在标准库中有只用于Unicode字符串的函数,如wcscpy(), swprintf(), _wtol()。

微软在C运行库(CRT)中加入了对DBCS字符串的支持。对应于strxxx()函数,DBCS使用_mbsxxx()函数。在处理DBCS字符串(如日语,中文,或其它DBCS)时,就要用_mbsxxx()函数。这些函数也能用于处理SBCS字符串(因为DBCS字符串可能就只含有单字节字符)。

现在用一个示例来说明字符串处理函数的不同。如有Unicode字符串L"Bob":
42 6F 62 00
B  o  b  EOS

x86 CPU的排列顺序是低位优先(little-endian)的,值0x0042的存储顺序为42 00。这时如用strlen()函数求字符串的长度就发生问题。函数找到第一个字节42,然后是00,意味着字符串结尾,于是返回1。反之,用wcslen()函数求"Bob"的长度更糟糕。wcslen()首先找到0x6F42,然后是0x0062,以后就在内存缓冲内不断地寻找00 00直至发生一般性保护错(GPF)。

strxxx()及其对应的_mbsxxx()究竟是如何运作的?二者之间的不同是非常重要的,直接影响到正确遍历DBCS字符串的方法。下面先介绍字符串遍历,然后再回来讨论strxxx()和 _mbsxxx()。

我们中的大多数人都是从SBCS成长过来的,都习惯于用指针的 ++ 和 -- 操作符来遍历字符串,有时也使用数组来处理字符串中的字符。这二种方法对于SBCS 和 Unicode 字符串的操作都是正确无误的,因为二者的字符都是等长的,编译器能够的正确返回我们寻求的字符位置。

但对于DBCS字符串就不能这样了。用指针访问DBCS字符串有二个原则,打破这二个原则就会造成错误。

1. 不可使用 ++ 算子,除非每次都检查是否为前导字节。

2. 绝不可使用 -- 算子来向后遍历。

先说明原则2,因为很容易找到一个非人为的示例。假设,有一个配制文件,程序启动时要从安装路径读取该文件,如:C:/Program Files/MyCoolApp/config.bin。文件本身是正常的。

假设用以下代码来配制文件名:

bool GetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];
    // 这里从注册表读取文件的安装路径,假设一切正常。
    // 如果路径末尾没有反斜线,就加上反斜线。
    // 首先,用指针指向结尾零:
char* pLastChar = strchr ( szConfigFilename, '/0' );
    // 然后向后退一个字符:
    pLastChar--;  
    if ( *pLastChar != '//' )
        strcat ( szConfigFilename, "//" );
    // 加上文件名:
    strcat ( szConfigFilename, "config.bin" );
    // 如果字符串长度足够,返回文件名:
    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()函数来检查文件路径末尾是否含有反斜线就会出错,得到错误的文件名。
错在哪里?注意上面的二个十六进制值0x5C(蓝色)。前面的0x5C是字符"/",后面则是字符值83 5C,代表字符"ソ"。可是函数把它误认为反斜线了。

正确的方法是用DBCS函数将指针指向恰当的字符位置,如下所示:

bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];
    // 这里从注册表读取文件的安装路径,假设一切正常。
    // 如果路径末尾没有反斜线,就加上反斜线。
    // 首先,用指针指向结尾零:
char* pLastChar = _mbschr ( szConfigFilename, '/0' );
    // 然后向后退一个双字节字符:
    pLastChar = CharPrev ( szConfigFilename, pLastChar );
    if ( *pLastChar != '//' )
        _mbscat ( szConfigFilename, "//" );
    // 加上文件名:
    _mbscat ( szConfigFilename, "config.bin" );
    // 如果字符串长度足够,返回文件名:
    if ( _mbslen ( szInstallDir ) >= nBuffSize )
        return false;
    else
        {
        _mbscpy ( pszName, szConfigFilename );
        return true;
        }
}
这个改进的函数用CharPrev() API 函数将指针pLastChar向后移动一个字符。如果字符串末尾的字符是双字节字符,就向后移动2个字节。这时返回的结果是正确的,因为不会将字符误判为反斜线。
现在可以想像到第一原则了。例如,要遍历字符串寻找字符":",如果不使用CharNext()函数而使用++算子,当跟随字节值恰好也是":"时就会出错。

与原则2相关的是数组下标的使用:

 2a. 绝不可在字符串数组中使用递减下标。

出错原因与原则2相同。例如,设置指针pLastChar为:

char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];
结果与原则2的出错一样。下标减1就是指针向后移动一个字节,不符原则2。

再谈strxxx() 与_mbsxxx()
现在可以清楚为什么要用 _mbsxxx() 函数了。strxxx() 函数不认识DBCS字符而 _mbsxxx()认识。如果调用strrchr("C://", '//')函数可能会出错,但 _mbsrchr()认识双字节字符,所以能返回指向最后出现反斜线字符的指针位置。

最后提一下strxxx() 和 _mbsxxx() 函数族中的字符串长度测量函数,它们都返回字符串的字节数。如果字符串含有3个双字节字符,_mbslen()将返回6。而Unicode的函数返回的是wchar_ts的数量,如wcslen(L"Bob") 返回3(本文开头示例的出错原因 - 译注)。

下一篇重我们将要讲述Win32 API中的MBCS 和 Unicode。

 

 2006-3-22 17:40:58    Win32字符编码(二)

http://www.zdnet.com.cn/developer/tech/story/0,3800067013,39098306-1,00.htm
Win32 API中的MBCS 和 Unicode
API的二个字符集
 
 
也许你没有注意到,Win32的API和消息中的字符串处理函数有二种,一种为MCBS字符串,另一种为Unicode字符串。例如,Win32中没有SetWindowText()这样的接口,而是用SetWindowTextA()和 SetWindowTextW()函数。后缀A (表示ANSI)指明是MBCS函数,后缀W(表示宽字符)指明是Unicode函数。

编写Windows程序时,可以选择用MBCS或Unicode API接口函数。用VC AppWizards向导时,如果不修改预处理器设置,缺省使用的是MBCS函数。但是在API接口中没有SetWindowText()函数,该如何调用呢?实际上,在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应用时,不必定义UNICODE,预处理为:

#define SetWindowText  SetWindowTextA
然后将SetWindowText()处理为真正的API接口函数SetWindowTextA() (如果愿意的话,可以直接调用SetWindowTextA() 或SetWindowTextW()函数,不过很少有此需要)。

如果要将缺省应用接口改为Unicode,就到预处理设置的预处理标记中去掉 _MBCS标记,加入UNICODE 和 _UNICODE (二个标记都要加入,不同的头文件使用不同的标记)。不过,这时要处理普通字符串反而会遇到问题。如有代码:

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字符串处理函数来处理单字节字符串。

第一种解决办法是使用宏定义:
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二种编码。程序中也不必到处使用宏定义。

TCHAR的宏定义如下:

#ifdef UNICODE
 typedef wchar_t TCHAR;
#else
 typedef char TCHAR;
#endif
所以,TCHAR中在MBCS程序中是char类型,在Unicode中是 wchar_t 类型。

对于Unicode字符串,还有个 _T() 宏,用于解决 L 前缀:

#ifdef UNICODE
 #define _T(x) L##x
#else
 #define _T(x) x
#endif
## 是预处理算子,将二个变量粘贴在一起。不管什么时候都对字符串用 _T 宏处理,这样就可以在Unicode编码中给字符串加上L前缀,如:

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 类型定义
Win32 API 文件中列出的函数名都是通用名(如"SetWindowText"),所有的字符串都按照TCHAR类型处理。(只有XP除外,XP只使用Unicode类型)。下面是MSDN给出的常用类型定义:

 

类型
 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 (constchar*)
 constant zero-terminated string of char (constchar*)
 
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?我一直用的都是普通字符串。”

在三种情况下要用到Unicode:

程序只运行于Windows NT。
处理的字符串长于MAX_PATH定义的字符数。
程序用于Windows XP中的新接口,那里没有A/W版本之分。
大部分Unicode API不可用于Windows 9x。所以如果程序要在Windows 9x上运行的话,要强制使用MBCS API (微软推出一个可运行于Windows 9x的新库,叫做Microsoft Layer for Unicode。但我没有试用过,无法说明它的好坏)。相反,NT内部全部使用Unicode编码,使用Unicode API可以加速程序运行。每当将字符串处理为MBCS API时,操作系统都会将字符串转换为Unicode并调用相应的Unicode API 函数。对于返回的字符串,操作系统要做同样的转换。尽管这些转换经过了高度优化,模块尽可能地压缩到最小,但毕竟会影响到程序的运行速度。

NT允许使用超长文件名(长于MAX_PATH 定义的260),但只限于Unicode API使用。Unicode API的另外一个优点是程序能够自动处理输入的文字语言。用户可以混合输入英文,中文和日文作为文件名。不必使用其它代码来处理,都按照Unicode编码方式处理。

最后,作为Windows 9x的结局,微软似乎抛弃了MBCS API。例如,SetWindowTheme() 接口函数的二个参数只支持Unicode编码。使用Unicode编码省却了MBCS与Unicode之间的转换过程。

如果程序中还没有使用到Unicode编码,要坚持使用TCHAR和相应的宏。这样不但可以长期保持程序中DBCS编码的安全性,也利于将来扩展使用到Unicode编码。那时只要改变预处理中的设置即可!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值