<>原著:Michael Dunn 9 z# m5 J' b w0 \* t/ y( l3 h ; O+ m* A. A( ]6 v: x% W$ A翻译:<a href="mailtcjsun@insun.hit.edu.cn" target="_blank" >Chengjie Sun</A></P> ) N R1 r5 z/ |7 I<>原文出处:<FONT size=2><a href="http://www.codeproject.com/string/cppstringguide1.asp" target="_blank" >CodeProject:The Complete Guide to C++ Strings, Part I</A></FONT></P>1 E' g- Z$ d. Z: s
# }! f6 U! f7 }3 w<>. e6 G0 S% s$ g1 P, o- x
<TABLE cellSpacing=0 cellPadding=0 width="100%" border=0>+ Z- H1 X% P& [% i$ _5 @" h3 s
2 C4 Q$ n+ ?: _ F6 f& w( o/ C' `
<TR>+ {7 B- @% K) n$ [( Y3 Z* @# [8 K5 S
<TD>6 Q! I$ e. l) V5 o2 }3 _* t
<><B>引言</B> ! P( w' x/ I$ c" z3 U" F$ N1 _2 P4 n; t
毫无疑问,我们都看到过像 TCHAR, std::string, BSTR 等各种各样的字符串类型,还有那些以 _tcs 开头的奇怪的宏。你也许正在盯着显示器发愁。本指引将总结引进各种字符类型的目的,展示一些简单的用法,并告诉您在必要时,如何实现各种字符串类型之间的转换。 + |) o) `- |8 b- T \ 在第一部分,我们将介绍3种字符编码类型。了解各种编码模式的工作方式是很重要的事情。即使你已经知道一个字符串是一个字符数组,你也应该阅读本部分。一旦你了解了这些,你将对各种字符串类型之间的关系有一个清楚地了解。 0 h0 G& E1 `7 t) c% A# Z8 H 在第二部分,我们将单独讲述string类,怎样使用它及实现他们相互之间的转换。( F9 ?+ l+ C$ h' |) e8 Z
( w' o$ ?3 j" _$ S9 A+ O
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"> <B>字符基础 -- ASCII, DBCS, Unicode</B> 7 J/ {2 L6 R0 A, W4 ?. P: G& K; w! q1 z7 d& C. u, j( N
所有的 string 类都是以C-style字符串为基础的。C-style 字符串是字符数组。所以我们先介绍字符类型。这里有3种编码模式对应3种字符类型。第一种编码类型是单子节字符集(single-byte character set or SBCS)。在这种编码模式下,所有的字符都只用一个字节表示。ASCII是SBCS。一个字节表示的0用来标志SBCS字符串的结束。 $ d, ]4 E3 P$ ~# t 第二种编码模式是多字节字符集(multi-byte character set or MBCS)。一个MBCS编码包含一些一个字节长的字符,而另一些字符大于一个字节的长度。用在Windows里的MBCS包含两种字符类型,单字节字符(single-byte characters)和双字节字符(double-byte characters)。由于Windows里使用的多字节字符绝大部分是两个字节长,所以MBCS常被用DBCS代替。 Q# U, G( D5 d( e# W6 E* i% B
在DBCS编码模式中,一些特定的值被保留用来表明他们是双字节字符的一部分。例如,在Shift-JIS编码中(一个常用的日文编码模式),0x81-0x9f之间和 0xe0-oxfc之间的值表示"这是一个双字节字符,下一个子节是这个字符的一部分。"这样的值被称作"leading bytes",他们都大于0x7f。跟随在一个leading byte子节后面的字节被称作"trail byte"。在DBCS中,trail byte可以是任意非0值。像SBCS一样,DBCS字符串的结束标志也是一个单字节表示的0。 * ]3 e) q; G. p1 ^8 Y/ v 第三种编码模式是Unicode。Unicode是一种所有的字符都使用两个字节编码的编码模式。Unicode字符有时也被称作宽字符,因为它比单子节字符宽(使用了更多的存储空间)。注意,Unicode不能被看作MBCS。MBCS的独特之处在于它的字符使用不同长度的字节编码。Unicode字符串使用两个字节表示的0作为它的结束标志。" p6 I A$ p1 k5 ]
单字节字符包含拉丁文字母表,accented characters及ASCII标准和DOS操作系统定义的图形字符。双字节字符被用来表示东亚及中东的语言。Unicode被用在COM及Windows NT操作系统内部。- y0 |0 r+ c! E! {( a4 h1 w; T
你一定已经很熟悉单字节字符。当你使用char时,你处理的是单字节字符。双字节字符也用char类型来进行操作(这是我们将会看到的关于双子节字符的很多奇怪的地方之一)。Unicode字符用wchar_t来表示。Unicode字符和字符串常量用前缀L来表示。例如:</P><RE>wchar_t wch = L''1''; // 2 bytes, 0x00319 \( B2 W$ i. Y, s
wchar_t* wsz = L"Hello"; // 12 bytes, 6 wide characters</PRE>6 e v. w3 x- ]+ N# X
<><IMG src="http://www.vckbase.com/document/image/paragraph.gif"> <B>字符在内存中是怎样存储的</B>5 l1 ~9 i! e" i: n, }+ J9 L
( j* X6 O1 a5 j1 j7 x( D
单字节字符串:每个字符占一个字节按顺序依次存储,最后以单字节表示的0结束。例如。"Bob"的存贮形式如下:</P> 3 D& m4 t* z# `! u( b+ v& e<TABLE> 5 d! y/ P7 P X0 w. Q* [$ R& d6 R. [& N; a& \" H
<TR>0 R/ s7 p5 @1 `: v
<TD align=middle width="25%">42</TD> : N# R. u0 u' \ ^( T: a<TD align=middle width="25%"><FONT color=#990000>6F</FONT></TD> : w- X& {" o4 ~7 I<TD align=middle width="25%">62</TD> ; e9 g/ ^, V j; G6 S, S<TD align=middle width="25%">00</TD></TR>) G; G G+ m1 b& H+ z
<TR>9 H' E+ h z f/ K
<TD align=middle width="25%"><FONT color=#990000>B</FONT></TD> + x: e; E2 E" o8 J5 Q, e<TD align=middle width="25%"><FONT color=#990000>o</FONT></TD>1 I/ Y+ b# L, B Z, f$ Z" m
<TD align=middle width="25%"><FONT color=#990000>b</FONT></TD> ! }8 m o6 u+ v# U8 y% t6 {# f<TD align=middle width="25%"><FONT color=#990000>BOS</FONT></TD></TR></TABLE>( ^# ]" C7 _" ?0 x) U
<>Unicode的存储形式,L"Bob"</P>! A- u9 x& P- x8 y! Z( j8 f. Q5 ^( R1 C
<TABLE> 0 O( W" x" h2 P- k/ X3 R2 Z 8 l- T7 c+ N# S2 E. g9 m: u+ }<TR> # d, _+ K* j7 M* P3 {+ S( N<TD align=middle width="25%">42 00 </TD> 8 W `& ?( j7 T4 E: w: b9 f<TD align=middle width="25%"><FONT color=#990000>6F</FONT> 00</TD> ; F: j$ T- J0 \/ T+ F) H<TD align=middle width="25%">62 00</TD> : Z2 a. K; Z7 K/ N<TD align=middle width="25%">00 00</TD></TR>) d2 r; j- ?5 p+ R4 [7 @6 ~
<TR> . W. w. y! Q' e<TD align=middle width="25%"><FONT color=#990000>B</FONT></TD> i5 V g- D$ n- F( s
<TD align=middle width="25%"><FONT color=#990000>o</FONT></TD> * c9 r" u, \9 `0 z- ]9 N# ]<TD align=middle width="25%"><FONT color=#990000>b</FONT></TD> - w' D% Q+ A% a9 Q5 A<TD align=middle width="25%"><FONT color=#990000>BOS</FONT></TD></TR></TABLE>& B9 {7 Z0 L# R* L. a% u+ q4 t
<>使用两个字节表示的0来做结束标志。! V2 j3 ^# p6 c6 N, {/ Y
! X1 h. E% @9 K- Y% Y0 F* Y 一眼看上去,DBCS 字符串很像 SBCS 字符串,但是我们一会儿将看到 DBCS 字符串的微妙之处,它使得使用字符串操作函数和永字符指针遍历一个字符串时会产生预料之外的结果。字符串" " ("nihongo")在内存中的存储形式如下(LB和TB分别用来表示 leading byte 和 trail byte)</P> 6 u* Z4 Z3 R. n4 f3 A<TABLE>& |9 i/ W( G9 u% _/ e8 Y
/ D$ L: z8 G6 ~) b" v3 \7 n1 W
<TR>3 D3 W6 H, H ^( p4 N' S
<TD align=middle width="25%" height=20>93 <FONT color=#990000>FA</FONT></TD> " K- y, r5 b. b) ^<TD align=middle width="25%" height=20>96 <FONT color=#990000>7B</FONT></TD>! n% P- Q2 \( @
<TD align=middle width="25%" height=20><FONT color=#990000>8C EA</FONT></TD>% Z7 N+ m( u( t8 b( M
<TD align=middle width="25%" height=20>00</TD></TR> 1 z6 s/ J; D, ?, C, k- b<TR> + F0 k+ K) S; }, n<TD align=middle width="25%" height=20><FONT color=#990000>LB TB</FONT></TD> 5 S. r" k" \) V% J1 z8 e<TD align=middle width="25%" height=20><FONT color=#990000>LB TB</FONT></TD> 5 Y. h" h/ D* h+ T$ s5 K( l<TD align=middle width="25%" height=20><FONT color=#990000>LB TB</FONT></TD> % }" w' p5 U. O7 H( b<TD align=middle width="25%" height=20><FONT color=#990000>EOS</FONT></TD></TR> 5 o. C" Z2 c9 W1 k7 K# s. P<TR>! Z* m8 o: ?& O
<TD align=middle width="25%" height=20><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/ri.gif" border=0></TD> 8 f8 m: \+ C7 ?5 d<TD align=middle width="25%" height=20><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/ben.gif" border=0></TD> 9 s4 X7 Y# P' b8 b% G/ ]<TD align=middle width="25%" height=20><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/yu.gif" border=0></TD>. R' P9 R; h! n' c) H! w$ a
<TD align=middle width="25%" height=20><FONT color=#990000>EOS</FONT></TD></TR></TABLE> ; A- t4 t! L6 g' Y<>值得注意的是,"ni"的值不能被解释成WORD型值0xfa93,而应该看作两个值93和fa以这种顺序被作为"ni"的编码。0 M1 M- B1 \! l. X3 ]$ W6 r9 r6 z
1 X) h7 o8 c7 m5 i. ~6 _7 A<IMG src="http://www.vckbase.com/document/image/paragraph.gif"> <B>正确的遍历和索引字符串</B> 2 s. E9 D( N7 M% F" o& n, K5 S: p, W* t7 F
因为我们中大多数人都是用着SBCS字符串成长的,所以我们在遍历字符串时,常常使用指针的++-和-操作。我们也使用数组下标的表示形式来操作字符串中的字符。这两种方式是用于SBCS和Unicode字符串,因为它们中的字符有着相同的宽度,编译器能正确的返回我们需要的字符。 2 A( w( l, P0 B; T. J. [ 然而,当碰到DBCS字符串时,我们必须抛弃这些习惯。这里有使用指针遍历DBCS字符串时的两条规则。违背了这两条规则,你的程序就会存在DBCS有关的bugs。</P> 6 w- r+ b% Q: p! R<DIR>. F4 X( V$ H1 R3 h# }; O
<LI>1.在前向遍历时,不要使用++操作,除非你每次都检查lead byte; 5 v# d) b* b2 L( ~2 H$ J7 k3 |<LI>2.永远不要使用-操作进行后向遍历。 </LI></DIR> 9 t( j$ [0 Q/ b0 X9 ^<> 我们先来阐述规则2,因为找到一个违背它的真实的实例代码是很容易的。假设你有一个程序在你自己的目录里保存了一个设置文件,你把安装目录保存在注册表中。在运行时,你从注册表中读取安装目录,然后合成配置文件名,接着读取该文件。假设,你的安装目录是C:\Program Files\MyCoolApp,那么你合成的文件名应该是C:\Program Files\MyCoolApp\config.bin。当你进行测试时,你发现程序运行正常。% b3 T9 ?/ ?, b* A [
现在,想象你合成文件名的代码可能是这样的:</P><RE>bool GetConfigFileName ( char* pszName, size_t nBuffSize ) ; h. |4 m8 Q3 T, a: l7 b) K5 e. X% p: c{ + X7 B5 ~! E- t& f7 }7 O char szConfigFilename[MAX_PATH];" O% \+ T- r1 V8 `. I
% n) }. }) r- p! k7 l0 ]/ t6 |/ x // Read install dir from registry... we''ll assume it succeeds. ; w$ Q- C! {! j) o, H! k $ `/ u: T, W3 a. p. }8 p) E
// Add on a backslash if it wasn''t present in the registry value.& q( M* h, u. l1 i" k
// First, get a pointer to the terminating zero., W7 ?2 x& f+ B' g
char* pLastChar = strchr ( szConfigFilename, ''\0'' ); * f- `, e+ b: N' N) C $ @' u2 w0 k# H# ^
// Now move it back one character.# q9 }$ b) B0 N; N& s0 [! n% u3 i
pLastChar--; 8 |# [3 N% L# Q, y3 _
3 P6 W7 K7 k/ f' W9 g( A5 x+ f
if ( *pLastChar != ''\\'' ) q B4 h( W1 t" u/ k- I0 { strcat ( szConfigFilename, "\\" ); . C/ ? n; ]( y" k. \. | ( m( r9 C( T& n' @- L% o // Add on the name of the config file.* G* y8 \ {% `2 J1 @
strcat ( szConfigFilename, "config.bin" ); s: E1 r1 Q6 ^" i8 \
: C# {# Z0 Y& z1 m3 K$ D // If the caller''s buffer is big enough, return the filename. - U* H5 ?$ H5 J' |8 j( o2 w. ? if ( strlen ( szConfigFilename ) >= nBuffSize ), s4 r" B+ l% I
return false; + L2 F' }/ v: y$ `1 @- T else' c% J' a9 P6 m5 H! b9 \1 ~
{: o4 S' _/ V. I; t
strcpy ( pszName, szConfigFilename );9 w# t0 Q9 e0 c
return true; 6 T& x }1 @: d }2 X3 C4 o3 _6 m
} </PRE> 这是一段很健壮的代码,然而在遇到 DBCS 字符时它将会出错。让我们来看看为什么。假设一个日本用户使用了你的程序,把它安装在 C:\<IMG src="http://www.vckbase.com/document/journal/vckbase30/images/youkoso.gif" border=0>。下面是这个名字在内存中的存储形式:# {$ ~" ~+ q: A8 r
+ p$ ~+ h, {0 I# I% [* _ L& C<TABLE> % R/ b" y! p0 R+ V3 T; @ / t9 D( R4 J" t1 Y2 b# g<TR> - f$ M7 E* l6 N% \: f/ W% r<TD align=middle width="12%">43</TD> ' P1 t% `. L, R/ N<TD align=middle width="12%"><FONT color=#990000>3A</FONT></TD>3 L4 q, G1 c; g+ A w8 f' H
<TD align=middle width="12%"><FONT color=#0000ff>5C</FONT></TD># q$ H& k2 C" f# J Q# f5 U
<TD align=middle width="12%">83 88</TD>9 G# f+ Z" O" J! a1 [2 ?; q& k7 ]( }, Q9 _
<TD align=middle width="13%">83 45</TD> $ Q0 T/ D6 c0 i4 K6 ?& T<TD align=middle width="13%">83 52</TD> 4 u# k* W8 y# T<TD align=middle width="13%">83 <FONT color=#0000ff>5C</FONT></TD> 9 \. l5 {7 s( ]! o! ~! ^<TD align=middle width="13%">00</TD></TR>! V: u. F1 @9 m, m: f
<TR> 7 {. L% e3 P( n# R; C+ H<TD align=middle width="12%"> </TD> % s6 U, J+ Q- \% A: |- h( z% R7 _<TD align=middle width="12%"> </TD>( }" Q8 Q A$ E4 u1 n* q
<TD align=middle width="12%"> </TD>3 ]& ~$ D5 T# A
<TD align=middle width="12%"><FONT color=#990000>LB TB </FONT></TD>; o; h- y7 s6 `3 J3 U% n N) @
<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> ) @ P, H4 I; N! \3 ^<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD># C0 n, M* N: F# z. t L
<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> ' F2 m" C, t+ S0 R9 Y! o6 g1 @<TD align=middle width="13%"> </TD></TR>" \% I, `2 i2 B+ Y2 P! h: a
<TR> o- S8 g* U: k; T; _0 t2 R
<TD align=middle width="12%">C</TD>* D9 E. G5 ?* @8 w! }: j
<TD align=middle width="12%">:</TD>) C! U# i- J2 _( R5 ?- B3 v! E, K
<TD align=middle width="12%">\</TD># I% v' s* S5 F3 I) j( {
<TD align=middle width="12%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/yo.gif" border=0></TD> ' M" ?7 W. S% {- o4 Y' w<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/u.gif" border=0></TD> ! N. r# v4 S% ^1 k: S6 @9 S<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/ko.gif" border=0></TD>9 y3 i+ z$ V2 Z1 T- O
<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/so.gif" border=0></TD> |% [ Q( d% W' t+ ?
<TD align=middle width="13%">EOS</TD></TR></TABLE> + }/ F7 c. M) S) k; y<> 当使用 GetConfigFileName() 检查尾部的''\\''时,它寻找安装目录名中最后的非0字节,看它是等于''\\''的,所以没有重新增加一个''\\''。结果是代码返回了错误的文件名。$ C* i- E8 T) {2 r
哪里出错了呢?看看上面两个被用蓝色高量显示的字节。斜杠''\\''的值是0x5c。'' ''的值是83 5c。上面的代码错误的读取了一个 trail byte,把它当作了一个字符。7 E+ Z7 r9 s$ Q% t7 U- K e
正确的后向遍历方法是使用能够识别DBCS字符的函数,使指针移动正确的字节数。下面是正确的代码。(指针移动的地方用红色标明) </P><RE>bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )/ y3 M5 n) }! [5 _: k% V& i% @
{ % Q2 m5 H/ \$ k! e% q char szConfigFilename[MAX_PATH]; # ?" i" c. W! K' y: n1 x 1 J) i! G: |6 j E0 A* A6 l$ v
// Read install dir from registry... we''ll assume it succeeds. 0 \" s' {4 w; p9 q' Y) d2 Q. U + b7 T+ c- P7 O5 b( }2 G
// Add on a backslash if it wasn''t present in the registry value.) ?, {9 x& O6 r2 ~' S$ Z( j
// First, get a pointer to the terminating zero.& I2 ~3 @5 t. V& {* l8 [) q6 n
char* pLastChar = _mbschr ( szConfigFilename, ''\0'' ); 4 i' E( u! t* T2 y* I% Y 4 ^) A9 m, m+ j4 t
// Now move it back one double-byte character. $ j/ |; P$ [% F <FONT color=#ff0000> pLastChar = CharPrev ( szConfigFilename, pLastChar );</FONT>, `. V) K% { m/ I* F% `
- E& f+ x* U" D8 Q! }5 D. t if ( *pLastChar != ''\\'' ) 4 H4 W9 `! ~( M6 B6 R6 z _mbscat ( szConfigFilename, "\\" ); / C1 K9 B$ ?. t9 J! b$ k6 r$ ~ ! f8 m l& U7 N0 M3 K( B // Add on the name of the config file.' v" A, j. @" c, y9 A
_mbscat ( szConfigFilename, "config.bin" );9 i* q7 t! B% Y% F$ ]. ~: l' T# {
% U4 B. O* r9 g! W // If the caller''s buffer is big enough, return the filename. 9 b2 e. |9 R3 J6 d if ( _mbslen ( szInstallDir ) >= nBuffSize )3 t3 F' Z: V' B0 x
return false; & w) }! l: _5 A2 J. X) o8 c else) J, O. y5 Y# r
{ ; ?2 O* ~6 X( I1 F: J _mbscpy ( pszName, szConfigFilename ); 6 g, w/ O. i9 ]' F2 A0 y; G return true; % Q9 v) Z2 P8 s% ~* |* P }+ c) b, r7 K+ T* _6 z
} & N) O$ i% T7 Y</PRE> 上面的函数使用CharPrev() API使pLastChar向后移动一个字符,这个字符可能是两个字节长。在这个版本里,if条件正常工作,因为lead byte永远不会等于0x5c。- _* p+ |( M+ I; b
让我们来想象一个违背规则1的场合。例如,你可能要检测一个用户输入的文件名是否多次出现了'':''。如果,你使用++操作来遍历字符串,而不是使用CharNext(),你可能会发出不正确的错误警告如果恰巧有一个trail byte它的值的等于'':''的值。 5 A5 F6 o ^( V8 w与规则2相关的关于字符串索引的规则:<RE>2a. 永远不要使用减法去得到一个字符串的索引。</PRE>6 V0 C/ c3 x( @& D, [
<>违背这条规则的代码和违背规则2的代码很相似。例如,</P><RE>char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];</PRE>- b1 R/ ]; P) b5 H5 D
<>这和向后移动一个指针是同样的效果。 ( O- c# @" ~$ L7 }' S 1 ~8 z& Y0 z7 C3 p3 m' T<IMG src="http://www.vckbase.com/document/image/paragraph.gif"><B> 回到关于str***()和_mbs***()的区别</B> # J, t b& ~0 T1 Q& V . }% }8 J Q$ e# k 现在,我们应该很清楚为什么_mbs***()函数是必需的。Str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:\\ ", ''\\''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''\\''的指针。 8 F# g) K: _8 D& k V 关于字符串函数的最后一点:str***()和_mbs***()函数认为字符串的长度都是以char来计算的。所以,如果一个字符串包含3个双字节字符,_mbslen()将会返回6。Unicode函数返回的长度是按wchar_t来计算的。例如,wcslen(L"Bob")返回3。 ( w, g/ K; d# u9 y/ `* g' N# k/ i' r0 y7 }/ @
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"><B> Win32 API中的MBCS和Unicode</B> % _0 A$ N+ F5 @5 ~) I7 j, s- m0 K# z |! v
两组 APIs: , e$ m$ m0 z! L* D! z/ w
尽管你也许从来没有注意过,Win32中的每个与字符串相关的API和message都有两个版本。一个版本接受MBCS字符串,另一个接受Unicode字符串。例如,根本没有SetWindowText()这个API,相反,有SetWindowTextA()和SetWindowTextW()。后缀A表明这是MBCS函数,后缀W表示这是Unicode版本的函数。+ ^9 H6 ?4 h4 t6 U
当你 build 一个 Windows 程序,你可以选择是用 MBCS 或者 Unicode APIs。如果,你曾经用过VC向导并且没有改过预处理的设置,那表明你用的是MBCS版本。那么,既然没有 SetWindowText() API,我们为什么可以使用它呢?winuser.h头文件包含了一些宏,例如: </P><RE>BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString ); ! C9 s7 K2 Y4 H, w) _5 aBOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );, }4 k; [! i8 X8 t% _% S. G( Z