代码改变世界

[转] 字符集编码(GBK,BIG5,UNICODE)与C++的string/wstring

2011-08-07 14:09 zhenjing 阅读(...) 评论(...) 编辑 收藏

 

GBK,BIG5等字符集编码范围的具体说明 

一 预备知识

1,字符:字符是抽象的最小文本单位。它没有固定的形状(可能是一个字形),而且没有值。“A”是一个字符,“€”(德国、法国和许多其他欧洲国家通用货币的标志)也是一个字符。”“这是两个汉字字符。字符仅仅代表一个符号,没有任何实际值的意义。
2,字符集:字符集是字符的集合。例如,汉字字符是中国人最先发明的字符,在中文、日文、韩文和越南文的书写中使用。这也说明了字符和字符集之间的关系,字符组成字符集(iso8859-1GB2312/GBKunicode)。
3,代码点:字符集中的每个字符都被分配到一个代码点。每个代码点都有一个特定的唯一数值,称为标值。该标量值通常用十六进制表示。
4,代码单元: 在每种编码形式中,代码点被映射到一个或多个代码单元。代码单元是各个编码方式中的单个单元。代码单元的大小等效于特定编码方式的位数:
UTF-8 UTF-8 中的代码单元由 位组成;在 UTF-8 中,因为代码单元较小的缘故,每个代码点常常被映射到多个代码单元。代码点将被映射到一个、两个、三个或四个代码单元;
UTF-16 UTF-16 中的代码单元由 16 位组成;UTF-16 的代码单元大小是 位代码单元的两倍。所以,标量值小于 U+10000 的代码点被编码到单个代码单元中;
UTF-32UTF-32  中的代码单元由 32 位组成; UTF-32 中使用的 32 位代码单元足够大,每个代码点都可编码为单个代码单元;
GB18030GB18030  中的代码单元由 位组成;在 GB18030 中,因为代码单元较小的缘故,每个代码点常常被映射到多个代码单元。代码点将被映射到一个、两个或四个代码单元。
5,举例:
中国北京香蕉是个大笨蛋这是我定义的aka字符集;各字符对应代码点为:
北 00000001
京 00000010
香 10000001
蕉 10000010
是 10000100
个 10001000
大 10010000
笨 10100000
蛋 11000000
中 00000100
国 00001000
下面是我定义的 zixia 编码方案(8位),可以看到它的编码中表示了aka字符集的所有字符对应的 代码单元;
北 10000001
京 10000010
香 00000001
蕉 00000010
是 00000100
个 00001000
大 00010000
笨 00100000
蛋 01000000
中 10000100
国 10001000
所谓文本文件 就是我们按一定编码方式将二进制数据表示为对应的文本如 00000001000000100000010000001000000100000010000001000000这样的文件。我用一个支持 zixia编码和aka字符集的记事本打开,它就按照编码方案显示为  香蕉是个大笨蛋 
如果我把这些字符按照GBK另存一个文件,那么则肯定不是这个,而是
1100111111100011 1011110110110110 1100101011000111 1011100011110110 1011010011110011 1011000110111111 1011010110110000 110100001010

二,字符集

