转载:从程序员的角度看ASCII, GB2312, UNICODE, UTF-8

以下内容转自博客:http://blog.chinaunix.net/uid-22670933-id-1771613.html

一、字符编码是怎么回事

0. 概念

字节是计算机的最基本存储单位,一个字节包括8个位.

字符是一种文字的基本单位,比如'A' 是一个字符,'汉' 也是一个字符.

1. 计算机被发明之后,程序员们编写了很多复杂的计算让计算机运行.

但是一个问题是,计算机如何把辛苦计算的结果告知程序员? 假设计算机把计算结果放在某个寄存器,内容是 1010010

总不能让程序员去检测每个引脚的电位吧? 还是得有个显示器.

显示器是依靠点阵来显示图像的. CPU必须告诉显示器,当CPU 把一个字节的数据比如 00101010 放入显示器寄存器的时候,显示器要显示怎样的一个点阵(图像),这个图像就是我们人类可以看得懂的字符. 这样问题就解决了,比如当CPU把00110001放入显示器寄存器时,显示器就显示(控制点阵画出一个图像-字体)字符 "1", 这是一个查表的过程,内存中的值(内码)和字符是一一对应的. 问题是这个对应关系是可以自由确定的,我可以指定显示器把 00110001(内码) 显示为字符 "1",也可以指定显示为字符"2". 这样当然会引起混乱,同一个内码被映射为不同的字符,不利于人们的交流.

美国国家标准学会(ANSI)决定着手解决这个问题, 英语有一个很小的字符集,26个字母再加上一些控制字符和标点符号,7位2进制值就足以表示所有的变化.于是ANSI公布了一个标准的对应关系,以字节为单位. 当内码为 0110001 时,大家都公认它代表字符 "1",在所有显示器都显示为同一个字符. 这样大家就可以按照同一个标准相互交换数据而不会引起误解. 这个表就是一个包含了128项的对应关系, 叫做 "ASCII", 美国信息交换标准代码.

2.对于中国这样不使用ABC字符的国家来说,如何显示自己的文字是一个大问题.

我们可以制定一个内码表,指定一个内码对应一个汉字. (由于中文的字符非常多,所以一个字节是不够的,至少也要有2个字节存储一个内码.) 这是很容易的,只要国家公布一个标准的内码字符对应表,大家都遵照这个表就可以了.但是还是有一些问题要注意:

(1). 即使在中国,计算机还是得能显示英文吧?

而英文的内码已经有 ASCII 标准在先,并且已经有无数的程序已经在这个标准上运行了很多年,成为不可或缺的部分. 所以我们新制定的内码表必须和 ASCII 兼容.

(2) 很多C语言的库函数是以内码0作为字符串结束标志的,为了兼容那些以前就已经编写好,并且运行良好的程序,我们指定的内码中不能含有值为0的字节.

巧合的是,所有的ASCII内码的最高位都为0. 那么我们只要让第一个和第二个字节(一个汉字占用2个字节)的最高位都为1,这样既和ASCII内码区分开来,又不会出现0.符合这个规则的内码(2字节长)理论上一共可以标识 127 * 127 = 16129个字符(实际上只用了6000多个码位,保留了一些,不过也已经够用了,常用的中文字符只有4000多个).

我们国家公布的这个内码标准表就是GB2312. 

原有的英文软件可以很好的运行,C的库函数也不用做修改, 比如 strlen("ABC") 在GB2312表示的内码中, 由于GB2312对英文字符的编码是和ASCII完全一样的,所以返回

3.对于 strlen("A汉字"), 由于strlen()是以内码为0作为边界的,而所有中文字符的GB2312内码高位都为1,不会出现0,并且每个汉字占用2个字节,所以 strlen 返回5. 对于程序来说只要检查一个字节的最高位,就可以很容易的判断这个字符是中文还是英文字符,非常方便.

"一个字母一个字节,一个汉字2个字节" 的观念深入人心.

有了GB2312之后,汉字显示/存储/交换就基本上没什么问题了.

几乎所有的非英语国家都制定了和GB2312类似兼容ASCII的内码字符对应表.

(BIG5 由于有几个字符的内码和ASCII相同但表示不同的字符,不符合2.(1)条件.所以被认为是有"瑕疵"的.) 

