C++
字符串完全指引之一
—— Win32
字符编码
引言
毫无疑问,我们都看到过像
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
游戏。他因忘了自己曾经学过的语言:法语、汉语、日语而感到悲哀。