UNICODE与多字节字符集的区别及转换

时间:2023-01-04 12:59:24

一、一点历史

在计算机中字符通常并不是保存为图像,每个字符都是使用一个编码来表示的,而每个字符究竟使用哪个编码代表,要取决于使用哪个字符集(charset)。 在最初的时候,Internet上只有一种字符集——ANSI的ASCII字符集,它使用7 bits来表示一个字符,总共表示128个字符,其中包括了英文字母、数字、标点符号等常用字符。之后,又进行扩展,使用8 bits表示一个字符,可以表示256个字符,主要在原来的7 bits字符集的基础上加入了一些特殊符号例如制表符。 后来,由于各国语言的加入,ASCII已经不能满足信息交流的需要,因此,为了能够表示其它国家的文字,各国在ASCII的基础上制定了自己的字符集,这些从ANSI标准派生的字符集被习惯的统称为ANSI字符集,它们正式的名称应该是MBCS(Multi-Byte Chactacter System,即多字节字符系统)。


  这些派生字符集的特点是以ASCII 127 bits为基础,兼容ASCII 127,他们使用大于128的编码作为一个Leading Byte,紧跟在Leading Byte后的第二(甚至第三)个字符与Leading Byte一起作为实际的编码。这样的字符集有很多,我们常见的GB-2312就是其中之一。 例如在GB-2312字符集中,“连通”的编码为C1 AC CD A8,其中C1和CD就是Leading Byte。前127个编码为标准ASCII保留,例如“0”的编码是30H(30H表示十六进制的30)。软件在读取时,如果看到30H,知道它小于128就是标准ASCII,表示“0”,看到C1大于128就知道它后面有一个另外的编码,因此C1 AC一同构成一个整个的编码,在GB-2312字符集中表示“连”。 由于每种语言都制定了自己的字符集,导致最后存在的各种字符集实在太多,在国际交流中要经常转换字符集非常不便。因此,提出了Unicode字符集,它固定使用16 bits(两个字节、一个字)来表示一个字符,共可以表示65536个字符。将世界上几乎所有语言的常用字符收录其中,方便了信息交流。标准的Unicode称为UTF-16。后来为了双字节的Unicode能够在现存的处理单字节的系统上正确传输,出现了UTF-8,使用类似MBCS的方式对Unicode进行编码。注意UTF-8是编码,它属于Unicode字符集。Unicode字符集有多种编码形式,而ASCII只有一种,大多数MBCS(包括GB-2312)也只有一种。Unicode的最初目标,是用1个16位的编码来为超过65000字符提供映射。但这还不够,它不能覆盖全部历史上的文字,也不能解决传输的问题 (implantation head-ache's),尤其在那些基于网络的应用中。已有的软件必须做大量的工作来程序16位的数据。因此,Unicode用一些基本的保留字符制定了三套编码方式。它们分别是UTF-8,UTF-16和UTF-32。正如名字所示,在UTF-8中,字符是以8位序列来编码的,用一个或几个字节来表示一个字符。这种方式的最大好处,是UTF-8保留了ASCII字符的编码做为它的一部分,例如,在UTF-8和ASCII中,“A”的编码都是0x41.UTF-16和UTF-32分别是Unicode的16位和32位编码方式。


  考虑到最初的目的,通常说的Unicode就是指UTF-16。 例如“连通”两个字的Unicode标准编码UTF-16 (big endian)为:DE 8F 1A 90而其UTF-8编码为:E8 BF 9E E9 80 9A 最后,当一个软件打开一个文本时,它要做的第一件事是决定这个文本究竟是使用哪种字符集的哪种编码保存的。软件有三种途径来决定文本的字符集和编码: 最标准的途径是检测文本最开头的几个字节,如下表:开头字节 Charset/encodingEF BB BF UTF-8FE FF UTF-16/UCS-2, little endianFF FE UTF-16/UCS-2, big endianFF FE 00 00 UTF-32/UCS-4, little endian.00 00 FE FF UTF-32/UCS-4, big-endian.例如**标记后,连通”两个字的UTF-16 (big endian)和UTF-8码分别为:FF FE DE 8F 1A 90EF BB BF E8 BF 9E E9 80 9A 但是MBCS文本没有这些位于开头的字符集标记,更不幸的是,一些早期的和一些设计不良的软件在保存Unicode文本时不**这些位于开头的字符集标记。因此,软件不能依赖于这种途径。这时,软件可以采取一种比较安全的方式来决定字符集及其编码,那就是弹出一个对话框来请示用户,例如将那个“连通”文件拖到MS Word中,Word就会弹出一个对话框。 如果软件不想麻烦用户,或者它不方便向用户请示,那它只能采取自己“猜”的方法,软件可以根据整个文本的特征来猜测它可能属于哪个charset,这就很可能不准了。使用记事本打开那个“连通”文件就属于这种情况。


  我们可以证明这一点:在记事本中键入“连通”后,选择“Save As”,会看到最后一个下拉框中显示有“ANSI”,这时保存。当再当打开“连通”文件出现乱码后,再点击“File”->“Save As”,会看到最后一个下拉框中显示有“UTF-8”,这说明记事本认为当前打开的这个文本是一个UTF-8编码的文本。而我们刚才保存时是用ANSI字符集保存的。这说明,记事本猜测了“连通”文件的字符集,认为它更像一个UTF-8编码文本。这是因为“连通”两个字的GB-2312编码看起来更像UTF-8编码导致的,这是一个巧合,不是所有文字都这样。可以使用记事本的打开功能,在打开“连通”文件时在最后一个下拉框中选择ANSI,就能正常显示了。反过来,如果之前保存时保存为UTF-8编码,则直接打开也不会出现问题。 如果将“连通”文件放入MS Word中,Word也会认为它是一个UTF-8编码的文件,但它不能确定,因此会弹出一个对话框询问用户,这时选择“简体中文(GB2312)”,就能正常打开了。


  记事本在这一点上做得比较简化罢了,这与这个程序的定位是一致的。需要提醒大家的是,部分Windows 2000字型无法显示所有的Unicode字符。如果发现文件中缺少了某些字符,只需将其变更为其它字型即可。big endian和little endianbig endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次*,其中一个皇帝送了命,另一个丢了王位。我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。Unicode big endian:在Big-endian处理器(如苹果Macintosh电脑)上建立的Unicode文件中的文字位元组(存放单位)排列顺序,与在Intel处理器上建立的文件的文字位元组排列顺序相反。最重要的位元组拥有最低的地址,且会先储存文字中较大的一端。为使这类电脑的用户能够存取你的文件,可选择Unicode big-endian格式。


  应该说Unicode字符集更加通用,但这里读取目录因为用到Windows.h里面的一些东西,所以需要多字节字符集,毕竟Microsoft的东西兼容性一直不太好,所以如果可以的话,还是使用Unicode字符集比较好!


二、UNICODE与多字节字符集

VS2008默认的字符集是Unicode,而VC6.0默认是多字节字符集,Unicode字符集你要加_T("")或L"",你也可以“

工程-属性-修改字符集”。


 1. UNICODE:它是用两个字节表示一个字符的方法。比如字符'A'在ASCII下面是一个字符,可'A'在UNICODE下面是两个字符,高字符用0填充,而且汉字'程'在ASCII下面是两个字节,而在UNICODE下仍旧是两个字节。UNICODE的用处就是定长表示世界文字,据统计,用两个字节可以编码现存的所有文字而没有二义。 

 
 2. MBCS,它是多字节字符集,它是不定长表示世界文字的编码。MBCS表示英文字母时就和ASCII一样(这也是我们容易把MBCS和ASCII搞混的原因),但表示其他文字时就需要用多字节。

 

WINDOWS下面的程序设计可以支持MBCS和UNICODE两种编码的字符串,具体用那种就看你定义了MBCS宏还是UNICODE宏。MBCS宏对应的字符串指针是char*也就是LPSTR,UNICODE对应的指针是unsigned   short*也就是LPWSTR,为了写程序方便微软定义了类型LPTSTR,在MBCS下他就是char*,   在UNICODE下它unsigned  

char*,这样你就可以重定义一个宏进行不同字符集的转换了。


3. LPTSTR、LPCSTR、LPCTSTR、LPSTR的意义

LPSTR:  32-bit指针 指向一个字符串,每个字符占1字节
LPCSTR:  32-bit指针 指向一个常字符串,每个字符占1字节
LPCTSTR: 32-bit指针 指向一个常字符串,每字符可能占1字节或2字节,取决于Unicode是否定义
LPTSTR:  32-bit指针 每字符可能占1字节或2字节,取决于Unicode是否定义

 

Windows使用两种字符集ANSI和UNICODE,前者就是通常使用的单字节方式,但这种方式处理象中文这样的双字节字符不方便,容易出现半个汉字的情况。而后者是双字节方式,方便处理双字节字符。WindowsNT的所有与字符有关的函数都提供两种方式的版本,而Windows9x只支持ANSI方式。_T一般同字常数相关,如_T("Hello"。如果你编译一个程序为ANSI方式,_T实际不起任何作用。而如果编译一个程序为UNICODE方式,则编译器会把"Hello"字符串以UNICODE方式保存。_T和_L的区别在于,_L不管你是以什么方式编译,一律以UNICODE方式保存.

 

 4. 例1:

Windows核心编程的第一章。

L是表示字符串资源为Unicode的。

比如wchar_t Str[] = L"Hello World!";    这个就是双子节存储字符了。

_T是一个适配的宏~

当#ifdef _UNICODE的时候_T就是L

没有#ifdef _UNICODE的时候_T就是ANSI的。

 

比如

LPTSTR lpStr = new TCHAR[32];
TCHAR* szBuf = _T("Hello");
以上两句使得无论是在UNICODE编译条件下都是正确编译的。

而且MS推荐你使用相匹配的字符串函数。
比如处理LPTSTR或者LPCTSTR 的时候,不要用strlen ,而是要用_tcslen

否则在UNICODE的编译条件下,strlen不能处理 wchar_t*的字符串。

T是非常有意思的一个符号(TCHAR、LPCTSTR、LPTSTR、_T()、_TEXT()...),它表示使用一种中间类型,既不明确表示使用 MBCS,也不明确表示使用 UNICODE。那到底使用哪种字符集?编译的时候才决定


在大多数情况下,CString 转换成 LPTSTR是非常容易的,如果函数要求传入LPTSTR型的参数,直接传一个

CString也行,但是在visual studio 2008中,却偶尔会出现不能转换的情况,这个为什么呢?

有人以为这是ASCII(多字节)与Unicode(宽字节)之间的问题,其实不是,要知LPTSTR这个宏是随编译器参数不同而不同的,如果在编译器——常规里面设置程序按ASCII编译,那LPTSTR就表示char*,如果选择Unicode

编译那就是wchar_t*。CString也是如此,随编译器选项的不同,可以是ASCII字符串也可以是Unicode字符串。

那么CString与LPTSTR,要么全是多字节,要么全是宽字节,不可能存在两者之间不能转换的问题。

 

例2:

1. 如何将 CString 型转换为 LPBYTE 
CString   str;   
LPBYTE   by   =   (LPBYTE)(LPCSTR)str;

 

2. LPBYTE 如何转为CString 型

CString   str;

str.Format("%s", by);


 

在vc++中有着各种字符串的表示法,如您所说。        
    首先char*   是指向ANSI字符数组的指针,其中每个字符占据8位(有效数据是除掉最高位的其他7位),这里保持了与传统的C,C++的兼容。      
 LP的含义是长指针(long   pointer)。
LPSTR是一个指向以‘’结尾的ANSI字符数组的指针,与char*可以互换使用,在win32中较多地使用LPSTR。而LPCSTR中增加的‘C’的含义是“CONSTANT”(常量),表明这种数据类型的实例不能被使用它的API函数改变,除此之外,它与LPSTR是等同的。    
    为了满足程序代码国际化的需要,业界推出了Unicode标准,它提供了一种简单和一致的表达字符串的方法,所有字符中的字节都是16位的值,其数量也可以满足差不多世界上所有书面语言字符的编码需求,开发程序时使用Unicode(类型为wchar_t)是一种被鼓励的做法。    
    LPWSTR与LPCWSTR由此产生,它们的含义类似于LPSTR与LPCSTR,只是字符数据是16位的wchar_t而不是char。       
 然后为了实现两种编码的通用,提出了TCHAR的定义:   
如果定义_UNICODE,声明如下:     typedef   wchar_t   TCHAR;    
如果没有定义_UNICODE,则声明如下:     typedef   char   TCHAR;     
LPTSTR和LPCTSTR中的含义就是每个字符是这样的TCHAR。       
CString类中的字符就是被声明为TCHAR类型的,它提供了一个封装好的类供用户方便地使用。

注意:

这两个函数是由Windows提供的转换函数,不具有通用性

C语言提供的转换函数为mbstowcs()/wcstombs()

一、函数简单介绍

涉及到的头文件:

函数所在头文件:windows.h

#include <windows.h>

wchar_t类型所需头文件:wchar.h

#include <wchar.h>

( 1 ) MultiByteToWideChar()

函数功能:该函数映射一个字符串到一个宽字符(unicode)的字符串。由该函数映射的字符串没必要是多字节字符组。 

函数原型: 

int MultiByteToWideChar(

  UINT CodePage,   DWORD dwFlags,   LPCSTR lpMultiByteStr,   int cchMultiByte,   LPWSTR lpWideCharStr,   int cchWideChar   );

参数:

1> CodePage:指定执行转换的多字节字符所使用的字符集

这个参数可以为系统已安装或有效的任何字符集所给定的值。你也可以指定其为下面的任意一值:

Value Description
CP_ACP ANSI code page
CP_MACCP Not supported
CP_OEMCP OEM code page
CP_SYMBOL Not supported
CP_THREAD_ACP Not supported
CP_UTF7 UTF-7 code page
CP_UTF8 UTF-8 code page
2> dwFlags:一组位标记,用以指出是否未转换成预作或宽字符(若组合形式存在),是否使用象形文字替代控制字符,以及如何处理无效字符。你可以指定下面是标记常量的组合,含义如下:
  MB_PRECOMPOSED:通常使用预作字符——就是说,由一个基本字符和一个非空字符组成的字符只有一个单一的字符值。这是缺省的转换选择。不能与MB_COMPOSITE值一起使用。   MB_COMPOSITE:通常使用组合字符——就是说,由一个基本字符和一个非空字符组成的字符分别有不同的字符值。不能与MB_PRECOMPOSED值一起使用。   MB_ERR_INVALID_CHARS:如果函数遇到无效的输入字符,它将运行失败,且GetLastErro返回ERROR_NO_UNICODE_TRANSLATION值。   MB_USEGLYPHCHARS:使用象形文字替代控制字符。 
组合字符由一个基础字符和一个非空字符构成,每一个都有不同的字符值。每个预作字符都有单一的字符值给基础/非空字符的组成。在字符è中,e就是基础字符,而重音符标记就是非空字符。 
标记MB_PRECOMPOSED和MB_COMPOSITE是互斥的,而标记MB_USEGLYPHCHARS和MB_ERR_INVALID_CHARS则不管其它标记如何都可以设置。 
一般不使用这些标志,故取值为0时。
3> lpMultiByteStr:指向 待转换的字符串的缓冲区。 
4> cchMultiByte:指定由参数 lpMultiByteStr指向的字符串中字节的个数。可以设置为-1,会自动判断lpMultiByteStr指定的字符串的长度
(如果字符串不是以空字符中止,设置为-1可能失败,可能成功),此参数设置为0函数将失败。 
5> lpWideCharStr:指向 接收被转换字符串的缓冲区。 
6> cchWideChar:指定由参数 lpWideCharStr指向的缓冲区的宽字节数。若此值为0,函数不会执行转换,而是返回目标缓存lpWideChatStr所需的宽字符数。
返回值:

如果函数运行成功,并且cchWideChar不为0,返回值是由lpWideCharStr指向的缓冲区中写入的宽字符数;

如果函数运行成功,并且cchMultiByte为0,返回值是待转换字符串的缓冲区所需求的宽字符数大小。(此种情况用来获取转换所需的wchar_t的个数)

如果函数运行失败,返回值为零。

若想获得更多错误信息,请调用GetLastError()函数。它可以返回下面所列错误代码:

  ERROR_INSUFFICIENT_BUFFER;     ERROR_INVALID_FLAGS;   ERROR_INVALID_PARAMETER;         ERROR_NO_UNICODE_TRANSLATION。
( 2 ) WideCharToMultiByte()

函数功能:该函数映射一个unicode字符串到一个多字节字符串。 

函数原型: 

int WideCharToMultiByte(

  UINT  CodePage,   DWORD  dwFlags,   LPCWSTR  lpWideCharStr,   int  cchWideChar,   LPSTR  lpMultiByteStr,   int  cchMultiByte,   LPCSTR  lpDefaultChar,   LPBOOL  pfUsedDefaultChar   );

参数:

与MultiByteToWideChar()函数中的参数类似,但是多了两个参数:

lpDefaultCharpfUsedDefaultChar:只有当WideCharToMultiByte函数遇到一个宽字节字符,而该字符在uCodePage参数标识的代码页中并没有它的表示法时,WideCharToMultiByte函数才使用这两个参数。(通常都取值为NULL)

1> 如果宽字节字符不能被转换,该函数便使用lpDefaultChar参数指向的字符。如果该参数是NULL(这是大多数情况下的参数值),那么该函数使用系统的默认字符。该默认字符通常是个问号。这对于文件名来说是危险的,因为问号是个通配符。

2> pfUsedDefaultChar参数指向一个布尔变量,如果Unicode字符串中至少有一个字符不能转换成等价多字节字符,那么函数就将该变量置为TRUE。如果所有字符均被成功地转换,那么该函数就将该变量置为FALSE。当函数返回以便检查宽字节字符串是否被成功地转换后,可以测试该变量。

返回值

如果函数运行成功,并且cchMultiByte不为零,返回值是由 lpMultiByteStr指向的缓冲区中写入的字节数;

如果函数运行成功,并且cchMultiByte为零,返回值是接收到待转换字符串的缓冲区所必需的字节数。(此种情况用来获取转换所需Char的个数)

如果函数运行失败,返回值为零。

若想获得更多错误信息,请调用GetLastError函数。它可以返回下面所列错误代码:

  ERROR_INSUFFICIENT_BJFFER;ERROR_INVALID_FLAGS;   ERROR_INVALID_PARAMETER;ERROR_NO_UNICODE_TRANSLATION。

二、使用方法

( 1 ) 将多字节字符串转为宽字符串:

1) 调用MultiByteToWideChar()函数,设置cchWideChar参数为0(用以获取转换所需的接收缓冲区大小);

2) 获取输入缓存的大小,作为cchMultiByte的值;(这样做是为了节省空间,也可以给cchMultiByte取值-1(字符串需要以空字符结尾,否则会出错))