1, 常用字符集分类
ASCII及其扩展字符集
作用:表语英语及西欧语言。
位数:ASCII是用7位表示的,能表示128个字符;其扩展使用8位表示,表示256个字符。
范围:ASCII007F,扩展从00FF
ISO-8859-1字符集
作用:扩展ASCII,表示西欧、希腊语等。
位数:8位,
范围:从00FF,兼容ASCII字符集。
GB2312字符集
作用:国家简体中文字符集,兼容ASCII
位数:使用2个字节表示,能表示7445个符号,包括6763个汉字,几乎覆盖所有高频率汉字。
范围:高字节从A1F7, 低字节从A1FE。将高字节和低字节分别加上0XA0即可得到编码。
BIG5字符集
作用:统一繁体字编码。
位数:使用2个字节表示,表示13053个汉字。
范围:高字节从A1F9,低字节从407EA1FE
GBK字符集
作用:它是GB2312的扩展,加入对繁体字的支持,兼容GB2312
位数:使用2个字节表示,可表示21886个字符。
范围:高字节从81FE,低字节从40FE
GB18030字符集
作用:它解决了中文、日文、朝鲜语等的编码,兼容GBK
位数:它采用变字节表示(1 ASCII24字节)。可表示27484个文字。
范围:1字节从007F; 2字节高字节从81FE,低字节从407E80FE4字节第一三字节从81FE,第二四字节从3039
UCS字符集
作用:国际标准 ISO 10646 定义了通用字符集 (Universal Character Set)。它是与UNICODE同类的组织,UCS-2UNICODE兼容。
位数:它有UCS-2UCS-4两种格式,分别是2字节和4字节。
范围:目前,UCS-4只是在UCS-2前面加了0×0000
UNICODE字符集
作用:为世界650种语言进行统一编码,兼容ISO-8859-1
位数:UNICODE字符集有多个编码方式,分别是UTF-8UTF-16UTF-32
,按所表示的文字分类
语言                                 字符集                                     正式名称
英语、西欧语                     ASCIIISO-8859-1                MBCS 多字节
简体中文                             GB2312                                    MBCS 多字节
繁体中文                             BIG5                                         MBCS 多字节
简繁中文                             GBK                                         MBCS 多字节
中文、日文及朝鲜语         GB18030                                  MBCS 多字节
各国语言                             UNICODEUCS                    DBCS 宽字节

三,编码

UTF-8:采用变长字节 (1 ASCII, 2 希腊字母, 3 汉字, 4 平面符号表示,网络传输即使错了一个字节,不影响其他字节,而双字节只要一个错了,其他也错了,具体如下:
如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的字节数,其余各字节均以10开头。UTF-8最多可用到6个字节。
UTF-16:采用2字节,Unicode中不同部分的字符都同样基于现有的标准。这是为了便于转换。从 0×00000×007FASCII字符,从0×00800×00FFISO-8859-1ASCII的扩展。希腊字母表使用从0×0370到 0×03FF 的代码,斯拉夫语使用从0×04000×04FF的代码,美国使用从0×05300×058F的代码,希伯来语使用从0×05900×05FF的代 码。中国、日本和韩国的象形文字(总称为CJK)占用了从0×30000×9FFF的代码;由于0×00c语言及操作系统文件名等中有特殊意义,故很 多情况下需要UTF-8编码保存文本,去掉这个0×00。举例如下:
UTF-16: 0×0080  = 0000 0000 1000 0000
UTF-8:   0xC280 = 1100 0010 1000 0000
UTF-32:采用4字节。
优缺点
UTF-8UTF-16UTF-32都可以表示有效编码空间 (U+000000-U+10FFFF) 内的所有Unicode字符。
使用UTF-8编码时ASCII字符只占1个字节,存储效率比较高,适用于拉丁字符较多的场合以节省空间。
对于大多数非拉丁字符(如中文和日文)来说,UTF-16所需存储空间最小,每个字符只占2个字节。
Windows NT内核是UnicodeUTF-16),采用UTF-16编码在调用系统API时无需转换,处理速度也比较快。
采用UTF-16UTF-32会有Big EndianLittle Endian之分,而UTF-8则没有字节顺序问题,所以UTF-8适合传输和通信。
UTF-32采用4字节编码,一方面处理速度比较快,但另一方面也浪费了大量空间,影响传输速度,因而很少使用。

四,如何判断字符集

1,字节序
首先说一下字节序对编码的影响,字节序分为Big Endian字节序和Little Endian字节序。不同的处理器可能不一样。所以,传输时需要告诉处理器当时的编码字节序。对于前者而言,高位字节存在低地址,低字节存于高地址;后者相反。例如,0X03AB,
Big Endian字节序
0000: 0 3
0001: AB
Little Endian字节序是
0000: AB
0001: 0 3
2,编码识别
UNICODE,根据前几个字节可以判断UNICODE字符集的各种编码,叫做Byte Order Mask方法BOM
UTF-8: EFBBBF (符合UTF-8格式,请看上面。但没有含义在UCSUNICODE)
UTF-16 Big EndianFEFF (没有含义在UCS-2)
UTF-16 Little EndianFFFE (没有含义在UCS-2)
UTF-32 Big Endian0000FEFF (没有含义在UCS-4)
UTF-32 Little EndianFFFE0000 (没有含义在UCS-4)
GB2312:高字节和低字节的第1位都是1
BIG5GBK&GB18030:高字节的第1位为1。操作系统有默认的编码,常为GBK,可以下载别的并升级。通过判断高字节的第1位从而知道是ASCII或者汉字编码。

