You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

98 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 11 | 二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫”?
上算法和数据结构课的时候,老师们都会和你说,程序 = 算法 + 数据结构。如果对应到组成原理或者说硬件层面,算法就是我们前面讲的各种计算机指令,数据结构就对应我们接下来要讲的二进制数据。
众所周知现代计算机都是用0和1组成的二进制来表示所有的信息。前面几讲的程序指令用到的机器码也是使用二进制表示的我们存储在内存里面的字符串、整数、浮点数也都是用二进制表示的。万事万物在计算机里都是0和1所以呢搞清楚各种数据在二进制层面是怎么表示的是我们必备的一课。
大部分教科书都会详细地从整数的二进制表示讲起,相信你在各种地方都能看到对应的材料,所以我就不再啰啰嗦嗦地讲这个了,只会快速地浏览一遍整数的二进制表示。
然后呢我们重点来看一看大家在实际应用中最常遇到的问题也就是文本字符串是怎么表示成二进制的特别是我们会遇到的乱码究竟是怎么回事儿。我们平时在开发的时候所说的Unicode和UTF-8之间有什么关系。理解了这些相信以后遇到任何乱码问题你都能手到擒来了。
## 理解二进制的“逢二进一”
二进制和我们平时用的十进制其实并没有什么本质区别只是平时我们是“逢十进一”这里变成了“逢二进一”而已。每一位相比于十进制下的09这十个数字我们只能用0和1这两个数字。
任何一个十进制的整数都能通过二进制表示出来。把一个二进制数对应到十进制非常简单就是把从右到左的第N位乘上一个2的N次方然后加起来就变成了一个十进制数。当然既然二进制是一个面向程序员的“语言”这个从右到左的位置自然是从0开始的。
比如0011这个二进制数对应的十进制表示就是$0×2^3+0×2^2+1×2^1+1×2^0$
$=3$代表十进制的3。
对应地,如果我们想要把一个十进制的数,转化成二进制,使用**短除法**就可以了。也就是把十进制数除以2的余数作为最右边的一位。然后用商继续除以2把对应的余数紧靠着刚才余数的右侧这样递归迭代直到商为0就可以了。
比如我们想把13这个十进制数用短除法转化成二进制需要经历以下几个步骤
![](https://static001.geekbang.org/resource/image/a2/d8/a2b6f2a92bcf99e9f96367bbb90383d8.jpg)
因此对应的二进制数就是1101。
刚才我们举的例子都是正数对于负数来说情况也是一样的吗我们可以把一个数最左侧的一位当成是对应的正负号比如0为正数1为负数这样来进行标记。
这样一个4位的二进制数 0011就表示为+3。而1011最左侧的第一位是1所以它就表示-3。这个其实就是整数的**原码表示法**。原码表示法有一个很直观的缺点就是0可以用两个不同的编码来表示1000代表0 0000也代表0。习惯万事一一对应的程序员看到这种情况必然会被“逼死”。
于是我们就有了另一种表示方法。我们仍然通过最左侧第一位的0和1来判断这个数的正负。但是我们不再把这一位当成单独的符号位在剩下几位计算出的十进制前加上正负号而是在计算整个二进制值的时候在左侧最高位前面加个负号。
比如一个4位的二进制补码数值1011转换成十进制就是$-1×2^3+0×2^2+1×2^1+1×2^0$
$=-5$。如果最高位是1这个数必然是负数最高位是0必然是正数。并且只有0000表示01000在这样的情况下表示-8。一个4位的二进制数可以表示从-8到7这16个整数不会白白浪费一位。
当然更重要的一点是,用补码来表示负数,使得我们的整数相加变得很容易,不需要做任何特殊处理,只是把它当成普通的二进制相加,就能得到正确的结果。
我们简单一点拿一个4位的整数来算一下比如 -5 + 1 = -4-5 + 6 = 1。我们各自把它们转换成二进制来看一看。如果它们和无符号的二进制整数的加法用的是同样的计算方式这也就意味着它们是同样的电路。
![](https://static001.geekbang.org/resource/image/bf/ae/bf4cfd001308da2be317b08d1f40a7ae.jpg)
## 字符串的表示,从编码到数字
不仅数值可以用二进制表示,字符乃至更多的信息都能用二进制表示。最典型的例子就是**字符串**Character String。最早计算机只需要使用英文字符加上数字和一些特殊符号然后用8位的二进制就能表示我们日常需要的所有字符了这个就是我们常常说的**ASCII码**American Standard Code for Information Interchange美国信息交换标准代码
![](https://static001.geekbang.org/resource/image/be/05/bee81480de3f6e7181cb7bb5f55cc805.png)
[图片来源](https://en.wikipedia.org/wiki/ASCII)
ASCII码就好比一个字典用8位二进制中的128个不同的数映射到128个不同的字符里。比如小写字母a在ASCII里面就是第97个也就是二进制的0110 0001对应的十六进制表示就是 61。而大写字母 A就是第65个也就是二进制的0100 0001对应的十六进制表示就是41。
在ASCII码里面数字9不再像整数表示法里一样用0000 1001来表示而是用0011 1001 来表示。字符串15也不是用0000 1111 这8位来表示而是变成两个字符1和5连续放在一起也就是 0011 0001 和 0011 0101需要用两个8位来表示。
我们可以看到最大的32位整数就是2147483647。如果用整数表示法只需要32位就能表示了。但是如果用字符串来表示一共有10个字符每个字符用8位的话需要整整80位。比起整数表示法要多占很多空间。
这也是为什么很多时候我们在存储数据的时候要采用二进制序列化这样的方式而不是简单地把数据通过CSV或者JSON这样的文本格式存储来进行序列化。**不管是整数也好,浮点数也好,采用二进制序列化会比存储文本省下不少空间。**
ASCII码只表示了128个字符一开始倒也堪用毕竟计算机是在美国发明的。然而随着越来越多的不同国家的人都用上了计算机想要表示譬如中文这样的文字128个字符显然是不太够用的。于是计算机工程师们开始各显神通给自己国家的语言创建了对应的**字符集**Charset和**字符编码**Character Encoding
字符集表示的可以是字符的一个集合。比如“中文”就是一个字符集不过这样描述一个字符集并不准确。想要更精确一点我们可以说“第一版《新华字典》里面出现的所有汉字”这是一个字符集。这样我们才能明确知道一个字符在不在这个集合里面。比如我们日常说的Unicode其实就是一个字符集包含了150种语言的14万个不同的字符。
而字符编码则是对于字符集里的这些字符怎么一一用二进制表示出来的一个字典。我们上面说的Unicode就可以用UTF-8、UTF-16乃至UTF-32来进行编码存储成二进制。所以有了Unicode其实我们可以用不止UTF-8一种编码形式我们也可以自己发明一套 GT-32 编码比如就叫作Geek Time 32好了。只要别人知道这套编码规则就可以正常传输、显示这段代码。
![](https://static001.geekbang.org/resource/image/99/3e/9911c58d79e8a1f106d48a83457d193e.jpg)
同样的文本,采用不同的编码存储下来。如果另外一个程序,用一种不同的编码方式来进行解码和展示,就会出现乱码。这就好像两个军队用密语通信,如果用错了密码本,那看到的消息就会不知所云。在中文世界里,最典型的就是“手持两把锟斤拷,口中疾呼烫烫烫”的典故。
我曾经听说过这么一个笑话没有经验的同学在看到程序输出“烫烫烫”的时候以为是程序让CPU过热发出报警于是尝试给CPU降频来解决问题。
既然今天要彻底搞清楚编码知识,我们就来弄清楚“锟斤拷”和“烫烫烫”的来龙去脉。
![](https://static001.geekbang.org/resource/image/5c/fd/5c6e03705f50c250ccb5300849c281fd.png)
搜索了一下我自己的个人邮件历史记录,不出意外,里面出现了各种“锟斤拷”
首先“锟斤拷”的来源是这样的。如果我们想要用Unicode编码记录一些文本特别是一些遗留的老字符集内的文本但是这些字符在Unicode中可能并不存在。于是Unicode会统一把这些字符记录为U+FFFD这个编码。如果用UTF-8的格式存储下来就是\\xef\\xbf\\xbd。如果连续两个这样的字符放在一起\\xef\\xbf\\xbd\\xef\\xbf\\xbd这个时候如果程序把这个字符用GB2312的方式进行decode就会变成“锟斤拷”。这就好比我们用GB2312这本密码本去解密别人用UTF-8加密的信息自然没办法读出有用的信息。
而“烫烫烫”则是因为如果你用了Visual Studio的调试器默认使用MBCS字符集。“烫”在里面是由0xCCCC来表示的而0xCC又恰好是未初始化的内存的赋值。于是在读到没有赋值的内存地址或者变量的时候电脑就开始大叫“烫烫烫”了。
了解了这些原理,相信你未来在遇到中文的编码问题的时候,可以做到“手中有粮,心中不慌”了。
## 总结延伸
到这里,相信你发现,我们可以用二进制编码的方式,表示任意的信息。只要建立起字符集和字符编码,并且得到大家的认同,我们就可以在计算机里面表示这样的信息了。所以说,如果你有心,要发明一门自己的克林贡语并不是什么难事。
不过光是明白怎么把数值和字符在逻辑层面用二进制表示是不够的。我们在计算机组成里面关心的不只是数值和字符的逻辑表示更要弄明白在硬件层面这些数值和我们一直提的晶体管和电路有什么关系。下一讲我就会为你揭开神秘的面纱。我会从时钟和D触发器讲起最终让你明白计算机里的加法是如何通过电路来实现的。
## 推荐阅读
关于二进制和编码,我推荐你读一读《编码:隐匿在计算机软硬件背后的语言》。从电报机到计算机,这本书讲述了很多计算设备的历史故事,当然,也包含了二进制及其背后对应的电路原理。
## 课后思考
你肯定会计算十进制整数的加减法,二进制的加减法也是一样的。如果二进制的加法中,有数是负数的时候该怎么处理呢?我们今天讲了补码的表示形式,如果这个负数是原码表示的,又应该如何处理?如果是补码表示的呢?请你用二进制加法试着算一算,-5+4=-1通过原码和补码是如何进行的
欢迎你在留言区写下你的思考和疑问,和大家一起探讨。你也可以把今天的文章分享给你朋友,和他一起学习和进步。