3. 很明显,GB2312的码位是不够的, 一个例子就是有很多人的人名电脑里打不出来.(只有6000多个码位,而<<康熙字典>>就收录了4万多个汉字).所以后来有出现了诸如GBK, GB18030以及同期流行于台湾香港的BIG5编码. 虽然编码有些不同, 但是设计思想是一致的: 兼容ASCII,并确保不会有某个字节值为0的内码出现.有一个共同的特点是: 它们都是局部的标准,只流行于某个地区/国家内.

4.由于内码表都是各个国家独自制定的,同一个内码,在不同的国家表示的可能是不同的字符.(除了ASCII字符, ASCII字符在所有国家指定的内码表中都有同样的值.)不利于国家间的信息交换. 于是 UNICODE 应运而生.

UNICODE 采用一种很简单的办法来解决这个问题. 就是采用2个 - UCS-2 (或者4个字节 - UCS-4)字节标识一个字符. 2个字节总共可以表示65535个字符,足够表示世界上的所有语言的所有字符.(汉字不就有4万多个吗,65535怎么够. 我估计只是常用的汉字几千个被编在UCS-2中吧. 目前被正式编码到UNICODE码位的只有不超过65534个, 所以就目前的情况来说,用2个字节是可以的.) 注意 UCS-4, UCS-2 和 ASCII是向下兼容的,只要前面补0就可以了.这点很重要,可以一直扩展下去包含全宇宙的字符.

现在地球上每个字符在所有采用UNICODE字符编码的计算机内都有一个唯一的内码了.

要注意, 除了ASCII字符外,其他国家文字的字符的内码是重新分配过的,不一定和各国原有的编码相同.比如大部分汉字的GB2312内码和UNICODE 内码都是不同的.

5. 很显然,对于英语国家来说,UNICODE内码非常浪费空间,对于UCS-2 浪费了50%的存储空间,对于 UCS-4 则浪费了70%的存储空间. 而且还有一个更大的问题, UNICODE的内码中含有很多 '\0', 原有的C标准库函数没办法处理这些字符串.于是有人发明了一种针对UNICODE的变换规则,把UNICODE字符串中的0去除. 注意这个变换规则不是通过查表实现的,而只要用一些位移操作就可以实现. 这就是UTF8.

总结:

UTF8 只是 UNICODE内码在存储/传输时的状态. 而从GB2312编码转换到UNICODE编码需要查表. UTF8 和 UNICODE 的关系 与 GB2312 和 UNICODE的关系有本质的不同. UTF8 和 UNICODE 是一个人的两个面孔, GB2312 和 UNICODE 是两个人.

所以,要实现UTF8编码到GB2312编码的转换必须先把 UTF8编码还原为UNICODE编码,再通过查表的方式,把UNICODE编码转化为GB2312编码.

以上,虽然说得不是很严谨(比如GB2312其实是区位码,真正的内码还要给每个字节加上A0, 这些我都没提,免得分散注意力),但是文字编码的原理大致就是那么回事,理解就好了. 要想详细了解细节Google一下能找到很多资料.

二、字符编码的编程相关问题

1. Windows从NT开始,内核使用UNICODE内码. 为了向前兼容,前端使用的还是GB2312内码(中文环境). 

所以用 Visual Studio 编写代码时, 如果在CPP文件中写这样一句 const char* pszText = "中文", 编译器让 pszText 指向"中文"的GB2312内码值的内存空间. 当调用 printf(pszText)时, WINAPI 把这个GB2312字符串转化为UNICODE字符串再输出.(WIndows自然知道你的编码是GB2312,因为你在Windows系统中设置的语言区域是中国, CodePage 936. 如果改成其它语言,就会显示为乱码.)

微软非常鼓励Windows程序员用Unicode编写程序,很明显,由于Windows内核就是原生的Unicode环境,调用API时,省却了编码转换的操作,效率更高. 而且一个额外的好处就是不会有乱码. 注意,MS的C/C++编译器把sizeof(wchar_t)设置为2个字节. 由于目前所有的UNICODE字符只有65534个码位(BMP),所以用2个字节是没问题的.

2. Linux系统(比如Ubuntu)现在一般都用UTF8编码了.

