$ C( z# ^* P1 t 一眼看上去,DBCS 字符串很像 SBCS 字符串,但是我们一会儿将看到 DBCS 字符串的微妙之处,它使得使用字符串操作函数和永字符指针遍历一个字符串时会产生预料之外的结果。字符串" " ("nihongo")在内存中的存储形式如下(LB和TB分别用来表示 leading byte 和 trail byte)</P> # N+ B4 L, O( D3 g7 E% \. p1 ^! N; V<TABLE>; ^' A! G( M. w3 \; Q3 v
9 V4 ?3 u1 @9 F) h" _/ [1 |
<TR> 6 ]( A. p1 y" w. ?* @: J0 o. \+ G<TD align=middle width="25%" height=20>93 <FONT color=#990000>FA</FONT></TD> % Z3 E, `8 B: s0 d7 Q8 F<TD align=middle width="25%" height=20>96 <FONT color=#990000>7B</FONT></TD> 4 w# R* u% q( z<TD align=middle width="25%" height=20><FONT color=#990000>8C EA</FONT></TD> , h# a) @5 K) _" \<TD align=middle width="25%" height=20>00</TD></TR> 9 u9 l4 t0 Z( A<TR> ' P/ g) r" T% l# j3 B<TD align=middle width="25%" height=20><FONT color=#990000>LB TB</FONT></TD>% b. b* n1 ^3 c
<TD align=middle width="25%" height=20><FONT color=#990000>LB TB</FONT></TD>7 m. |8 H3 i& l; v5 m2 O
<TD align=middle width="25%" height=20><FONT color=#990000>LB TB</FONT></TD>+ D) N* }, \; @2 D2 h0 O
<TD align=middle width="25%" height=20><FONT color=#990000>EOS</FONT></TD></TR>$ B) g0 s: H, e: Z! N/ C
<TR> ( v, b) h( A! [- r<TD align=middle width="25%" height=20><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/ri.gif" border=0></TD>; I4 D+ ?$ e) f0 Q7 N" |& x
<TD align=middle width="25%" height=20><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/ben.gif" border=0></TD>0 B+ [! r1 P4 V' S% s. q2 O
<TD align=middle width="25%" height=20><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/yu.gif" border=0></TD>/ C% s- c1 Y6 `
<TD align=middle width="25%" height=20><FONT color=#990000>EOS</FONT></TD></TR></TABLE> & ]$ D8 y i* t8 Z! g( M @. W. X<>值得注意的是,"ni"的值不能被解释成WORD型值0xfa93,而应该看作两个值93和fa以这种顺序被作为"ni"的编码。 ' e2 P. w0 p9 E6 p3 T' c# W2 |* M ]" [5 r* ^; s$ ~
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"> <B>使用字符串处理函数</B> 9 d# p: g+ a% o3 C u 1 @+ u, w! r9 {+ L( ]% Q! l) g1 Z 我们都已经见过C语言中的字符串函数,strcpy(), sprintf(), atoll()等。这些字符串只应该用来处理单字节字符字符串。标准库也提供了仅适用于Unicode类型字符串的函数,比如wcscpy(), swprintf(), wtol()等。# q4 Y' D- J7 `5 Y- a$ a( K
微软还在它的CRT(C runtime library)中增加了操作DBCS字符串的版本。Str***()函数都有对应名字的DBCS版本_mbs***()。如果你料到可能会遇到DBCS字符串(如果你的软件会被安装在使用DBCS编码的国家,如中国,日本等,你就可能会),你应该使用_mbs***()函数,因为他们也可以处理SBCS字符串。(一个DBCS字符串也可能含有单字节字符,这就是为什么_mbs***()函数也能处理SBCS字符串的原因) * u9 p, Q" M+ z ` 让我们来看一个典型的字符串来阐明为什么需要不同版本的字符串处理函数。我们还是使用前面的Unicode字符串 L"Bob":</P>2 N% p3 K4 t$ I, Y3 B
<TABLE> 2 N4 c4 P6 F5 z& r* e8 N; t; ~ r( R0 U5 W- a8 X<TR>' V. F4 n7 V7 B* l( ], w, t# a) J
<TD align=middle width="25%">42 00 </TD>/ N: O: H0 z1 g0 Y0 G) ]7 U' d; U$ [
<TD align=middle width="25%"><FONT color=#990000>6F</FONT> 00</TD># b, Z. {3 c' l* P# z7 r
<TD align=middle width="25%">62 00</TD> 1 v7 K/ p5 ]. [$ E) Q" I/ R<TD align=middle width="25%">00 00</TD></TR> % T3 ^ _) Y( k, C; x6 M4 |3 w<TR># [1 H7 f. m( W5 J0 P! K8 X5 A$ w
<TD align=middle width="25%"><FONT color=#990000>B</FONT></TD> t3 h3 Y, k4 r3 J( W! `# m<TD align=middle width="25%"><FONT color=#990000>o</FONT></TD> ! S L+ }, W) k) |) q5 f( k<TD align=middle width="25%"><FONT color=#990000>b</FONT></TD> Z+ B0 G) b N<TD align=middle width="25%"><FONT color=#990000>BOS</FONT></TD></TR></TABLE> 0 P6 r G. ~7 h! ~* R<> 因为x86CPU是little-endian,值0x0042在内存中的存储形式是42 00。你能看出如果这个字符串被传给strlen()函数会出现什么问题吗?它将先看到第一个字节42,然后是00,而00是字符串结束的标志,于是strlen()将会返回1。如果把"Bob"传给wcslen(),将会得出更坏的结果。wcslen()将会先看到0x6f42,然后是0x0062,然后一直读到你的缓冲区的末尾,直到发现00 00结束标志或者引起了GPF。4 @+ h; G' L3 }9 `; c; ~
到目前为止,我们已经讨论了str***()和wcs***()的用法及它们之间的区别。Str***()和_mbs**()之间的有区别区别呢?明白他们之间的区别,对于采用正确的方法来遍历DBCS字符串是很重要的。下面,我们将先介绍字符串的遍历,然后回到str***()与_mbs***()之间的区别这个问题上来。 : i, Y$ @' [ R& U$ U$ ~4 Y: V7 \6 T7 t2 V6 Y$ Y9 W' k, `$ c
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"> <B>正确的遍历和索引字符串</B>& r! e2 ?* ^3 V# F n& s3 k
- S$ \" q# f" S$ P1 I
因为我们中大多数人都是用着SBCS字符串成长的,所以我们在遍历字符串时,常常使用指针的++-和-操作。我们也使用数组下标的表示形式来操作字符串中的字符。这两种方式是用于SBCS和Unicode字符串,因为它们中的字符有着相同的宽度,编译器能正确的返回我们需要的字符。* ~. D2 N& U9 p2 Z! N
然而,当碰到DBCS字符串时,我们必须抛弃这些习惯。这里有使用指针遍历DBCS字符串时的两条规则。违背了这两条规则,你的程序就会存在DBCS有关的bugs。</P>" ~- N( y9 ~: i/ O+ j' Q
<DIR> ' q8 I% E( e$ s0 e7 Q<LI>1.在前向遍历时,不要使用++操作,除非你每次都检查lead byte; : d0 y: `0 k# o; `4 r<LI>2.永远不要使用-操作进行后向遍历。 </LI></DIR>* T d: T1 g, T- X: Z
<> 我们先来阐述规则2,因为找到一个违背它的真实的实例代码是很容易的。假设你有一个程序在你自己的目录里保存了一个设置文件,你把安装目录保存在注册表中。在运行时,你从注册表中读取安装目录,然后合成配置文件名,接着读取该文件。假设,你的安装目录是C:\Program Files\MyCoolApp,那么你合成的文件名应该是C:\Program Files\MyCoolApp\config.bin。当你进行测试时,你发现程序运行正常。) b5 _4 f1 I; \ b+ J% \
现在,想象你合成文件名的代码可能是这样的:</P><RE>bool GetConfigFileName ( char* pszName, size_t nBuffSize )1 i" i7 ]$ E* K% J0 y* F
{, [$ ^+ a( e; p) K
char szConfigFilename[MAX_PATH]; , ?( I" w: C6 m5 o' D3 ^ X ( t! [* O( J4 P1 Q. a) C* U
// Read install dir from registry... we''ll assume it succeeds." F s$ }* C4 n
( W9 V1 T$ r: G8 ~0 h$ D) c // Add on a backslash if it wasn''t present in the registry value. $ z' {- y5 r9 y o% g/ ^- ^9 p" z8 H // First, get a pointer to the terminating zero.5 t; d2 ~! {# ^! H' N, e
char* pLastChar = strchr ( szConfigFilename, ''\0'' );$ c( B; ^6 v* ?
6 x' U3 n& g5 M
// Now move it back one character." S! ?* N6 y0 W' o! n( |- q
pLastChar--; ( n2 o, K* q3 l, W# R8 A R
2 z( a3 V/ F9 N if ( *pLastChar != ''\\'' )* w* d Q8 R! U
strcat ( szConfigFilename, "\\" ); & S1 a6 z5 N& G* V + U: e3 S' i9 {/ I3 u% Q // Add on the name of the config file.. z! N. t9 R* L9 f& Q/ F* G" y
strcat ( szConfigFilename, "config.bin" ); 2 {) s3 }& @1 S8 |# {3 | 8 G, X" X, w/ J: P2 B* M // If the caller''s buffer is big enough, return the filename.7 F& S( x2 J \% ~) S2 D
if ( strlen ( szConfigFilename ) >= nBuffSize ) , g$ M5 ]$ d% W return false; 8 K$ w4 j5 |( P$ i else 0 m; f0 S" R. o. |8 T+ i% e {7 {& I& C: \; L5 _/ @( J" s
strcpy ( pszName, szConfigFilename ); : D6 c$ }: o5 U6 S return true;7 ^2 E; H% N5 x
} - T7 }9 P: n" w0 R/ l8 T# D} </PRE> 这是一段很健壮的代码,然而在遇到 DBCS 字符时它将会出错。让我们来看看为什么。假设一个日本用户使用了你的程序,把它安装在 C:\<IMG src="http://www.vckbase.com/document/journal/vckbase30/images/youkoso.gif" border=0>。下面是这个名字在内存中的存储形式: 4 Z4 G$ Q, G8 X2 J) ~/ G 8 W! ^) d$ E w
<TABLE>: V& p6 j( `, w/ e( G9 o' }
1 Q+ w* y- ^6 X
<TR> 9 `* k+ `1 G, M6 M<TD align=middle width="12%">43</TD>$ r+ R, n. N) K+ j* `3 L- N
<TD align=middle width="12%"><FONT color=#990000>3A</FONT></TD>8 W/ }3 b2 k4 ^8 `7 G" h1 w
<TD align=middle width="12%"><FONT color=#0000ff>5C</FONT></TD>- B( z. t- g* L! L( U# k4 ~' y: U2 |# z
<TD align=middle width="12%">83 88</TD>6 u' R9 c) u) J2 m
<TD align=middle width="13%">83 45</TD>& v7 X4 k/ }; W
<TD align=middle width="13%">83 52</TD> 9 d' w) T* v3 ~3 f) J* @4 j7 t4 h' N<TD align=middle width="13%">83 <FONT color=#0000ff>5C</FONT></TD>9 T3 `$ o# K: B# q9 N
<TD align=middle width="13%">00</TD></TR>% @6 \. I4 Y# L }$ w F: v% T
<TR> ; Y& |& A2 Y4 [: R<TD align=middle width="12%"> </TD> 3 h/ L z( c; g1 v# W<TD align=middle width="12%"> </TD> " d v" D$ Z; V# l<TD align=middle width="12%"> </TD>" C7 t# \* e/ y! |0 i8 B) e
<TD align=middle width="12%"><FONT color=#990000>LB TB </FONT></TD>: M: @6 z2 W2 F. a2 H. ?1 b( v
<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> ( |* Y) v* P# r' |<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> 0 ~1 x* P7 u# A3 \6 f# s# O<TD align=middle width="13%"><FONT color=#990000>LB TB </FONT></TD> 2 N2 f0 E% ^) X+ F; H<TD align=middle width="13%"> </TD></TR>4 J- P5 S% z: q0 a7 Z; c
<TR> ' k2 F: z% K7 w9 ?<TD align=middle width="12%">C</TD>) m- [7 ]# }: ]( Y
<TD align=middle width="12%">:</TD>3 `# M Y/ n1 M6 C9 ?3 U4 s
<TD align=middle width="12%">\</TD> 5 L2 g0 y/ t7 O4 p<TD align=middle width="12%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/yo.gif" border=0></TD> * ~- p( f/ B+ U<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/u.gif" border=0></TD>. J! I, q+ N. e+ P$ q9 E/ x
<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/ko.gif" border=0></TD> t) `. ?/ B" d: y: _
<TD align=middle width="13%"><IMG src="http://www.vckbase.com/document/journal/vckbase30/images/so.gif" border=0></TD>- o2 K9 x/ ]+ Q( Q# u
<TD align=middle width="13%">EOS</TD></TR></TABLE> 2 W @) Q( z3 V$ Z- i<> 当使用 GetConfigFileName() 检查尾部的''\\''时,它寻找安装目录名中最后的非0字节,看它是等于''\\''的,所以没有重新增加一个''\\''。结果是代码返回了错误的文件名。# i. r3 r, _' k% S. f* v% D9 X
哪里出错了呢?看看上面两个被用蓝色高量显示的字节。斜杠''\\''的值是0x5c。'' ''的值是83 5c。上面的代码错误的读取了一个 trail byte,把它当作了一个字符。& ?2 v) a! D: B+ Z( ^; f
正确的后向遍历方法是使用能够识别DBCS字符的函数,使指针移动正确的字节数。下面是正确的代码。(指针移动的地方用红色标明) </P><RE>bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )1 z& n+ J. F9 a/ o
{ # e$ U1 y7 ~8 C9 {. ] char szConfigFilename[MAX_PATH]; 8 U4 z! h( w& y$ d- e0 d" k$ @' _# { 3 |. i! v4 }: J& c0 } f4 O
// Read install dir from registry... we''ll assume it succeeds. 4 ]4 f' z9 q* E4 q" O X % j1 B2 B% g5 [/ e% f
// Add on a backslash if it wasn''t present in the registry value. & x, n/ n8 { {4 L) J% [" A# {+ ^ // First, get a pointer to the terminating zero., r I/ b% ^7 `( i: d; w
char* pLastChar = _mbschr ( szConfigFilename, ''\0'' ); ; L9 w$ J( ?+ A/ b- H& b( K ! F2 J1 X$ J( `9 { // Now move it back one double-byte character." G) d) \' s3 g) R
<FONT color=#ff0000> pLastChar = CharPrev ( szConfigFilename, pLastChar );</FONT> * G; U8 _7 F% Y# q) w & x) ?3 o5 R% T: T6 G3 G+ m# x if ( *pLastChar != ''\\'' )6 S' B- `9 ^; j5 {( a
_mbscat ( szConfigFilename, "\\" );% Q# @. ?+ E# y% r, n) k' k
T* p8 o" I9 i( O // Add on the name of the config file. 5 ~2 @4 I3 i4 q+ S7 G0 ?% {) ^+ F _mbscat ( szConfigFilename, "config.bin" );6 L( A, m) k; Q6 q# }
" L7 ^0 W- n# r- t* l // If the caller''s buffer is big enough, return the filename. / _! Y7 `) n! A0 t if ( _mbslen ( szInstallDir ) >= nBuffSize ) . x/ e3 }1 W) R- S8 [$ C* g return false;& p6 E2 m% y& I6 k+ e1 ]0 u, l
else2 k% }, O1 ^) m& g# s0 n
{ # C% I; X( ^4 Y0 u7 ?, ~ _mbscpy ( pszName, szConfigFilename );6 @: |+ G4 v/ n# S4 @5 W' B
return true; . u9 j1 l6 o6 G5 d; d! i/ k }8 ^" G7 W u' Z x; P0 E0 [
} d# ~9 u4 P* m* ~
</PRE> 上面的函数使用CharPrev() API使pLastChar向后移动一个字符,这个字符可能是两个字节长。在这个版本里,if条件正常工作,因为lead byte永远不会等于0x5c。 # K, ` A# C; L. q 让我们来想象一个违背规则1的场合。例如,你可能要检测一个用户输入的文件名是否多次出现了'':''。如果,你使用++操作来遍历字符串,而不是使用CharNext(),你可能会发出不正确的错误警告如果恰巧有一个trail byte它的值的等于'':''的值。 ) }$ g" n; C: u与规则2相关的关于字符串索引的规则:<RE>2a. 永远不要使用减法去得到一个字符串的索引。</PRE> 3 m# V* B( B8 J, e; X9 n<>违背这条规则的代码和违背规则2的代码很相似。例如,</P><RE>char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];</PRE> ) I( M% z5 @, }<>这和向后移动一个指针是同样的效果。 : _) S7 z% C! p3 \7 r$ \/ \/ m* x, S8 |2 @3 ^# F' A
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"><B> 回到关于str***()和_mbs***()的区别</B> , T4 I- C r& ^9 z0 x$ y2 g: D& |% o, F
现在,我们应该很清楚为什么_mbs***()函数是必需的。Str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:\\ ", ''\\''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''\\''的指针。% k2 m8 E- _# p
关于字符串函数的最后一点:str***()和_mbs***()函数认为字符串的长度都是以char来计算的。所以,如果一个字符串包含3个双字节字符,_mbslen()将会返回6。Unicode函数返回的长度是按wchar_t来计算的。例如,wcslen(L"Bob")返回3。" q" W$ G+ k! C8 O Z5 e g
, s1 C S5 _& d* O2 a
<IMG src="http://www.vckbase.com/document/image/paragraph.gif"><B> Win32 API中的MBCS和Unicode</B> ' M5 h& B' d, @+ _$ } 6 ~% l# D. e' o& ^: l0 O4 N! h7 G. ^# _两组 APIs: " I, N. b2 I: n* S- W3 C3 S8 M 尽管你也许从来没有注意过,Win32中的每个与字符串相关的API和message都有两个版本。一个版本接受MBCS字符串,另一个接受Unicode字符串。例如,根本没有SetWindowText()这个API,相反,有SetWindowTextA()和SetWindowTextW()。后缀A表明这是MBCS函数,后缀W表示这是Unicode版本的函数。# Z- w. Z `2 A( _+ @
当你 build 一个 Windows 程序,你可以选择是用 MBCS 或者 Unicode APIs。如果,你曾经用过VC向导并且没有改过预处理的设置,那表明你用的是MBCS版本。那么,既然没有 SetWindowText() API,我们为什么可以使用它呢?winuser.h头文件包含了一些宏,例如: </P><RE>BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString ); ! n2 c% e; Y' pBOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );6 g+ n0 v# |4 ^6 D! ~! T