8 I. G* f5 O+ k5 z 因为我们中大多数人都是用着SBCS字符串成长的,所以我们在遍历字符串时,常常使用指针的++-和-操作。我们也使用数组下标的表示形式来操作字符串中的字符。这两种方式是用于SBCS和Unicode字符串,因为它们中的字符有着相同的宽度,编译器能正确的返回我们需要的字符。$ L9 F9 ]- Q, |, g( f
然而,当碰到DBCS字符串时,我们必须抛弃这些习惯。这里有使用指针遍历DBCS字符串时的两条规则。违背了这两条规则,你的程序就会存在DBCS有关的bugs。</P>7 z9 C8 K" a2 U0 ^9 {9 C
<DIR> 5 X" W( l g% c8 z<LI>1.在前向遍历时,不要使用++操作,除非你每次都检查lead byte; " k; u( ]$ c S/ g. t<LI>2.永远不要使用-操作进行后向遍历。 </LI></DIR>: G4 w% k- X1 R2 P! K2 u! @
<> 我们先来阐述规则2,因为找到一个违背它的真实的实例代码是很容易的。假设你有一个程序在你自己的目录里保存了一个设置文件,你把安装目录保存在注册表中。在运行时,你从注册表中读取安装目录,然后合成配置文件名,接着读取该文件。假设,你的安装目录是C:\Program Files\MyCoolApp,那么你合成的文件名应该是C:\Program Files\MyCoolApp\config.bin。当你进行测试时,你发现程序运行正常。 7 A9 l7 } B* {+ q# {; F 现在,想象你合成文件名的代码可能是这样的:</P><RE>bool GetConfigFileName ( char* pszName, size_t nBuffSize ) 5 w+ {" Z# ?* m. m' f& X{% R+ l2 {; t- y9 i7 D
char szConfigFilename[MAX_PATH];5 U/ E9 k1 _% o+ p8 Z+ u6 T
+ k' C5 c- q" g4 s7 ?" S // Read install dir from registry... we''ll assume it succeeds. : R; {; a( y# i5 z- P. i/ m: X 5 _4 l2 b" Q8 p9 a // Add on a backslash if it wasn''t present in the registry value.0 N8 f/ P) t. b- @- s f7 H
// First, get a pointer to the terminating zero. 9 P/ Z; t& x: T char* pLastChar = strchr ( szConfigFilename, ''\0'' ); 4 p* A0 h; Y5 d3 R5 @, W& o . q4 X0 u1 D: J, e8 s
// Now move it back one character.' m- e8 u1 H* P
pLastChar--; 3 Z! R9 s# u6 ~! y) Y6 L $ @! y/ v: |' X9 Y+ o if ( *pLastChar != ''\\'' ) - d$ b% M1 K1 x0 N7 {$ `: a strcat ( szConfigFilename, "\\" );' S7 B& ]( U+ Y
* S! m7 A, Y# ~3 E // Add on the name of the config file.5 |4 D4 Z8 ?9 z7 |
strcat ( szConfigFilename, "config.bin" ); 4 I% H: a5 R+ f 0 _. _- U( w5 c/ R0 @1 d // If the caller''s buffer is big enough, return the filename. - P1 @: O s3 w if ( strlen ( szConfigFilename ) >= nBuffSize ) + b0 r' {- b2 M return false; " r& O8 n. K% J$ x5 F0 |5 f4 q else0 v2 y5 Y; {: X1 ~. P0 S6 f
{ ! F+ N( j4 e% x strcpy ( pszName, szConfigFilename );0 @0 W( `8 s/ V) ~- ?
return true; * ~6 ^7 L5 {& E }- ]" @9 o& O- x5 i+ n
} </PRE> 这是一段很健壮的代码,然而在遇到 DBCS 字符时它将会出错。让我们来看看为什么。假设一个日本用户使用了你的程序,把它安装在 C:\<IMG src="http://www.vckbase.com/document/journal/vckbase30/images/youkoso.gif" border=0>。下面是这个名字在内存中的存储形式:/ K5 s& Y7 `$ I" P
4 H0 E; i3 t6 j3 d<TABLE>) n5 i% Z6 I) V. n V5 e
" N' X. H) i1 {<TR>" y; j4 _% P' k$ u T+ D
<TD align=middle width="12%">43</TD> . {4 i) A' N' y, O( T2 J<TD align=middle width="12%"><FONT color=#990000>3A</FONT></TD>5 [; u- x5 k" T# r& \
<TD align=middle width="12%"><FONT color=#0000ff>5C</FONT></TD> + z5 M0 L0 y5 \. y<TD align=middle width="12%">83 88</TD>6 G7 o' H/ R: K
<TD align=middle width="13%">83 45</TD> & k. R6 i( w7 _1 v<TD align=middle width="13%">83 52</TD> 0 Z& y; v4 c! Z" L1 P1 n& S4 T7 @<TD align=middle width="13%">83 <FONT color=#0000ff>5C</FONT></TD>3 S. ^0 i% K3 p) `
<TD align=middle width="13%">00</TD></TR> ' r' s9 j! n8 ~# ^% @2 @. z<TR> / j) r. y _) M: W @<TD align=middle width="12%"> </TD>. y! N, f+ g5 H, m: O/ Z
<TD align=middle width="12%"> </TD> 8 z% ]! N) ^- f8 X% T& c<TD align=middle width="12%"> </TD>, t2 y- f( P* S. C
<TD align=middle width="12%"><FONT color=#990000>LB TB </FONT></TD>) g6 G8 ~3 z) _4 Q. \% Q3 ~' o
<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> 9 Q; e% ^/ a3 }& Y/ ~<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> % v- T4 z* l. u9 t8 p' e: |& i) f<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> 2 \3 J! N+ L) A$ x# ?& Q<TD align=middle width="13%"> </TD></TR> ) J* D1 _* M1 r) Y; c. l<TR> % P0 c8 v+ d% N% O<TD align=middle width="12%">C</TD> 3 P" [! v Y% q0 K<TD align=middle width="12%">:</TD> z M! u$ P& K0 Z
<TD align=middle width="12%">\</TD> 4 D" p |+ y N3 m: k" {* j. x8 M; \8 {<TD align=middle width="12%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/yo.gif" border=0></TD>0 \/ Z7 m& u) y/ x, e0 A
<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/u.gif" border=0></TD> D) _/ B+ u* P) h( ~# M" @; `
<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/ko.gif" border=0></TD>1 z' Q$ b' U: X) c7 K6 _& @
<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/so.gif" border=0></TD>$ u$ p0 Z f. v. n9 Z) G# W2 f
<TD align=middle width="13%">EOS</TD></TR></TABLE>/ e6 ?9 e4 K& h% _" y
<> 当使用 GetConfigFileName() 检查尾部的''\\''时,它寻找安装目录名中最后的非0字节,看它是等于''\\''的,所以没有重新增加一个''\\''。结果是代码返回了错误的文件名。 0 A; X! ]; x1 L/ A- `8 K 哪里出错了呢?看看上面两个被用蓝色高量显示的字节。斜杠''\\''的值是0x5c。'' ''的值是83 5c。上面的代码错误的读取了一个 trail byte,把它当作了一个字符。- A6 o+ z& f: j: {; ?
正确的后向遍历方法是使用能够识别DBCS字符的函数,使指针移动正确的字节数。下面是正确的代码。(指针移动的地方用红色标明) </P><RE>bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize ) ( d3 y' [$ q( c8 w5 T+ D+ I& X{ 5 I' T9 a& N& ^1 j { char szConfigFilename[MAX_PATH]; 7 S' O- v1 B; r6 s 0 W' e( S& [: X: _5 K
// Read install dir from registry... we''ll assume it succeeds. a! e0 z" i6 | ) C7 S2 X/ G1 Q* U' q7 L5 l0 g // Add on a backslash if it wasn''t present in the registry value. # k# @, q6 c' E% w; [ // First, get a pointer to the terminating zero. % ?; O9 P+ E# J( W& ` char* pLastChar = _mbschr ( szConfigFilename, ''\0'' );- F' X: v3 h5 c; g. A
$ X' |& j. |8 {: @) f% _; C
// Now move it back one double-byte character.. R8 @4 ]4 {+ @2 [. G+ i
<FONT color=#ff0000> pLastChar = CharPrev ( szConfigFilename, pLastChar );</FONT>! h ~+ S7 ]2 ^# D
7 G5 \" j6 f9 P$ T2 D$ |; G
if ( *pLastChar != ''\\'' ) $ T# @/ A1 I* W! L% @3 D" n5 E _mbscat ( szConfigFilename, "\\" ); 4 s5 f9 t0 ~% y4 b/ i+ @; R" m " n: |- d0 T$ k
// Add on the name of the config file. t. A& B+ f, ]- d* _: t' G _mbscat ( szConfigFilename, "config.bin" ); * g6 o$ J) ]: Z+ V 6 c; }) w7 i9 S // If the caller''s buffer is big enough, return the filename. % U$ c" a( H2 I if ( _mbslen ( szInstallDir ) >= nBuffSize )) Z+ P; |( }( {
return false;3 g. c4 K2 d; d: N N
else# @5 N) R" p' D8 n
{ U, z$ G7 z1 a2 g+ H1 }/ L
_mbscpy ( pszName, szConfigFilename ); 7 x7 y$ f! Z9 P/ A4 l return true;8 i0 ]2 j1 y1 r4 E
} X& f% }- z7 O
} + ~9 D5 h" T$ d" ]! c</PRE> 上面的函数使用CharPrev() API使pLastChar向后移动一个字符,这个字符可能是两个字节长。在这个版本里,if条件正常工作,因为lead byte永远不会等于0x5c。8 ^; Z/ h0 D7 R
让我们来想象一个违背规则1的场合。例如,你可能要检测一个用户输入的文件名是否多次出现了'':''。如果,你使用++操作来遍历字符串,而不是使用CharNext(),你可能会发出不正确的错误警告如果恰巧有一个trail byte它的值的等于'':''的值。 7 b) g% g, Z- J6 ^# W' }与规则2相关的关于字符串索引的规则:<RE>2a. 永远不要使用减法去得到一个字符串的索引。</PRE>) V! u$ ~. z. `
<>违背这条规则的代码和违背规则2的代码很相似。例如,</P><RE>char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];</PRE> 0 Q W4 i( K0 X/ a, V: Y1 u( C<>这和向后移动一个指针是同样的效果。' }: F6 }8 k/ Y7 O' Z
' P" N+ A. I2 m
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"><B> 回到关于str***()和_mbs***()的区别</B> 2 \6 @" M# k. @7 ~- n* W5 s4 R" ]# u+ w0 |
现在,我们应该很清楚为什么_mbs***()函数是必需的。Str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:\\ ", ''\\''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''\\''的指针。 1 s0 `5 W; \/ y! Q, u6 e 关于字符串函数的最后一点:str***()和_mbs***()函数认为字符串的长度都是以char来计算的。所以,如果一个字符串包含3个双字节字符,_mbslen()将会返回6。Unicode函数返回的长度是按wchar_t来计算的。例如,wcslen(L"Bob")返回3。 + Q9 n. ]/ Y) l! |( H' J& d7 _3 u# ]5 U; k1 R% L
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"><B> Win32 API中的MBCS和Unicode</B> / W- s, D2 G$ V7 H: m A) N1 S * z" }% P5 c, O, D% J两组 APIs: & `/ g6 R4 V- [/ f1 L% N3 T* L
尽管你也许从来没有注意过,Win32中的每个与字符串相关的API和message都有两个版本。一个版本接受MBCS字符串,另一个接受Unicode字符串。例如,根本没有SetWindowText()这个API,相反,有SetWindowTextA()和SetWindowTextW()。后缀A表明这是MBCS函数,后缀W表示这是Unicode版本的函数。 " \" h. o! m# _" W4 I6 }$ a 当你 build 一个 Windows 程序,你可以选择是用 MBCS 或者 Unicode APIs。如果,你曾经用过VC向导并且没有改过预处理的设置,那表明你用的是MBCS版本。那么,既然没有 SetWindowText() API,我们为什么可以使用它呢?winuser.h头文件包含了一些宏,例如: </P><RE>BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString );+ K* O/ P* `/ k& b% f
BOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );% e1 S0 e( V9 x* G# c! ^8 }! w. h