摘自: http://blog.minidx.com/2008/12/06/1689.html 

字符编码笔记:ASCIIUnicodeUTF-8

 摘自:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html  
今天中午,我突然想搞清楚UnicodeUTF-8之间的关系,于是就开始在网上查资料。
结果,这个问题比我想象的复杂,从午饭后一直看到晚上9点,才算初步搞清楚。
下面就是我的笔记,主要用来整理自己的思路。但是,我尽量试图写得通俗易懂,希望能对其他朋友有用。毕竟,字符编码是计算机技术的基石,想要熟练使用计算机,就必须懂得一点字符编码的知识。

1. ASCII

我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有01两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从000000011111111
上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。
ASCII码一共规定了128个字符的编码,比如空格“SPACE”32(二进制00100000),大写的字母A65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0

2、非ASCII编码

英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。
中文编码的问题需要专文讨论,这篇笔记不涉及。这里只指出,虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的UnicodeUTF-8是毫无关系的。

3.Unicode

正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。
可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码。
Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母AinU+0041表示英语的大写字母AU+4E25表示汉字。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表

4. Unicode的问题

需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
这里就有两个严重的问题,第一个问题是,如何才能区别unicodeascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
它们造成的结果是:1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode2unicode在很长一段时间内无法推广,直到互联网的出现。

5.UTF-8

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8Unicode的实现方式之一。
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编码。
已知unicode4E25100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5

6. UnicodeUTF-8之间的转换

通过上一节的例子,可以看到Unicode码是4E25UTF-8编码是E4B8A5,两者是不一样的。它们之间的转换可以通过程序实现。
Windows平台下,有一个最简单的转化方法,就是使用内置的记事本小程序Notepad.exe。打开文件后,点击文件菜单中的另存为命令,会跳出一个对话框,在最底部有一个编码的下拉条。
里面有四个选项:ANSIUnicodeUnicode big endian 和 UTF-8
1ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对Windows简体中文版,如果是繁体中文版会采用Big5码)。
2Unicode编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。这个选项用的little endian格式。
3Unicode big endian编码与上一个选项相对应。我在下一节会解释little endianbig endian的涵义。
4UTF-8编码,也就是上一节谈到的编码方法。
选择完编码方式后,点击保存按钮,文件的编码方式就立刻转换好了。

7. Little endianBig 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表示。这正好是两个字节,而且FFFE1
如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

8. 实例

下面,举一个实例。
打开记事本程序Notepad.exe,新建一个文本文件,内容就是一个字,依次采用ANSIUnicodeUnicode big endian 和 UTF-8编码方式保存。
然后,用文本编辑软件UltraEdit十六进制功能,观察该文件的内部编码方式。
1ANSI:文件的编码就是两个字节“D1 CF”,这正是GB2312编码,这也暗示GB2312是采用大头方式存储的。
2Unicode:编码是四个字节“FF FE 25 4E”,其中“FF FE”表明是小头方式存储,真正的编码是4E25
3Unicode big endian:编码是四个字节“FE FF 4E 25”,其中“FE FF”表明是大头方式存储。
4UTF-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编码
RFC3629UTF-8, a transformation format of ISO 10646(如果实现UTF-8的规定)
(完)

C++的中英文字符串表示(string,wstring)