我们在Linux下创建CPP文件并添加同样的: const char* pszText = "中文" 编译器会让 pszText 指向"中文"UTF8的内码值的内存空间.Linux的终端可以理解为一个只接收UTF8字符串的显示器. 任何被写到终端的字符流都被认为是是一个UTF8字符流.所以,编程的时候,从外部(文件或者控制台)读入UTF8字符流,转换为wchar_t,然后程序在内部使用宽字符处理,最后再把要输出的宽字符流转换为UTF8字符流并输出到控制台/文件中. 用户程序可以通过环境变量LANG的值得知当前的系统环境所使用的字符编码.由此可见,C库函数的 mbstowcs()/wcstombs()主要是为应付这种情况设计的. 如果要处理XML, HTML 等等有明确指明字符编码的字符流,用专门的字符转换库更为方便.

为什么很多Windows下的C源文件的注释在Linux编辑器下会显示为乱码就很好理解了.

3. 字符编码转换相关的函数和库

Windows 的字符转换函数: MultiByteToWideChar() / WideCharToMultiByte()

Linux 的字符转换库: GLIBC iconv函数组.

C标准库使用的 mbstowcs()和wcstombs()和 locale 相关,用起来很不方便,而且功能有限.

(注意不要假设 wchar_t 的大小, 它可能是4字节也可能是2字节,取决于编译器. 比如 MS VC9.0 (2008) 里, sizeof(wchar_t) = 2, 而在GCC中, sizeof(wchar_t) = 4.)

4. 给定一个ANSI兼容的字符串(包括GB2312,GBK,UTF8等),无法确定它的编码类型,只能猜测.所以不要指望会有一个万能的转换函数.

5. BOM (Byte Order Mark)UNICODE: FF FE / FE FF 和 UTF8: EF BB BF 是不完全靠谱的,仅供参考.

最后说明一点,对于不是专门处理字符编码的程序来说,所有字符编码相关的问题只是显示的问题,并不会影响到程序的内在逻辑.

开始用 Unicode 来编写我们的代码吧.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏华仔的技术笔记

编程规范之《招聘一个靠谱的iOS》

3287
来自专栏数据之美

Java 多线程之 Runnable VS Thread 及其资源共享问题

对于 Java 多线程编程中的 implements Runnable 与 extends Thread,部分同学可能会比较疑惑,它们之间究竟有啥区别和联系呢?...

2386
来自专栏耕耘实录

漫谈正则表达式

版权声明:本文为耕耘实录原创文章,各大自媒体平台同步更新。欢迎转载,转载请注明出处,谢谢

1164
来自专栏海天一树

小朋友学Python(6):中文编码

编码一直是让新手头疼的问题,特别是 GBK、GB2312、UTF-8 这三个比较常见的网页编码的区别,更是让许多新手晕头转向。但是编码又是那么重要,特别在网页这...

33710
来自专栏Java3y

《阿里巴巴 Java开发手册》读后感

前一阵子一直在学Redis,结果在黄金段位被虐了,暂时升不了段位了,每天都拿不到首胜(好烦)。

1483
来自专栏CRPER折腾记

一篇不大靠谱的常用正则表达式汇总(前端)

我没有那么多奇奇怪怪的正则,这些都是工作中很常见的, 所有正则都是经过真实环境下的考验,不是假想推断的正则....

963
来自专栏lulianqi

UNICODE,GBK,UTF-8

UNICODE,GBK,UTF-8     简单来说,unicode,gbk和大五码就是编码的值,而utf-8,uft-16之类就是这个值的表现形式.而前面那三...

3432
来自专栏Jimoer

Java设计模式之GoF设计模式概述

最近要开始学习设计模式了,以前是偶尔会看看设计模式的书或是在网上翻到了某种设计模式,就顺便看看,也没有仔细的学习过。前段时间看完了JVM的知识,然后就想着JVM...

1043
来自专栏chafezhou

Python 工匠:使用数字与字符串的技巧

数字是几乎所有编程语言里最基本的数据类型,它是我们通过代码连接现实世界的基础。在 Python 里有三种数值类型:整型(int)、浮点型(float)和复数(c...

921
来自专栏怀英的自我修炼

Java漫谈11

这次我们接着聊String,这次我们聊聊String类为什么是final的。 之所以聊这个,是因为在知乎上看了一篇帖子,看完后让我对这个点有了认识,在这里跟你分...

36811

扫码关注云+社区

领取腾讯云代金券