- r. J, _, a" s6 e$ e<IMG src="http://www.vckbase.com/document/image/paragraph.gif"> <B>正确的遍历和索引字符串</B> 9 Q6 ^4 p; F! J, P( X/ w# {& { 2 d3 g2 {6 e8 a8 q6 y 因为我们中大多数人都是用着SBCS字符串成长的,所以我们在遍历字符串时,常常使用指针的++-和-操作。我们也使用数组下标的表示形式来操作字符串中的字符。这两种方式是用于SBCS和Unicode字符串,因为它们中的字符有着相同的宽度,编译器能正确的返回我们需要的字符。; H" C% v2 n0 o
然而,当碰到DBCS字符串时,我们必须抛弃这些习惯。这里有使用指针遍历DBCS字符串时的两条规则。违背了这两条规则,你的程序就会存在DBCS有关的bugs。</P> 8 }. w3 @ ^, Q) G, a; U! T<DIR>+ N5 S, x' s& Y% o$ A
<LI>1.在前向遍历时,不要使用++操作,除非你每次都检查lead byte; " r0 j+ y7 {6 ^# z2 g<LI>2.永远不要使用-操作进行后向遍历。 </LI></DIR># i# G: j ~5 A
<> 我们先来阐述规则2,因为找到一个违背它的真实的实例代码是很容易的。假设你有一个程序在你自己的目录里保存了一个设置文件,你把安装目录保存在注册表中。在运行时,你从注册表中读取安装目录,然后合成配置文件名,接着读取该文件。假设,你的安装目录是C:\Program Files\MyCoolApp,那么你合成的文件名应该是C:\Program Files\MyCoolApp\config.bin。当你进行测试时,你发现程序运行正常。( p' B x6 w9 | y
现在,想象你合成文件名的代码可能是这样的:</P><RE>bool GetConfigFileName ( char* pszName, size_t nBuffSize ) - n% e( U' Y6 A; F3 D% V{ 8 C, g! u+ b* C. ]' h' O3 D* m char szConfigFilename[MAX_PATH]; - O0 V0 }5 V2 ~- v' V. `" e . U, U1 C! _ C7 w // Read install dir from registry... we''ll assume it succeeds. $ A# `7 \, y3 p: a2 W 0 s b* P4 S$ R0 X // Add on a backslash if it wasn''t present in the registry value.$ @2 R/ ?2 c/ ?* S$ l
// First, get a pointer to the terminating zero. - i$ i1 |7 U! u( h# H. ` char* pLastChar = strchr ( szConfigFilename, ''\0'' );) w6 ?: [; d- |+ M) f
$ [; s0 O% j8 y' Z/ A
// Now move it back one character. 0 E. z: Y8 `) U' b+ n pLastChar--; : K$ u) |3 f0 d# [" _3 I" h/ R; n
( N, T6 l. d* \6 r" d' O* G
if ( *pLastChar != ''\\'' ): f* o0 q' v y L
strcat ( szConfigFilename, "\\" );; N4 e/ g7 W/ D8 x% w3 F! k5 C
- Y* p% [/ I/ C) {0 d // Add on the name of the config file. ' h4 ^, _8 ~# M0 u1 {4 J strcat ( szConfigFilename, "config.bin" );1 \- B) r7 h) _- Y1 F1 V# Y
' k2 D! w& M. J2 O& }' n8 i0 C
// If the caller''s buffer is big enough, return the filename.+ W2 @* _1 {7 f& c3 |% ?$ a) K" v
if ( strlen ( szConfigFilename ) >= nBuffSize ) d0 p; G* V, h0 R( o0 Y1 V8 a$ y return false; $ Z" d$ R2 N8 j" A# | else/ i' c" n6 T* Q7 O
{ E( q- ^* l8 p- Q v4 t* c strcpy ( pszName, szConfigFilename );* E# a, k9 \% M2 @+ X
return true;4 o8 `$ D6 w) \0 C& u
}4 x+ i. t) q8 O u
} </PRE> 这是一段很健壮的代码,然而在遇到 DBCS 字符时它将会出错。让我们来看看为什么。假设一个日本用户使用了你的程序,把它安装在 C:\<IMG src="http://www.vckbase.com/document/journal/vckbase30/images/youkoso.gif" border=0>。下面是这个名字在内存中的存储形式:( m( a5 {; a5 V3 o: [
8 d/ e/ t/ B( n5 [<TABLE> 0 C5 f1 w: _' L# A5 d 1 b' \* q# {4 _9 R/ P0 J' F" m<TR> $ @1 I) |! W _4 [( ^! ]<TD align=middle width="12%">43</TD>+ S' O& F+ b G* R$ i( {/ a( t6 _# m
<TD align=middle width="12%"><FONT color=#990000>3A</FONT></TD>3 ]2 v$ }* b0 X0 r* Q$ s7 ]9 z
<TD align=middle width="12%"><FONT color=#0000ff>5C</FONT></TD> 5 ]) [/ `( d, G<TD align=middle width="12%">83 88</TD>% {( L) b B. B- o* |
<TD align=middle width="13%">83 45</TD> ( N t/ D. v% l% C& e$ T<TD align=middle width="13%">83 52</TD> : X+ V( B, |7 H+ p5 {<TD align=middle width="13%">83 <FONT color=#0000ff>5C</FONT></TD> % ~: j( o/ Q8 ?8 c<TD align=middle width="13%">00</TD></TR> $ o) k# k9 b8 W<TR> U' U2 a7 o& r$ q0 l<TD align=middle width="12%"> </TD>+ \, H8 s+ K4 U# q
<TD align=middle width="12%"> </TD>2 u/ c; Q% ]2 G
<TD align=middle width="12%"> </TD>2 d9 [! ^& \& y* u3 m- d# L; p
<TD align=middle width="12%"><FONT color=#990000>LB TB </FONT></TD> ( J& H' l- i, {" J<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> ' O }/ P# [4 z! G7 d<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD>( J- i0 Y1 n K0 F( G" n
<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD>; r/ m' |4 g7 T1 p6 D/ r
<TD align=middle width="13%"> </TD></TR>9 E* I9 B( r9 X8 P3 m
<TR>$ I+ w! G4 c8 b8 V
<TD align=middle width="12%">C</TD> ; ?- Z) t0 h' }<TD align=middle width="12%">:</TD>+ S7 [0 s; s1 c0 R% ?
<TD align=middle width="12%">\</TD> % r2 m: ~: x2 y* O( O2 U<TD align=middle width="12%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/yo.gif" border=0></TD>2 t( y2 b. N, }1 ?
<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/u.gif" border=0></TD> * F K- \( t8 n6 Z) \<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/ko.gif" border=0></TD>. o" h. f/ @: r$ z3 x3 W: v- `
<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/so.gif" border=0></TD> - Q, W1 |- C, s' N4 O9 m3 R/ [1 N7 l6 M<TD align=middle width="13%">EOS</TD></TR></TABLE> - j$ j4 \4 W$ r2 C3 C; n<> 当使用 GetConfigFileName() 检查尾部的''\\''时,它寻找安装目录名中最后的非0字节,看它是等于''\\''的,所以没有重新增加一个''\\''。结果是代码返回了错误的文件名。 & O- {: o. H$ o! u K/ l 哪里出错了呢?看看上面两个被用蓝色高量显示的字节。斜杠''\\''的值是0x5c。'' ''的值是83 5c。上面的代码错误的读取了一个 trail byte,把它当作了一个字符。 0 A G7 a7 g$ e6 f/ J* e/ a 正确的后向遍历方法是使用能够识别DBCS字符的函数,使指针移动正确的字节数。下面是正确的代码。(指针移动的地方用红色标明) </P><RE>bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize ) 0 f# \! ^4 b! R H9 g/ |0 W9 P{ 7 g3 S% o. `# X4 Y( O, C/ P char szConfigFilename[MAX_PATH]; ( _! ^) G, X) E& C0 g) [ & C/ {8 C3 V6 l6 k8 s2 a
// Read install dir from registry... we''ll assume it succeeds.: d& k P! }% x4 i- K& @
5 n; t6 S' _/ i. Z6 p$ S. `0 |* T
// Add on a backslash if it wasn''t present in the registry value. 5 ]; {& b- F& ~) P" q // First, get a pointer to the terminating zero. 4 }$ q- y: G1 ^' ?3 R4 a char* pLastChar = _mbschr ( szConfigFilename, ''\0'' );5 C( O7 U1 {3 }: M
! w3 `0 }" q( y8 L8 h // Now move it back one double-byte character. 9 A K, g. {+ q6 @9 r# Y2 v <FONT color=#ff0000> pLastChar = CharPrev ( szConfigFilename, pLastChar );</FONT> w! H) y, X; D) d1 \. a
6 ?6 f/ C8 b U; E! q7 y" f if ( *pLastChar != ''\\'' ) : F& S! u e5 |* g2 I _mbscat ( szConfigFilename, "\\" ); 6 `8 p8 C) {1 d& h0 S 6 k) @0 N1 \; j! ~# i3 ` // Add on the name of the config file. : k' m1 Z" ^7 V. b U( M" K9 n2 {$ ] _mbscat ( szConfigFilename, "config.bin" ); + X$ B0 g5 J* j5 m; H0 D 4 I! ?: M: t/ F; n3 K // If the caller''s buffer is big enough, return the filename. , Y% k0 X, U! x5 z if ( _mbslen ( szInstallDir ) >= nBuffSize )2 w5 a( |2 i& {: M3 P0 [
return false;* ]1 M( r8 m7 @! b" a
else$ d3 p) U) \( _; m- t
{1 x3 D4 K7 g3 Q, p# z. N
_mbscpy ( pszName, szConfigFilename ); % [7 ^8 T' o* q2 r2 G9 g3 |: _ return true; ) d: w8 ~0 J' Z; |! X4 b }* W1 Q1 i" H1 V+ m5 e4 I- V+ O
}, C$ N( I' ?9 L" f" x& t% O
</PRE> 上面的函数使用CharPrev() API使pLastChar向后移动一个字符,这个字符可能是两个字节长。在这个版本里,if条件正常工作,因为lead byte永远不会等于0x5c。1 p4 h. K* B; v8 b2 z, Y' ]9 L
让我们来想象一个违背规则1的场合。例如,你可能要检测一个用户输入的文件名是否多次出现了'':''。如果,你使用++操作来遍历字符串,而不是使用CharNext(),你可能会发出不正确的错误警告如果恰巧有一个trail byte它的值的等于'':''的值。+ g% H0 l5 [5 j
与规则2相关的关于字符串索引的规则:<RE>2a. 永远不要使用减法去得到一个字符串的索引。</PRE> ; g+ S# m W2 a! C' y& b5 x3 _7 Z% E<>违背这条规则的代码和违背规则2的代码很相似。例如,</P><RE>char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];</PRE> 1 u; U% g3 [/ j8 [* s<>这和向后移动一个指针是同样的效果。8 _# t5 Z7 }+ ~( |' E. e
/ K, b: R, q+ x
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"><B> 回到关于str***()和_mbs***()的区别</B> ! ^7 F* |6 w( b% d , _- H" B/ W) q 现在,我们应该很清楚为什么_mbs***()函数是必需的。Str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:\\ ", ''\\''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''\\''的指针。' f5 m! O T4 t8 @7 q* P
关于字符串函数的最后一点:str***()和_mbs***()函数认为字符串的长度都是以char来计算的。所以,如果一个字符串包含3个双字节字符,_mbslen()将会返回6。Unicode函数返回的长度是按wchar_t来计算的。例如,wcslen(L"Bob")返回3。# W* b: O0 L* C; U* c* |