(从前面的资料可知,string完全可以存储中文(有效编码只有'\0'=0,其他字符均不为0),但是在显示、字符操作等方面是无法保证的!
      在C++中字符串类的string的模板原型是basic_string
template <class _Elem, class traits = char_traits<_Elem>, class _Ax = allocator<_Elem>>
class basic_string{};
      第一个参数_Elem表示类型。第二个参数traits的缺省值使用char_traits类型,定义了类型和字符操作的函数,如比较、等价、分配等。第三个参数_Ax的默认值是allocator类,表示了内存模式,不同的内存结构将操作指针的不同行为,例如栈、堆或段内存模式等。
     在C++标准里定义了两个字符串stringwstring
typedef basic_string<charstring;
typedef basic_string<wchar_t> wstring;
      前者string是常用类型,可以看作char[],其实这正是与string定义中的_Elem=char相一致。而wstring,使用的是wchar_t类型,这是宽字符,用于满足非ASCII字符的要求,例如Unicode编码,中文,日文,韩文什么的。对于wchar_t类型,实际上C++中都用与char函数相对应的wchar_t的函数,因为他们都是从同一个模板类似于上面的方式定义的。因此也有wcout, wcin, werr等函数。
     实际上string也可以使用中文,但是它将一个汉字写在2char中。而如果将一个汉字看作一个单位wchar_t的话,那么在wstring中就只占用一个单元,其它的非英文文字和编码也是如此。这样才真正的满足字符串操作的要求,尤其是国际化等工作。
     看一下下面的程序,就会理解两者的差别。
#include <iostream>
#include <string>
using namespace std;

#define tab "\t"

int main()
{
    locale def;
    cout<<def.name()<<endl;
    locale current = cout.getloc();
    cout<<current.name()<<endl;

    float val=1234.56;
    cout<<val<<endl;

    //chage to french/france
    cout.imbue(locale("chs"));
    current=cout.getloc();
    cout<<current.name()<<endl;
    cout<<val<<endl;

    //上面是说明locale的用法,下面才是本例的内容,因为其中用到了imbue函数
    cout<<"*********************************"<<endl;

    //为了保证本地化输出(文字/时间/货币等),chs表示中国,wcout必须使用本地化解析编码
    wcout.imbue(std::locale("chs"));

    //string 英文,正确颠倒位置,显示第二个字符正确
    string str1("ABCabc");
    string str11(str1.rbegin(),str1.rend());
    cout<<"UK\ts1\t:"<<str1<<tab<<str1[1]<<tab<<str11<<endl;

    //wstring 英文,正确颠倒位置,显示第二个字符正确
    wstring str2=L"ABCabc";
    wstring str22(str2.rbegin(),str2.rend());
    wcout<<"UK\tws4\t:"<<str2<<tab<<str2[1]<<tab<<str22<<endl;

    //string 中文,颠倒后,变成乱码,第二个字符读取也错误
    string str3("你好么?");
    string str33(str3.rbegin(),str3.rend());
    cout<<"CHN\ts3\t:"<<str3<<tab<<str3[1]<<tab<<str33<<endl;

    //正确的打印第二个字符的方法
    cout<<"CHN\ts3\t:RIGHT\t"<<str3[2]<<str3[3]<<endl;

    //中文,正确颠倒位置,显示第二个字符正确
    wstring str4=L"你好么?";
    wstring str44(str4.rbegin(),str4.rend());
    wcout<<"CHN\tws4\t:"<<str4<<tab<<str4[1]<<tab<<str44<<endl;

    wstring str5(str1.begin(),str1.end());//只有char类型的string时才可以如此构造
    wstring str55(str5.rbegin(),str5.rend());
    wcout<<"CHN\tws5\t:"<<str5<<tab<<str5[1]<<tab<<str55<<endl;

    wstring str6(str3.begin(),str3.end());//如此构造将失败!!!!
    wstring str66(str6.rbegin(),str6.rend());
    wcout<<"CHN\tws6\t:"<<str6<<tab<<str6[1]<<tab<<str66<<endl;

    return 0;
}

 结果如下:(略)

     上面显示了本地化的作用,是在数字中每三位加一个逗号,其实对时间/文字等都是用影响的。
     下面的输出说明了,如何正确使用stringwstring的方法。第三个因为使用string来表示汉字,出现了一些错误。最后一行也是错误,导致了输出也受到了影响,没有空格与回车。(最后两个就不要管中英文了,仅仅说明一下中文构造方法是错误的)
     《掌握标准C++类》在第十二章《语言支持》中专门讲C++的国际化和本地化问题,C++提供了I18N的标准处理,软件开发者可以参考。
       C++标准库还是非常博大精深的,功能比较齐全的。继续学习。
摘自: http://www.cnblogs.com/xiaoyz/archive/2008/10/11/1308860.html