3) 分配足够的内存块,用于存放转换后的Unicode字符串;

该内存块的大小由前面对cchWideChar()函数的返回值来决定;(也可以用别的方法,但该方法更节省内存)

4) 再次调用MultiByteToWideChar()函数,这次将缓存的地址作为lpWideCharStr,参数来传递,并传递第一次调用MultiByteToWideChar()函数时返回值作为cchWideChar参数的值;

5) 使用转换后的字符串;

6) 释放接收缓冲区占用的内存块;

示例代码:

[cpp]  view plain  copy
  1. void main()  
  2. {  
  3.     char sBuf[25]={0};  
  4.   
  5.     strcpy(sBuf, "我最棒");  
  6.   
  7.     //获取输入缓存大小  
  8.     int sBufSize=strlen(sBuf);  
  9.     //获取输出缓存大小  
  10.     //VC++ 默认使用ANSI,故取第一个参数为CP_ACP  
  11.     DWORD dBufSize=MultiByteToWideChar(CP_ACP, 0, sBuf, sBufSize, NULL, 0);  
  12.     printf("需要wchar_t%u个\n", dBufSize);  
  13.   
  14.     wchar_t * dBuf=new wchar_t[dBufSize];  
  15.     wmemset(dBuf, 0, dBufSize);  
  16.   
  17.     //进行转换  
  18.     int nRet=MultiByteToWideChar(CP_ACP, 0, sBuf, sBufSize, dBuf, dBufSize);  
  19.       
  20.     if(nRet<=0)  
  21.     {  
  22.         cout<<"转换失败"<<endl;  
  23.         DWORD dwErr=GetLastError();  
  24.         switch(dwErr)  
  25.         {  
  26.         case ERROR_INSUFFICIENT_BUFFER:  
  27.             printf("ERROR_INSUFFICIENT_BUFFER\n");  
  28.             break;  
  29.         case ERROR_INVALID_FLAGS:  
  30.             printf("ERROR_INVALID_FLAGS\n");  
  31.             break;  
  32.         case ERROR_INVALID_PARAMETER:  
  33.             printf("ERROR_INVALID_PARAMETER\n");  
  34.             break;  
  35.         case ERROR_NO_UNICODE_TRANSLATION:  
  36.             printf("ERROR_NO_UNICODE_TRANSLATION\n");  
  37.             break;  
  38.         }  
  39.     }  
  40.     else  
  41.     {  
  42.         cout<<"转换成功"<<endl;  
  43.         cout<<dBuf;   
  44.     }  
  45.   
  46.     delete(dBuf);  
  47. }  
