一、分清字符集与字符编码
字符集:是一个系统支持的所有抽象字符的集合,也就是一系列的字符集合。
字符编码: 是一套法则,使用该法则能够对自然语言的字符的一个字符集与计算机能识别的二进制数字进行配对。即它能在字符集合与数字系统之间建立对应关系是信息处理的一项基本技术。字符编码就是将符号转换为计算机能识别的二进制编码。
字符集(Charset Set),仅仅是一套从【字符】到【数字】的映射字典,它只规定了应该用什么数字来标识字符,仅此而已,至于计算机在存储的时候应该用什么字节来标识,字符集是根本不管这事的。Unicode, GB2312, ASCII都属于字符集。
字符集编码(Character Encoding),专门规定了字符集中的字符在计算机中应该如何存储,说白了就是怎么用字节来表示他们。UTF8, UTF16, 都属于字符集编码。
大家感受到区别了么,如果把字符集比作立了一套法律,那么字符集编码就是法律的执行方式。只立法而不去执行则立法毫无意义,字符集也是如此。只有字符集而没有规定如何编码,那对于计算机来说也毫无用处。因为在计算机看来,世间万物都是字节,而只有将人类规定的字符集【编码】成字节,计算机才能识别和存储。
二、字符集与字符编码的发展与介绍
+---->ISO8859-1
|
+---------->ANSI/OEM----+---->GB2312---->GBK---->GB18030
| |
| +---->BIG5
|
ASCII-------|
|
| +---->UTF-8
| | +---->BMP
+---------->Unicode-----+---->UTF-16--|
| +---->Planes--->Surrogate
+---->UTF-32
很久很久以前,有一群人,他们决定用8个可以开合的晶体管来组合成不同的状态,以表示世界上的万物。他们看到8个开关状态是好的,于是他们把这称为"字节"。
再后来,他们又做了一些可以处理这些字节的机器,机器开动了,可以用字节来组合出很多状态,状态开始变来变去。他们看到这样是好的,于是它们就这机器称为"计算机"。
开始计算机只在美国用。八位的字节一共可以组合出256(2的8次方)种不同的状态。
他们把其中的编号从0开始的32种状态分别规定了特殊的用途,一但终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作。遇上00x10, 终端就换行,遇上0x07, 终端就向人们嘟嘟叫,例好遇上0x1b, 打印机就打印反白的字,或者终端就用彩色显示字母。他们看到这样很好,于是就把这些0x20以下的字节状态称为"控制码"。
他们又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。大家看到这样,都感觉很好,于是大家都把这个方案叫做 ANSI 的"Ascii"编码(American Standard Code for Information Interchange,美国信息互换标准代码)。当时世界上所有的计算机都用同样的ASCII方案来保存英文文字。
由此引出我们对ASCll编码的介绍:
2.1 ASCll编码
在计算机中所有的数据都是以二进制存储的。每一个二进制位 bit 都有 0 和 1 两种状态,因此 8 个二进制位就可以组合出 2**8 ,即 256 种状态。这个八位的二进制数,被称为一个字节 byte 。
因此,一个字节总共可以表示 256 个不同的状态,每一个状态对应一个符号的话,就是 256 个符号,从 0000 0000 - 1111 1111
,对应的十六进制形式为 0x00 - 0xFF
。
二进制数据只有在特定的上下文中才有意义,否则就是一堆不可读的二进制编码。比如,在计算机的字符编码中,最简单也是最常用的就是 ASCII 编码。
ASCII 码诞生于上世纪 60 年代的美国,ASCII 码将英文字符和二进制位之间的关系做了统一规定。ASCII 编码将 128 个英文的字符映射到一个字节的后 7 位,最前面的一位统一规定为 0。
因此 ASCII 码正好使用一个字节存储一个字符,又被称为原始 8 位值,由于最高位始终为 0 ,也被称为 7 位 ASCII 编码。
在 ASCII 编码中,将数字 0 映射到 48,将大写字母 A 映射到 65,将小写字母 a 映射到 97 等等。下面就是一张 ASCII 编码表:
2.2 非ASCll编码
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
但是欧洲的语言体系有个特点:小国家特别多,每个国家可能都有自己的语言体系,语言环境十分复杂。因此即使EASCII可以表示256个字符,也不能统一欧洲的语言环境。
为了解决上面这个问题,人们想出了一个折中的方案:在EASCII中表示的256个字符中,前128字符和ASCII编码表示的字符完全一样,后128个字符每个国家或地区都有自己的编码标准。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。
根据这个规则,就形成了很多子标准:ISO-8859-1、ISO-8859-2、ISO-8859-3、……、ISO-8859-16。这些子标准适用于欧洲不同的国家地区。
ISO8859-1 字符集,也就是 Latin-1,是西欧常用字符,包括德法两国的字母。 ISO8859-2 字符集,也称为 Latin-2,收集了东欧字符。 ISO8859-3 字符集,也称为 Latin-3,收集了南欧字符。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。
不过这种各自编码的方式存在一个严重的问题,就是一个文本信息无法混合不同的语言文字。如果有一种编码,能将世界上所有的符号都纳入其中,对世界上所有的符号都赋予一个独一无二的编码,那么,是不是就可以在同一个文本信息中混合使用不同的语言文字了呢?
答案是肯定的,Unicode 就是就是这样的一种编码。Unicode 又被成为万国码,国际码,统一码,是计算机科学领域的一项业界标准。它对世界上大部分文字进行了整理、编码,使得计算机可以用简单的方式来呈现和处理各种各样的文字。
2.3 Unicode字符集
ASCII码字符集,总共才能容纳256个字符,对于全世界各国语言来说,很难全部包含在内,所有后来就出现了Unicode字符集。
Unicode字符集是一个很大的字符集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。
需要注意的是,Unicode只是一个字符集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何编码如何存储。这就造成了两个问题:
第一个问题是,如何才能区别Unicode和ASCII?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?
第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
为了解决Unicode字符集存在的问题,就出现了UTF(Unicode Transformation Formats)系列的编码规则。UTF编码规则具体规定了Unicode字符集中的字符是如何编码的。
总结:Unicode是一个很大的字符集,这个字符集只规定了这个字符集中每个字符对应的码值是多少,但是这个字符集并没有规定具体的编码规则,具体的编码规则有UTF系列的编码规则实现。
下面我们就来看看UTF系列编码的具体实现。
2.4 UTF-8编码
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种Unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是:UTF-8编码是Unicode的实现方式之一。
UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码规则,又称万国码。由Ken Thompson于1992年创建。现在已经标准化为RFC 3629。UTF-8用1到4个字节编码Unicode字符。用在网页上可以统一页面显示中文简体繁体及其它语言(如英文,日文,韩文)。
UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度(UTF-8编码可以容纳2^21个字符,总共200多万个字符)。
UTF-8的编码规则很简单,只有二条:
1.对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2.对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 | UTF-8编码方式UTF字节数 (十六进制) | (二进制)一个字节 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
单字节可编码的Unicode范围:\u0000~\u007F(0~127)
双字节可编码的Unicode范围:\u0080~\u07FF(128~2047)
三字节可编码的Unicode范围:\u0800~\uFFFF(2048~65535)
四字节可编码的Unicode范围:\u10000~\u1FFFFF(65536~2097151)
下面, 还是以汉字“严”为例,演示如何实现UTF-8编码。
已知“严”的unicode是\u4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。
2.5 UTF-16与UTF-32编码格式
UTF-32用四个字节表示代码点,这样就可以完全表示UCS-4的所有代码点,而无需像UTF-8那样使用复杂的算法。 与UTF-16类似,UTF-32也包括UTF-32、UTF-32BE、UTF-32LE三种编码,UTF-32也同样需要BOM字符。UTF-32任何一个符号都占用 4 个字节。可以想象,这会浪费多大空间,对英文而言,空间扩大了四倍,中文也扩大了两倍,所以这种编码方式也导致了 Unicode 在最初并没有被大家广泛的接受。
UTF-16由RFC2781规定,它使用两个字节来表示一个代码点。不难猜到,UTF-16是完全对应于UCS-2的,即把UCS-2规定的代码点通过Big Endian或Little Endian方式直接保存下来。UTF-16包括三种:UTF-16,UTF-16BE(Big Endian)和UTF-16LE(Little Endian)。UTF-16BE和UTF-16LE不难理解,而UTF-16就需要通过在文件开头以名为BOM(Byte Order Mark)的字符来表明文件是Big Endian还是Little Endian。BOM为U+FEFF这个字符。其实BOM是个小聪明的想法。由于UCS-2没有定义U+FEFF,因此只要出现 FF FE 或者 FE FF 这样的字节序列,就可以认为它是U+FEFF,并且可以判断出是Big Endian还是Little Endian。
UTF-16 编码相比较 UTF-32 做了一点改进,其采用 2 个字节或者 4 个字节来存储。大部分情况下 UTF-16 编码都是采用 2 个字节来存储,而当 2 个字节存储时,UTF-16 编码会将 Unicode 字符直接转成二进制进行存储,对于另外一些生僻字或者使用较少的符号,UTF-16 编码会采用 4 个字节来存储,但是采用四个字节存储时需要做一次编码转换。
UTF-16 编码的存储格式:
Unicode 编码范围(16 进制) UTF-16 编码的二进制存储格式
0x0000 0000 - 0x0000 FFFF xxxxxxxx xxxxxxxx
0x0001 0000 - 0x0010 FFFF 110110xx xxxxxxxx 110111xx xxxxxxxx
2.6 UTF-8-BOM与UTF-8
先说区别「UTF-8」和「带 BOM 的 UTF-8」的区别就是有没有 BOM。即文件开头有没有 U+FEFF,也就是说有没有这个标记。
BOM即Byte Order Mark字节序标记。BOM是为UTF-16和UTF-32准备的,用户标记字节序(byte order)。拿UTF-16来举例,其是以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流"594E",那么这是“奎”还是“乙”?
Unicode规范中推荐的标记字节顺序的方法是BOM:在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"(零宽度无间断空间)的字符,它的编码是FEFF。而FEFF在UCS中是不不能再的字符(即不可见),所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。这样如果接收者接收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称为BOM。
UTF-16是用定长2个字节来编码Unicode字符,其编码单位是两个字节,因此需要考虑字节序问题,因为2个字节哪个存高位哪个存低位需要确定。而UTF-8的编码单元是1个字节,所以就不用考虑字节序问题。
2.7 GB2312、GBK、GB18030
GB2312编码是第一个汉字编码国家标准,是由中国国家标准总局1980年发布,1981年5月1日开始实施的一套国家标准,标准号是GB2312—1980。GB2312编码适用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。GB2312编码共收录汉字6763个,其中一级汉字3755个,二级汉字3008个。同时,GB2312编码收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符。
GB2312是对ASCll码的扩展,占用两个字节。具体的编码规则这边就不介绍了,感兴趣的读者可以参考这篇博客。
GB2312能表示的汉字只有6000多个,但是中国的汉字有10万之多,所以GB2312字符集还是不够用,于是GBK出现了。GBK是对GB1212的扩展,也是占用2个字节,GBK不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。
GB18030采用变长编码,可以是1个字节、2个字节和4个字节。是对GB2312和GBK的扩展,完全兼容两者。
通过上面介绍,可以发现GBK、GB2312和GB18030字符集主要是对中文汉字的编码,同时兼顾了一些其他常用符号的编码。其中:
GB2312编码方案出现最早,占用2个字节,但是能表示的字符较少;
GBK也占用2个字节,采用了不同的编码方式,对GB2312进行了扩展,增加了近20000个新的汉字(包括繁体字)和符号;
GB18030采用变长编码,可以是1个字节、2个字节和4个字节。是对GB2312和GBK的扩展,完全兼容两者。
三、多字节字符集与Unicode字符集在工程上的使用区别
3.1 多字节字符与宽字节字符
char与wchar_t
C++的基本数据类型中表示字符的有两种:char和wchar_t。
通常char叫多字节字符,一个char占一个字节,之所以叫多字节字符是因为它在表示一个字时可能是一个字节也可能是多个字节
如:(注L是宽字符宏)
char c1 = 's'; //正确
char c2 = '好'; //错误,因为一个char不能存放一个完整的汉字信息(编译器不会报错)
char c3[3] = "好"; //正确,一般一个汉字占两个字节(文件编码中多占一个字节,即三个字节),最后一个字节存放结束符'\0'
char c4[2] = "好"; //错误,(编译器会报错,const char[3]不能初始化const char[2])
通常wchar_t被称为宽字符,一个wchar_t占用两个字节,之所以叫宽字符是因为所有的字都要用两个字节来表示,不管的英文还是中文
如:
//如果需要输出宽字符,需要设置,否则会输出一串数字
std::wcout.imbue(locale("chs")); //将wcout的本地化语言设置为中文
wchar_t w1 = L'a'; //正确
wchar_t w2 = L'好'; //正确,一个汉字用一个wchar_t表示
wchar_t w3[2] = L"好"; //正确,最后两个字节存放结束符'\0'
string与wstring
1、string是普通的多字符节版本,是基于char的,对char数组进行的一种封装
2、wstring是Unicode版本,是基于wchar_t的,对wchar_t数组进行的一种封装
3.2 工程中配置多字节与宽字节
字符集的预编译宏
1、当设置多字节字符集时,会有预编译宏_MBCS
2、当设置Unicode字符集时,会有预编译宏_UNICODE、UNICODE
3、_T和_Text宏,在使用Unicode字符集时_T和_Text会在常量字符串前面加上L,在使用多字节字符集时则会当做一般字符串处理
根据设置的字符集宏的不同,windows会对预定义的tchar数据类型进行转换
一般宏
类型 | MBCS | UNICODE |
WCHAR | wchar_t | wchar_t |
LPSTR | char* | char* |
LPCSTR | const char* | const char* |
LPWSTR | wchar_t* | wchar_t* |
LPCWSTR | const wchar_t* | const wchar_t* |
tchar数据类型宏
类型 | MBCS | UNICODE |
THCAR | char | wchar_t |
LPTSTR | TCAHR*(char*) | TCHAR*(wchar_t*) |
LPCTSTR | const TCHAR* | const TCHAR* |
多字节字符集与Unicode字符集的区别
当我们使用windows的Api时,会经常看到一个函数有两种形式:
一种结尾是W;
一种结尾是A;
如:MessageBox对应有MessageBoxW和MessageBoxA。
其实就是通过预编译宏进行区分,W是针对Unicode字符集的,而A是针对多字节字符集的。
多字节字符集
多字节字符集 (MBCS)
通常指的是ANSI、中文编码以及Shift-jis,jis,euc-jp,euc-kr等。
Unicode字符集
Unicode字符集即平常说的宽字节,包含Utf-8、Utf-16、Utf-32。
在国际交流中要经常转换字符集非常不便。因此,提出了Unicode字符集,它固定使用16 bits(两个字节、一个字)来表示一个字符,共可以表示65536个字符。将世界上几乎所有语言的常用字符收录其中,方便了信息交流。标准的Unicode称为UTF-16。
常用的字符串函数
MBCS | UNICODE | 用途 |
strlen() | wcslen() | 计算字符串中有效字符的长度 |
strcpy() | wcscpy() | 字符串复制 |
strcmp() | wcscmp() | 字符串比较 |
strcat() | wcscat() | 字符串追加 |
strchr() | wcschr() | 字符串字符搜索 |
... | ... | ... |
总结
1.一般来说使用Unicode编码,它可以适用于各个国家的语言,在软件国际化的时候会比较方便,但是由于Unicode编码中原本可以单字节表示的字符也需要两字节,在使用时会浪费很多不必要的内存,所以在对存储要求非常高的时候,使用Unicode字符集就不大行,需要使用多字节字符集。
2. 为了提高程序的可移植性,在使用TCHAR、LPTSTR、LPCTSTR等tchar数据类型时,以及使用windows的Api调用具有W和A两种版本的api时最好在字符串前加上宏_T。
_T和_Text宏
在使用Unicode字符集时_T和_Text会在常量字符串前面加上L,在使用多字节字符集时则会当做一般字符串处理
在字符串前加一个L作用: unicode字符集是两个字节组成的。L告示编译器使用两个字节的 unicode 字符集。
_T宏可以把一个引号引起来的字符串,根据你的环境设置,使得编译器会根据编译目标环境选择合适的(Unicode还是ANSI)字符处理方式,如果你定义了UNICODE,那么_T宏会把字符串前面加一个L。这时_T("ABCD")相当于"ABCD",这是宽字符串。如果没有定义,那么_T宏不会在字符串前面加那个L,_T("ABCD")就等价于ABCD