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。
我们先来阐述规则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。
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*) |
|
|
|
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将会使你受益:
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你的程序,你只需要改变一下预处理器中的设置就可以实现了。
在VC中从图标资源动态装载为光标 | |||
今天在CSDN上注册了个用户,顺便回答了论坛中的几个帖子,其中有一个是从图标资源动态装载为光标的问题,顺便在此贴出来: HANDLE hCursor = ::LoadImage(AfxGetResourceHandle(), MAKEINTRESOURCE(IDR_MAINFRAME),//icon id IMAGE_ICON, 0, 0, LR_DEFAULTSIZE); if(NULL == hCursor) { LPVOID lpMsgBuf; ::FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language (LPTSTR) &lpMsgBuf, 0, NULL ); ::MessageBox( NULL, (LPCTSTR)lpMsgBuf, "Error", MB_OK | MB_ICONINFORMATION ); LocalFree( lpMsgBuf ); } else { ::SetCursor((HCURSOR)hCursor); } |
|||
|
|||
|
|||
|
7月17日 | ||||||||||||
在VC中处理符号分隔的字符串 | ||||||||||||
一。前言
程序中经常会遇到要处理用某些符号(如空格,或“, ; . | \t”等)分隔的字符串的问题,我在此做了一些总结和比较。
二。处理方法
1. 用MFC CString之Find处理字符串
// 以下方法将一个字符串分解后放到一个CStringArray中:
void ExtractString(CStringArray& arr, const CString strSrc, const CString sep = "\r\n" )
{ // 预处理: 可根据需要决定是否需要Trim,以及是Trim掉空格/还是分隔符/还是其它 CString str(strSrc); str.TrimLeft(); str.TrimRight();
if(str.IsEmpty())
return;
// 开始分解
int pos = str.Find(sep); while (pos != -1) { //if(!str.Left(pos).IsEmpty()) // 如有必要也可在此Trim后再判断是否为空,为空则舍弃 arr.Add(str.Left(pos));
str = str.Mid(pos + sep.GetLength());
pos = str.Find(sep); }
arr.Add(str); // think
}
// BTW,CString提供了TrimLeft和TrimRight,却不能一次TrimAll,并返回Trim后的字符串,我写了一个:
CString TrimAll(CString str, CString strTrim = " ")
{ str.TrimLeft(strTrim); str.TrimRight(strTrim); return CString(str); }
// 同样是用Find处理字符串,和以上处理方式稍有不同,试比较:
void ExtractString2(CStringArray& arr, const CString strSrc, const CString sep = "\r\n" )
{ CString str(strSrc); if(TrimAll(str).IsEmpty()) return;
// 开始分解
int pos = str.Find(sep); if(pos == -1) // 未找到分隔符 arr.Add(str); else // 找到分隔符 { str += sep; // think
CString s;
while (pos != -1) { s = str.Left(pos); if(!TrimAll(s).IsEmpty()) arr.Add(s);
str = str.Mid(pos + sep.GetLength());
pos = str.Find(sep); } } }
2. 用MFC未公开函数AfxExtractSubString 处理
// 以下为函数定义及说明:
// AfxExtractSubString 从一个字符(chSep)分隔的字符串(lpszFullString)中取出第iSubString个子串,输出到rString
BOOL AFXAPI AfxExtractSubString (
CString& rString, // 用于输出子串 LPCTSTR lpszFullString, // 被分隔的字符串 int iSubString, // zero-based substring index TCHAR chSep = '\n' // 分隔符 )
// eg:
CString sDesc= "张三|男|28|医生";
CString sOccupation;
if(AfxExtractSubString ( sOccupation, sDesc, 3, '|'))
cout << "职业:" << sOccupation << endl;
3. 用C语言之strtok函数处理
#include <assert.h>
void test()
{ char* str = "06317377244|13805871280|20040210105049|193|NBGW1|040C|0017|8"; char seps[] = ",;|";
char* temp = (char*)malloc(sizeof(char) * (strlen(str)+1));
strcpy(temp, str); char* token = strtok(temp, seps); while (NULL != token) { printf("%s\t", token); token = strtok(NULL, seps); }
free(temp);
}
4. 其它方法
当然既然能用MFC的Find函数进行处理,也可能string的find处理,甚至可用最原始的字符比较,再配合一些字串处理函数进行处理。
但我觉得我提供的使用Find类函数处理字串的2种方法非常常用,可供参考。
三。比较
四。后记
以上只是我从个人的角度做的一个小结(我用MFC/C++多一些),难免有失偏颇,在BCB/Delphi中肯定也有对应的方式方法,不在本文的讨论范围内,这里就不再赘述。
|