注意:两次调用MultiCharToWideChar()时,形参cchMultiByte的取值需要相同,否则可能会出现接收缓存不足之类的错误,从而导致转换失败!

 ( 2 ) 从宽字节转为窄字节字符串

步骤与(1)类似,故不赘述

代码示例如下:

[cpp]  view plain  copy
  1. //从宽字符串转换窄字符串  
  2. wchar_t sBuf[25]={0};  
  3. wcscpy(sBuf, L"我最棒");  
  4.   
  5. //获取转换所需的目标缓存大小  
  6. DWORD dBufSize=WideCharToMultiByte(CP_OEMCP, 0, sBuf, -1, NULL,0,NULL, FALSE);  
  7.   
  8. //分配目标缓存  
  9. char *dBuf = new char[dBufSize];  
  10. memset(dBuf, 0, dBufSize);  
  11.   
  12. //转换  
  13. int nRet=WideCharToMultiByte(CP_OEMCP, 0, sBuf, -1, dBuf, dBufSize, NULL, FALSE);  
  14.   
  15. if(nRet<=0)  
  16. {  
  17.     printf("转换失败\n");  
  18. }  
  19. else  
  20. {  
  21.     printf("转换成功\nAfter Convert: %s\n", dBuf);  
  22. }  
  23. delete []dBuf;  

三、MultiByteToWideChar()函数乱码的问题

有的朋友可能已经发现,在标准的WinCE4.2或WinCE5.0 SDK模拟器下,这个函数都无法正常工作,其转换之后的字符全是乱码!

及时更改MultiByteToWideChar()参数也依然如此。不过这个不是代码问题,其结症在于所定制的操作系统.如果我们定制的操作系统默认语言不是中文,也会出现这种情况。

由于标准的SDK默认语言为英文,所以肯定会出现这个问题。而这个问题的解决,不能在简单地更改控制面板的"区域选项"的"默认语言",而是要在系统定制的时候,选择默认语言为"中文"。系统定制时选择默认语言的位置于:   Platform -> Setting... -> locale -> default language ,选择"中文",然后编译即可。