• 几个字符串的误区,以及setlocale函数的使用


    转自 http://www.blogjava.net/baicker/archive/2007/08/09/135642.html

    转自 http://witmax.cn/character-encoding-notes.html

             写了n年程序,近来在字符串上栽了。:( 认真的研究了一些关于字符串的文章,在此记下。许多关于字符串的问题,在文章最后的参考文章中,相信有更加深入和精确的描述。不过关于中文的处理,我想先补充一些自己的看法。

             背景:WIN32 console程序,使用printf输出字符串。相信许多人都有使用过。

             平台:VisualStudio.NET 2003(MFC 7.1)。

      MBCS UNICODE
    b2 cc 21 85
    A 41 00 41 00

    程序段1:使用std::string

    [cpp] view plain copy
     
    1. #include <string>  
    2. {  
    3.      // NOTE: use s1._Bx._Buf to see the memory  
    4.      std::string s1 ("蔡"); // b2 cc 00  
    5. }  
    6.   
    7. {  
    8.     std::wstring s1(L"蔡"); // b2 00 cc 00 00 00  
    9. }  

             以上代码,不管使用MBCS还是_UNICODE编译,得到的结果都是一样的。因为string (实际上是basic_string)不会自动进行从MBCS到UNICODE的转换。所以使用printf或者wprintf输出即可。前提当然是你的系统需要支持中文。让我们把代码修改一下,希望可以输出字符串的内容:
    [cpp] view plain copy
     
    1. {  
    2.     std::string s1 ("蔡"); // b2 cc 00      
    3.     OutputDebugStringA(s1.c_str());      
    4.     printf(s1.c_str());  
    5. }  
    6.   
    7. {  
    8.     std::wstring s1(L"蔡"); // b2 00 cc 00 00 00  
    9.     OutputDebugStringW(s1.c_str());  
    10.     wprintf(s1.c_str());  
    11. }  


             OutputDebugString 实际上就是ATLTRACE()最后调用的函数,该函数向VisualStudio的Output窗口输出,而printf和wprintf向 console窗口输出。最后的结果如何?OutputDebugStringW输出的是怪字符!!WHY?? s1.c_str()传递给OutputDebugStringW和wprintf不是都是内容相同的LPCWSTR吗?

             1)因为OutputDebugStringW的字符串必须是真正UNICODE编码的字符串,而不是所有的const wchar_t*(即LPCWSTR)都可以得到正确结果。在这里s1虽然使用wchar_t类型,但是实际的内容却是MBCS编码。

             2)反之亦然:CRT的wprintf只支持MBCS编码的字符串,而不能是UNICODE编码的字符串。在程序段2我们可以看到真正的UNICODE编码的字符串

             这是我多年来的一个误区:wchar_t类型的字符串就是UNICODE字符串。实际应该理解为UNICODE是16位的字符集,可以使用wchar_t类型进行存储。



    程序段2:使用CString 
    请看程序后面的说明。

    [cpp] view plain copy
     
    1. ////////////////// START: compile with _UNICODE ///////////////////  
    2. {  
    3.         CString s1 ("A"); // 41 00 00 00  
    4. }  
    5. {  
    6.         CString s1 (L"A"); // 41 00 00 00  
    7. }  
    8. {  
    9.         CString s1 (_T("A")); // 41 00 00 00  
    10. }  
    11. {  
    12.         CString s1 ("蔡 "); // 21 85 00 00  
    13. }  
    14. {  
    15.         CString s1 (L"蔡 "); // b2 00 cc 00 00 00  
    16. }  
    17. {  
    18.         CString s1 (_T("蔡 ")); // b2 00 cc 00 00 00  
    19. }  
    20. ////////////////// END: compile with _UNICODE ///////////////////  
    21. ////////////////// START: compile with _MBCS ///////////////////  
    22. {  
    23.        CString s1 ("A"); // 41 00  
    24. }  
    25. {  
    26.        CString s1 (L"A"); // 41 00  
    27. }  
    28. {  
    29.        CString s1 (_T("A")); // 41 00  
    30. }  
    31. {  
    32.        CString s1 ("蔡 "); // b2 cc 00  
    33. }  
    34. {  
    35.        CString s1 (L"蔡 "); // 32 a8 ac 00  
    36. }  
    37. {  
    38.        CString s1 (_T("蔡 ")); // b2 cc 00  
    39. }  
    40. ////////////////// END: compile with _MBCS ///////////////////  

             1)对于英文字母‘A’,MBCS和UNICODE的结果都是一样的

             2)Line 15.     

                      CString s1 (L" 蔡"); // b2 00 cc 00 00 00

             得到的还是MBCS的字符串,只是增加了0作为trail byte。而不是我理解的UNICODE字符串!这是我多年来的另外一个误区:_T在_UNICODE下转换为L,而L后面的字符串是UNICODE编码。在参考资料MSDN的“TCHAR.H 中的一般文本映射”中(以及MSDN的许多地方),可以看到类似的说明:

    一般文本数据类型映射

    一般文本数据类型名未定义 _UNICODE 或 _MBCS已定义
    _MBCS
    已定义 _UNICODE
    _TCHAR char char wchar_t
    _TINT int int wint_t
    _TSCHAR signed char signed char wchar_t
    _TUCHAR unsigned char unsigned char wchar_t
    _TXCHAR char unsigned char wchar_t
    _T 或 _TEXT 无效(由预处理器移除) 无效(由预处理器移除) (将后面的字符或字符串转换成相应的 Unicode 形式)

             实际上L"xxx"只是通知编译器,我们需要的是wchar_t类型的字符串,而不能影响编码。

             真正的UNICODE字符串在哪里?

             3)Line 12:

                      等同于   CStringW s1 ("蔡"); // 21 85 00 00

             我们看到,得到了真正的UNICODE 字符串。因为CString(在MFC 7.1中,不存在MFC的CString,实际上由ATL::CStringT通过typedef定义而得)的构造函数,在这里实际上是CStringW 的构造函数,根据输入的参数是char类型字符串,会自动调用MultiByteToWideChar转换MBCS字符串为UNICODE字符串。

             4)相应Line 12,那么Line 35得到的结果32 a8 ac 00是什么?和Line 12类似:

             CString构造函数,在这里实际上是CStringA的构造函数,根据输入的参数是wchar_t类型字符串,会自动调用WideCharToMultiByte转换UNICODE字符串为MBCS字符串。但是根据2),我们知道,输入的参数不是UNICODE字符串,只是MBCS的wchar_t类型字符串,所以得到的是错误的编码。

     

             总结以上,可知:

             1)CRT不能生成和处理UNICODE类型字符串,对于wchar_t类型字符串,只能处理MBCS编码;

             2)VC RunTime中带W后缀的函数,和所有的COM函数,对于wchar_t类型字符串,只能处理UNICODE编码;

             3)如果不考虑输出,只是进行拷贝、比较等操作,只要注意_T的含义和字符串的字符类型长度就可以了;但是如果需要输出,必须注意字符串的编码转换。
             4)使用UNICODE或者MBCS的编译选项,只是影响字符串的字符类型(自动识别_T,CString等,自动把函数转换为xxxxA()或者xxxxW ()),不影响字符串的编码。下表的代码结果不受编译选项影响(这也是编译器处理_T,CString等后的结果):

    CStringA s = "xxx"; // 等于 CA2A("xxx")
    结果为SBCS(单字节编码)
    printf()正确
    OutputDebugStringA()正确
    CStringW s = "xxx"; // 等于 CA2W("xxx")
    结果为UNICODE编码
    wprintf()错误
    OutputDebugStringW()正确
    CStringA s = L"xxx"; // 等于 CW2A(L"xxx")
    结果为MBCS编码(可能错误)
    printf()错误
    OutputDebugStringA()错误
    CStringW s = L"xxx"; // 等于 CW2W("xxx")
    不改变字符串的编码,仍然是MBCS。
    wprintf()正确
    OutputDebugStringW()错误

             疑问:我认为对于CW2W是由系统编码决定,可以直接得到UNICODE吗?

             关于CW2A,如果后面的字符串的确是UNICODE编码,则可以得到正确的相应MBCS编码字符串。实际上,这也是我们要输出UNICODE的方法:

             CStringW s = "蔡"; // s 现在是UNICODE编码

             // wprintf(s)不正确

             CW2A psz(s); // psz现在是s相应的正确的MBCS编码!

             printf(psz); // 正确

             // All is OK, a little more to say

             CA2W wsz(psz); // wsz现在是psz的错误的UNICODE编码,即32 a8 ac 00

     

     

    推荐参考资料

    • The Complete Guide to C++ Strings:个人认为很好和很全面的文章

            The Complete Guide to C++ Strings, Part I - Win32 Character Encodings http://www.codeproject.com/string/CPPStringGuide1.asp

            The Complete Guide to C++ Strings, Part I - Win32 Character Encodings http://www.codeproject.com/string/cppstringguide2.asp

     

    字符编码笔记:ASCII、Unicode、UTF-8、UTF-16、UCS、BOM、Endian

     
    字符编码笔记:ASCII,Unicode和UTF-8

    作者: 阮一峰 

     

    版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0 

    最后修改时间:2007年10月29日 09:46 

    今天中午,我突然想搞清楚Unicode和UTF-8之间的关系,于是就开始在网上查资料。 

    结果,这个问题比我想象的复杂,从午饭后一直看到晚上9点,才算初步搞清楚。 

    下面就是我的笔记,主要用来整理自己的思路。但是,我尽量试图写得通俗易懂,希望能对其他朋友有用。毕竟,字符编码是计算机技术的基石,想要熟练使用计算机,就必须懂得一点字符编码的知识。 

    1. ASCII码 

    我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000到11111111。 

    上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。 

    ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。 

    2、非ASCII编码 

    英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。 

    但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。 

    至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256×256=65536个符号。 

    中文编码的问题需要专文讨论,这篇笔记不涉及。这里只指出,虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的Unicode和UTF-8是毫无关系的。 

    3.Unicode 

    正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。 

    可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码。 

    Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。 

    4. Unicode的问题 

    需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。 

    比如,汉字“严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。 

    这里就有两个严重的问题,第一个问题是,如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。 

    它们造成的结果是:1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。2)unicode在很长一段时间内无法推广,直到互联网的出现。 

    5.UTF-8 

    互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。 

    UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。 

    UTF-8的编码规则很简单,只有二条: 

    1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。 

    2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。 

    下表总结了编码规则,字母x表示可用编码的位。 

    Unicode符号范围 | UTF-8编码方式
    (十六进制) | (二进制)
    ——————–+———————————————
    0000 0000-0000 007F | 0xxxxxxx
    0000 0080-0000 07FF | 110xxxxx 10xxxxxx
    0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
    0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 

    下面,还是以汉字“严”为例,演示如何实现UTF-8编码。 

    已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。 

    6. Unicode与UTF-8之间的转换 

    通过上一节的例子,可以看到“严”的Unicode码是4E25,UTF-8编码是E4B8A5,两者是不一样的。它们之间的转换可以通过程序实现。 

    在Windows平台下,有一个最简单的转化方法,就是使用内置的记事本小程序Notepad.exe。打开文件后,点击“文件”菜单中的“另存为”命令,会跳出一个对话框,在最底部有一个“编码”的下拉条。 

    bg2007102801.jpg 

    里面有四个选项:ANSI,Unicode,Unicode big endian 和 UTF-8。 

    1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对Windows简体中文版,如果是繁体中文版会采用Big5码)。 

    2)Unicode编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。这个选项用的little endian格式。 

    3)Unicode big endian编码与上一个选项相对应。我在下一节会解释little endian和big endian的涵义。 

    4)UTF-8编码,也就是上一节谈到的编码方法。 

    选择完”编码方式“后,点击”保存“按钮,文件的编码方式就立刻转换好了。 

    7. Little endian和Big endian 

    上一节已经提到,Unicode码可以采用UCS-2格式直接存储。以汉字”严“为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。 

    这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。 

    因此,第一个字节在前,就是”大头方式“(Big endian),第二个字节在前就是”小头方式“(Little endian)。 

    那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码? 

    Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。 

    如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。 

    8. 实例 

    下面,举一个实例。 

    打开”记事本“程序Notepad.exe,新建一个文本文件,内容就是一个”严“字,依次采用ANSI,Unicode,Unicode big endian 和 UTF-8编码方式保存。 

    然后,用文本编辑软件UltraEdit中的”十六进制功能“,观察该文件的内部编码方式。 

    1)ANSI:文件的编码就是两个字节“D1 CF”,这正是“严”的GB2312编码,这也暗示GB2312是采用大头方式存储的。 

    2)Unicode:编码是四个字节“FF FE 25 4E”,其中“FF FE”表明是小头方式存储,真正的编码是4E25。 

    3)Unicode big endian:编码是四个字节“FE FF 4E 25”,其中“FE FF”表明是大头方式存储。 

    4)UTF-8:编码是六个字节“EF BB BF E4 B8 A5”,前三个字节“EF BB BF”表示这是UTF-8编码,后三个“E4B8A5”就是“严”的具体编码,它的存储顺序与编码顺序是一致的。 

    9. 延伸阅读 

    The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets(关于字符集的最基本知识) 

    谈谈Unicode编码 

    RFC3629:UTF-8, a transformation format of ISO 10646(如果实现UTF-8的规定) 

    来源:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html 


     
    字符编码:Unicode/UTF-8/UTF-16/UCS/Endian/BMP/BOM

    Unicode(Universal Multiple-Octet Coded Character Set):目前最流行和最有前途的字符编码规范,因为它解决了不同语言编码的冲突。 

    Uicode由来: 

    最初的字符编码ascii(8bit,最高位为0)只能表示128个字符,表示英文、数字和一些符号是没问题。但是世界不止一种语言,即使用上了最高为1的扩展ascii码,也只有256个字符。 

    对中日韩文、阿拉伯文之类复杂的文字,就无法使用了。 

    于是,各国都制定了自己的兼容ascii编码规范,就是各种ANSI码,比如我国的gb2312,用两个扩展ascii字符来表示一个中文。但是这些ansi码无法同时存在,因为它们的定义互相重叠,要自由使用不同语言就必须有一个新编码,为各种文字统一分配编码。 

    ISO(国际标准化组织)和Uicode协会(一个软件制造商的协会)分别开始了这个工作。即ISO的ISO 10646项目和Unicode协会的Unicode项目。后来它们开始合并了双方的工作成果,采用相同的字库和字码。但目前两个项目都存在并独立地公布自己的标准。 

    UCS(Unicode Character Set): 

    这是Uicode在ISO的名称,目有两套编码方法,UCS-2(Unicode)用2个字节表示一个字符,UCS-4(Unicode-32)用4个字节表示一个字符。UCS-4是由USC-2扩展来的,增加了2字节的高位。即使是老UCS-2,它也可以表示2^16=65535个字符,基本上可以容纳所有常用各国字符,所以目前基本都使用UCS-2。 

    UTF(UCS Transformation Format): 

    Unicode使用2个字节表示一个字符,ascii使用1个字节,所以在很多方面产生了冲突,以前处理ascii的方法都必须重写。而且C语言用作为字符串结束标志,但Unicode中很多字符都含,C语言的字符串函数也无法正常处理Unicode。为了把unicode投入实用,出现了UTF,最常见的是UTF-8和UTF-16。 

    其中UTF-16和Unicode本身的编码是一致的,UTF-32和UCS-4也是相同的。最重要的是UTF-8,可以完全兼容ascii编码 。UTF是一种变长的编码,它的字节数是不固定的,使用第一个字节确定字节数。第一个字节首为0即一个字节,110即2字节,1110即3字节,字符后续字节都用10开始,这样不会混淆且单字节英文字符可仍用ASCII编码。理论上UTF-8最大可以用6字节表示一个字符,但Unicode目前没有用大于0xffff的字符,实际UTF-8最多使用了3个字节。 

    unicode转化为UTF-8的方法 

    Unicode码范围 UTF-8编码(把Unicode码转为二进制填充x处)
    0000-007F 0xxxxxxx
    0080-07FF 110xxxxx 10xxxxxx
    0800-FFFF 1110xxxx 10xxxxxx 10xxxxxx 

    汉字的Unicode编码范围是0080-07FF,因此是2字节编码。 

    Big Endian(大字节序)和Little Endian(小字节序): 

    Unicode存储时有个字节序问题,就是一个多字节数字,是从大到小排列还是反之。这和CPU处理有关,一般x86处理时都是倒置的,即大数在前。就像“莫”字的Unicode码0x83ab,按Big Endian就变成了0xab83。 

    BOM(Byte Order Mark): 

    因为Unicode存储时字节序的问题,在Unicode文本前插入一个不存在的字符(ZERO WIDTH NO-BREAK SPACE)作为标志来分辨两种字节序。标志0xfeff说明按Big Endian字节序,而0xfffe说明Little-Endian。 

    UTF-8不需要BOM来说明字节序,但可以用BOM标志编码方式。遇到带0xefbbbf开头的文本,计算机就可以不需要分辨直接按UTF-8编码处理。 

    BMP(Basic Multilingual Plane): 

    这是Unicode实际和字符对应的划分方式中的概念。 

    按UCS-4为例子 

    首字节首位恒为0,剩下7位可以划分2^7=128个group(组)。 

    第二个字节,每个group下面可以有2^8=256个plane(平面)。 

    第三个字节,可以给每个palne带来256个row(行)。 

    第四个字节,这里的8位又可以每row可以划分256个cell(格子)。 

    group 0中的plane 0就是BMP,即前两个字节为0×0000的UCS-4码。去掉0×0000的BMP上的UCS-4就变成了UCS-2编码。或者说UCS-2是USC-4的子集,BMP就是UCS-2在USC-4中的位置。我们从这里还可以得到USC-2转为UCS-4的方法,再UCS-2前面插入2个字节0×0000。 

    来源:http://blog.csdn.net/zzcv_/archive/2007/06/03/1636085.aspx 


    谈谈Unicode编码

    这是一篇程序员写给程序员的趣味读物。所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级。整理这篇文章的动机是两个问题: 

    问题一: 

    使用Windows记事本的“另存为”,可以在GBK、Unicode、Unicode big endian和UTF-8这几种编码方式间相互转换。同样是txt文件,Windows是怎样识别编码方式的呢? 

    我很早前就发现Unicode、Unicode big endian和UTF-8编码的txt文件的开头会多出几个字节,分别是FF、FE(Unicode),FE、FF(Unicode big endian),EF、BB、BF(UTF-8)。但这些标记是基于什么标准呢? 

    问题二: 

    最近在网上看到一个ConvertUTF.c,实现了UTF-32、UTF-16和UTF-8这三种编码方式的相互转换。对于Unicode(UCS2)、GBK、UTF-8这些编码方式,我原来就了解。但这个程序让我有些糊涂,想不起来UTF-16和UCS2有什么关系。 

    查了查相关资料,总算将这些问题弄清楚了,顺带也了解了一些Unicode的细节。写成一篇文章,送给有过类似疑问的朋友。本文在写作时尽量做到通俗易懂,但要求读者知道什么是字节,什么是十六进制。 

    0、big endian和little endian 

    Big endian和Little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。 

    “endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。 

    我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。 

    1、字符编码、内码,顺带介绍汉字编码 

    字符必须编码后才能被计算机处理。计算机使用的缺省编码方式就是计算机的内码。早期的计算机使用7位的ASCII编码,为了处理汉字,程序员设计了用于简体中文的GB2312和用于繁体中文的big5。 

    GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。 

    GB2312支持的汉字太少。1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区。汉字区包括21003个字符。2000年的GB18030是取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的PC平台必须支持GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持GB2312。 

    从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属于双字节字符集 (DBCS)。 

    有的中文Windows的缺省内码还是GBK,可以通过GB18030升级包升级到GB18030。不过GB18030相对GBK增加的字符,普通人是很难用到的,通常我们还是用GBK指代中文Windows内码。 

    这里还有一些细节: 

    GB2312的原文还是区位码,从区位码到内码,需要在高字节和低字节上分别加上A0。 

    在DBCS中,GB内码的存储格式始终是big endian,即高位在前。 

    GB2312的两个字节的最高位都是1。但符合这个条件的码位只有128*128=16384个。所以GBK和GB18030的低字节最高位都可能不是1。不过这不影响DBCS字符流的解析:在读取DBCS字符流时,只要遇到高位为1的字节,就可以将下两个字节作为一个双字节编码,而不用管低字节的高位是什么。 

    2、Unicode、UCS和UTF 

    前面提到从ASCII、GB2312、GBK到GB18030的编码方法是向下兼容的。而Unicode只与ASCII兼容(更准确地说,是与ISO-8859-1兼容),与GB码不兼容。例如“汉”字的Unicode编码是6C49,而GB码是BABA。 

    Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是”Universal Multiple-Octet Coded Character Set”,简称为UCS。UCS可以看作是”Unicode Character Set”的缩写。 

    根据维基百科的记载:历史上存在两个试图独立设计Unicode的组织,即国际标准化组织(ISO)和一个软件制造商的协会(unicode.org)。ISO开发了ISO 10646项目,Unicode协会开发了Unicode项目。 

    在1991年前后,双方都认识到世界不需要两个不兼容的字符集。于是它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode2.0开始,Unicode项目采用了与ISO 10646-1相同的字库和字码。 

    目前两个项目仍都存在,并独立地公布各自的标准。Unicode协会现在的最新版本是2005年的Unicode 4.1.0。ISO的最新标准是10646-3:2003。 

    UCS规定了怎么用多个字节表示各种文字。怎样传输这些编码,是由UTF(UCS Transformation Format)规范规定的,常见的UTF规范包括UTF-8、UTF-7、UTF-16。 

    IETF的RFC2781和RFC3629以RFC的一贯风格,清晰、明快又不失严谨地描述了UTF-16和UTF-8的编码方法。我总是记不得IETF是Internet Engineering Task Force的缩写。但IETF负责维护的RFC是Internet上一切规范的基础。 

    3、UCS-2、UCS-4、BMP 

    UCS有两种格式:UCS-2和UCS-4。顾名思义,UCS-2就是用两个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。下面让我们做一些简单的数学游戏: 

    UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。 

    UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个plane。每个plane根据第3个字节分为256行 (rows),每行包含256个cells。当然同一行的cells只是最后一个字节不同,其余都相同。 

    group 0的plane 0被称作Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。 

    将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。 

    4、UTF编码 

    UTF-8就是以8位为单元对UCS进行编码。从UCS-2到UTF-8的编码方式如下: 

    ╔══════════════╦═════════════════════╗
    ║UCS-2编码(16进制)    ║UTF-8 字节流(二进制)                   ║
    ║————————-║————————————–║
    ║0000 – 007F               ║0xxxxxxx                                         ║
    ║0080 – 07FF               ║110xxxxx 10xxxxxx                    ║
    ║0800 – FFFF                ║1110xxxx 10xxxxxx 10xxxxxx ║
    ╚══════════════╩═════════════════════╝ 

    例如“汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以肯定要用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 110001 001001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。 

    读者可以用记事本测试一下我们的编码是否正确。 

    UTF-16以16位为单元对UCS进行编码。对于小于0×10000的UCS码,UTF-16编码就等于UCS码对应的16位无符号整数。对于不小于0×10000的UCS码,定义了一个算法。不过由于实际使用的UCS2,或者UCS4的BMP必然小于0×10000,所以就目前而言,可以认为UTF-16和UCS-2基本相同。但UCS-2只是一个编码方案,UTF-16却要用于实际的传输,所以就不得不考虑字节序的问题。 

    5、UTF的字节序和BOM 

    UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”? 

    Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一个有点小聪明的想法: 

    在UCS编码中有一个叫做”ZERO WIDTH NO-BREAK SPACE”的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符”ZERO WIDTH NO-BREAK SPACE”。 

    这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符”ZERO WIDTH NO-BREAK SPACE”又被称作BOM。 

    UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符”ZERO WIDTH NO-BREAK SPACE”的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。 

    Windows就是使用BOM来标记文本文件的编码方式的。 

    6、进一步的参考资料 

    本文主要参考的资料是 “Short overview of ISO-IEC 10646 and Unicode” (http://www.nada.kth.se/i18n/ucs/unicode-iso10646-oview.html)。 

    我还找了两篇看上去不错的资料,不过因为我开始的疑问都找到了答案,所以就没有看: 

    “Understanding Unicode A general introduction to the Unicode Standard” (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter04a

    “Character set encoding basics Understanding character set encodings and legacy encodings” (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter03

    注:原文链接已无法打开 


    Unicode Table:http://www.ansell-uebersetzungen.com/gbuni.html
     
     
    ------------------------------------------------------------------------------------------------------------------------------------------------------------
     

    转自 http://blog.csdn.net/JsuFcz/article/details/3514568

    本文并不打算讲解Unicode的编码问题,因为本文主要对以下几个问题提一些见解:
    1. MBCS多字节码的原理?
    2. MBCS与Unicode的关系?
    3. MBCS与Unicode的转换?
    4. MBCS与Unicode的打印,乱码解决?

    早在Windows采用Unicode统一编码进行语言管理之前,Windows为了能够进行非ANSI标准字符的输出,于是采用两个字节来表示这些语言文字。因为这些双字节文字和ANSI是混和在一起的,为了加以区别,Windows将这些字符的最高位置为1(即这些双字节文字的每个字节都>=127),所以这种表示法可以表示 127x127 约一万多种非ANSI文字 ,其本上可以表示任何一种语言的常用文字了。于是,Windows为每一个区域版本,都制定了分别独立的文字编码,这就是MBCS(多字节码)。

    在采用Unicode之后,Windows仍然保留了MBCS技术,只不过它对每一种MBCS与Unicode建立了一种映射关系,当然这是通过Unicode的语言区域码实现的。windows对每个语言区域进行编号,并记录其范围。这样,只要给定这些区域编号,就可以实现任何MBCS与Unicode的转换。

    在VS编程环境下,L""表示Unicode字符(请切记:WCHAR即ushort只表示宽字符,而宽字符并不就是unicode,反而Unicode属于宽字符 ),可喜的是,VS编译器直接将L""宏编译成了Unicode编码。我们可以使用%S等进行转换,如
    setlocale(LC_ALL,"");  //这句很重要,后面会讲

    WCHAR swzMsg[] = L"Unicode测试";
    char szMsg[32] = {0};
    sprintf(szMsg, "%S", swzMsg);   //这里%S表示进行MBCS/Unicode转换
    反之,可以如下转:
    WCHAR swzMsg2[32] = {0};
    swprintf(swzMsg, L"%S", szMsg);

    我们仔细分析上面字符串的长度和编码:
    swzMsg : 55 00 6e 00 69 00 63 00 6f 00 64 00 65 00 4b 6d d5 8b 00 00
    szMsg:   55 6e 69 63 6f 64 65 b2 e2 ca d4 00
    注意到了没,swzMsg是Unicode编码的,其中文字"测试"部分是 4b 6b d5 8b,即"测" 6b4b,"试"8bd5,可以看出是很接近的一段区域了吧。
    而 szMsg其中文部分是 b2 e2 ca d4,即“测" e2b2,"试" d4ca,跟上面分析的一样吧,四个字节都大于127,e2b2 d4ca就是他们的MBCS码,
    内码是(d2-127)(b2-127) (d4-127)(ca-127)。 
    Unicode字符串用wcslen()求长度,MBCS用strlen()求长度 ,原因我想大家都很清楚。

    如果一个应用程序使用MBCS多字节编码,我们在中文环境下编译,再拿到韩文操作下去运行,会出现什么情况呢?肯定是乱码!
    原因:中文环境下编译的MBCS中文字符被编译成了MBCS内码(小于127x127的连续码),而在韩文系统下,这些码可能对应了一些韩文字符,当然也有可能什么都没有。

    其实,现代操作系统版本,如Windows内部已经用Unicode来表示各种文字编码了,它能识别任务它能表示的文字字符,
    Unicode字符 <-> 区域码起始值 + MBCS内码
    MBCS内码 = (高位MBCS外码-127)<<8 | (低位MBCS外码-127)

    操作系统内还记录了区域码CodePage,如中文是.936,它对应了一个Unicode起始值。
    任何一个Unicode字符,操作系统都可以根据区域码计算出其MBCS码。

    OK,讲到这里,
    setlocale(LC_ALL,"");
    再次登场了,它表示设置区位码,""串表示使用当前的,如果不使用本语句,
    wprintf(L"测试Unicode");
    将不能正常显示,
    sprintf(szMsg, "%S", swzMsg);
    swprintf(swzMsg, L"%S", szMsg);
    转换也会出现问题。

    关于这点,我感到很奇怪,%S在进行MBCS/Unicode字符串转换 和 wprintf()在输出Unicode字符串的时候,为什么运行时刻库不能自已默认使用
    setlocale(LC_ALL, "");
    而 L"" 又能使用默认区域码进行MBCS/Unicode转换,我想这应该是标准C时刻库和编译器版本之间的差异造成的吧。因此,
    微软的WideCharToMultiByte和MultiByteToWideChar就做得好一些,我想它应该内部给我们调用了setlocale(LC_ALL, "")语句 。
    WCHAR swzMsg[] = L"Unicode测试";
    char szMBAA[12] = {0};
    ::WideCharToMultiByte(CP_ACP, 0, swzMsg, -1, szMBAA, sizeof(szMBAA), NULL, NULL);
    printf("%s /n", szMBAA);
    WCHAR szWWAA[12] = {0};
    ::MultiByteToWideChar(CP_ACP, 0, szMBAA, -1, szWWAA, sizeof(szWWAA));

     
    ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    字符串编码与CString,string,wstring的互相转换

               首先,CString、string、wstring都是在C++编程中表示字符串的类,不同的是CString是MFC类库里的,而string和wstring是C++标准类库里的(经常写作std::string、std::wstring)。

               string和wstring的区别是什么呢?string是普通的多字节版本(基于char),而wstring是Unicode版本(基于wchar_t)。因为Unicode用2个字节来表示一个字符,因此wchar_t最少要占2个字节,事实上在Windows上就是这样(被定义为unsigned short),而Linux下默认是占4个字节,在用GCC编译时可以使用-fshort-wchar来强制使用2个字节。

               关于如何使用Unicode进行编程请参见另一篇文章《VC下的Unicode编程》,在这里着重说明一下什么是多字节编码。初学者经常会把”多字节编码”当成一种真的编码方式(如GBK、UTF-8),但其实不是的,它只是代表当前平台使用的编码方式,在Windows下是指GB2312编码,而在Linux下是UTF-8。因此,把一个Unicode编码字符串转换为多字节编码字符串,所得结果在两种平台下是全然不同的。如Unicode字符串”中文”转换为多字节编码Windows下的输出为D6 D0 CE C4,而在Linux下输出为e4 b8 ad e6 96 87。(GB2312一个汉字占2个字节,UTF-8一个汉字占3个字节)

               再来说一下CString。其实没有CString这个类,只有CStringA和CStringW,分别代表多字节版本和Unicode版本字符串。如果程序环境中定义了UNICODE宏,那么CString就会被定义为CStringW,否则就是CStringA。通常情况下,CString之间是不需要转换的。

               wstring和string互相转换
    [cpp] view plain copy
     
    1. //可移植版本 wstring => string  
    2. std::string ws2s(const std::wstring& ws)  
    3. {  
    4.     std::string curLocale = setlocale(LC_ALL, "");  
    5.     const wchar_t* _Source = ws.c_str();  
    6.     size_t _Dsize = wcstombs(NULL, _Source, 0) + 1;  
    7.     char *_Dest = new char[_Dsize];  
    8.     memset(_Dest,0,_Dsize);  
    9.     wcstombs(_Dest,_Source,_Dsize);  
    10.     std::string result = _Dest;  
    11.     delete []_Dest;  
    12.     setlocale(LC_ALL, curLocale.c_str());  
    13.     return result;  
    14. }  
    15.    
    16. //可移植版本 string => wstring  
    17. std::wstring s2ws(const std::string& s)  
    18. {  
    19.     std::string curLocale = setlocale(LC_ALL, "");   
    20.     const char* _Source = s.c_str();  
    21.     size_t _Dsize = mbstowcs(NULL, _Source, 0) + 1;  
    22.     wchar_t *_Dest = new wchar_t[_Dsize];  
    23.     wmemset(_Dest, 0, _Dsize);  
    24.     mbstowcs(_Dest,_Source,_Dsize);  
    25.     std::wstring result = _Dest;  
    26.     delete []_Dest;  
    27.     setlocale(LC_ALL, curLocale.c_str());  
    28.     return result;  
    29. }  
    30.    
    31. //MFC版本 string => wstring  
    32. std::wstring MbcsToUnicode(const std::string &str)  
    33. {  
    34.     std::wstring wstr;  
    35.     const char *p = str.c_str();  
    36.     wchar_t *wbuf = new wchar_t[str.length() * 2];      // Double length is enough?  
    37.    
    38.     memset(wbuf, NULL, str.length() * 2 * 2);  
    39.     MultiByteToWideChar(CP_ACP, NULL, p, str.length(), wbuf, str.length()*2);  
    40.    
    41.     wstr = wbuf;  
    42.     delete [] wbuf;  
    43.    
    44.     return wstr;  
    45. }  
    46.    
    47. //MFC版本 wstring => string  
    48. std::string UnicodeToMbcs(std::wstring &wstr)  
    49. {  
    50.     std::string str;  
    51.     const wchar_t *wp = wstr.c_str();  
    52.     char *buf = new char[wstr.length() * 2];  
    53.     BOOL bUsed;  
    54.    
    55.     memset(buf, NULL, wstr.length() * 2);  
    56.     WideCharToMultiByte(CP_ACP, NULL, wp, wstr.length(), buf, wstr.length()*2, NULL, &bUsed);  
    57.    
    58.     str = buf;  
    59.     delete [] buf;  
    60.    
    61.     return str;  
    62. }  


               UNICODE环境下CString[W]和wstring互相转换
    [cpp] view plain copy
     
    1. //CString 转换为 wstring  
    2. std::wstring CUtils::CStringWTowstring(CStringW &cstrw)  
    3. {  
    4.     std::wstring wstr;  
    5.    
    6.     wstr = cstrw.GetBuffer(0);  
    7.     cstrw.ReleaseBuffer();  
    8.    
    9.     return wstr;  
    10. }  
    11.    
    12. //wstring 转换为 CString  
    13. CStringW CUtils::wstringToCStringW(const std::wstring &wstr)  
    14. {  
    15.     CStringW str;  
    16.    
    17.     str = wstr.c_str();  
    18.    
    19.     return str;  
    20. }  
     

    http://blog.csdn.net/arau_sh/article/details/2971496

  • 相关阅读:
    poj3475
    poj2665
    poj2583
    poj2656
    【API进阶之路】破圈,用一个API代替10人内容团队
    除了方文山,用TA你也能帮周杰伦写歌词了
    敏捷转型谁先动:老总,项目经理or团队
    实战案例丨使用云连接CC和数据复制服务DRS实现跨区域RDS迁移和数据同步
    9块钱,构建个私有网盘,关键不限速
    终端传感了解吗?18个知识点为你扫盲
  • 原文地址:https://www.cnblogs.com/findumars/p/5869209.html
Copyright © 2020-2023  润新知