字符集历史和乱码问题(一)

96
卐鑫卍
2016.12.22 18:30* 字数 13496

网站乱码问题我们会经常碰到,大多见于非英文的中文字符或其他字符乱码,而且,这类问题常常是因为编码方式问题,主要原因就是所保存的编码方式和浏览器解析时用的编码方式不一致导致。解决办法就是要知道自己是用什么编码方式保存的,并告诉浏览器用你指定的编码方式去解析(在html<head>
里添加<meta charset="编码方式">),否则,浏览器就会用默认的编码方式解析,如果两者不一致则会乱码。

以上一段话,实际已经可以解决标题的问题了。但字符编码(常见有:ASCII、GB2312、GBK、ISOLatin-1、Unicode、UTF-8)是计算机存储技术的基础知识,我觉得还是有必要了解更多一些,比如:为什么计算机非要是二进制而不是其他进制呢?二进制是怎么产生的?计算机是怎么通过二进制存储和读取的?我们电脑上的内存和硬盘有何种区别?32位和64位操作系统指的又是什么?CPU的各种性能指标和二进制的关系?编程又和这些有何关系?等等问题,于是在开启了搜索模式后,便有了下面这个很长很长的故事:

字符编码涉及历史久远,专业术语多,又是计算机的基础,为了让自己对“字符编码”又更加深入的认识,以下内容参考和阅读了大量的文章资料,为了让大家读上去更有趣,有些内容在自己消化后稍有修改,文章太多,无法一一联系授权,侵删。(大概来自于百科、文库、知乎、博客园、CSDN、维基百科、谷歌、饥人谷讲师若愚直播课、阮一峰博客等),文内难免有理解错误之处,请多指正。

关于进位制的发现

我们都知道现在人类是离不开数学运算的,我们古人呢?他们也需要计数啊,比如我打的猎物要怎么样分配给我的爱妃们?我们现在自然很容易理解进位制的(十进制、二进制、七进制、八进制、十二进制、十六进制,二十进制、六十进制)运算,但那个时候我们的古人可不懂啊,人类在很久很久以前,对于10以上的数字是没有概念的。大家都是就着自己的手指头数数,数到10就达到了极限。后来有个基因突变的天才发明了一种做法(可能他发现了复杂的数字都是由简单的数字多次相加得来的道理),让人类的数数能力获得了质的飞升。他是这么做的:数完了自己的手指头,就在自己的身前放一颗小石子(人类的逻辑思维是需要不断进化的,当时可能还记不住)。然后让自己的手指头“清零”,继续数数。数完所有东西后,再回来数数小石子。于是,人类发明了二位数。二位数发明之后,三位数发明也不远了,后来不仅出现了十进制(手指),还出现了二十进制(手指+脚指),再后来发现了二进制、七进制、八进制、十六进制、甚至还有六十进制。十进制是我们现在最能理解和接受的,我们看到100立刻能明白它的意思,但如果看到二进制1100100肯定是懵逼的。所以,二进制虽然最简单,只需要两个基本数值0和1,但使用使用起来却很麻烦,假如你打猎收获3个猎物,就得进位,而进位的计算本来就麻烦,故而,玛雅人宁可使用二十进制数完手指再数脚指也不愿进位。再后来,古人有了月份和地支的概念后,十二进制得以使用(还有个说法是人类除大拇指外的其余四根手指都可弯曲为三节,三四得十二)。再再后来,有了十六进制,这个主要用在计算重量单位,比如十六两合一斤。二十进制据说是玛雅人在用,与使用十进制的人群如华夏人相比,玛雅人在数二十以内的猎物时不需要进位,数完手指再数脚指就可以了,这可能会产生一种思维惰性;华夏人数到十一个猎物就得进位,迫使向更复杂的进位运算前进,这就使一个种族在数学水平上先进了一步。六十进制主要源于中国十天干与十二地支的组合,也仅用于干支纪年。西方用于分割时间上的小时和分钟。七进制东西方都在使用,西方把七天作为一个周期,周而复始地循环;中国则把一个月分为四个星期,大月30天则有两周是8天,小月29天则有一周是8天,虽与西方固定的七天不同,但中国把日月五星谓之七政,也称“七曜”,算是巧合吧。这么多进制,只有十进制最为流行,可能源于这和我们身体结构相似有关,另外,八进制和十六进制最后在计算机中得到了广泛使用。这些进制里面,只有二进制最为简单,这也是计算机为何采用二进制而非其他进制的原因之一。当然,肯定不只这一个原因了,下面会有讲。

扩展阅读:古人的绳结计数法

古人用绳结计数

古埃及人图形计数法

摩尔斯码的发现

这里还有个重要的时间点需要提一下,就是1837年,也就是美国科学家莫尔斯发明的莫尔斯电码,他尝试用一些代码比如: 点、划、点和划之间的停顿、每个字符间短的停顿(在点和划之间)、每个词之间中等的停顿以及句子之间长的停顿来表示不同的字母、数字和标点符号!

为何要提它呢?它本身和二进制是有区别的,但注意时间点,我认为后面发现二进制并诞生计算机和这个存在关系,而且它除了是一种重要的远距离通讯手段外(二战期间被大量使用),更重要的是有助于我们理解字符集的问题,这个文章后面会解释到。

摩尔斯电码表

摩尔斯电码

摩尔斯电码,实际上就是符号与字母数字的对应。

电报的原理是:“点”对应于短的电脉冲信号,“划”对应于长的电脉冲信号,这些信号传到对方,接收机把短的电脉冲信号翻译成“点”,把长的电脉冲信号转换成“划”,译码员根据这些点划组合就可以译成英文字母,从而完成了通信任务。

这里每一个符号其实就是一个字符
而这所有的字符的集合就叫做字符集
“点”或“划”与字符之间的对应关系即可以理解为字符编码
把字符表示为“点”或“划”并对应为电脉冲信号的过程既是编码
译码员把接收机接收到的脉冲信号转化成点划后译成字符的过程即为解码

扩展阅读:
布莱叶盲文与二进制
电报机的原理

半导体特性的发现

  • 1833年,英国的巴拉迪发现硫化银材料的电阻是随着温度的上升而降低。
  • 1839年,法国的贝克莱尔发现半导体和电解质接触形成的结,在光照下会产生一个电压,这是被发现的半导体的第二个特征-光生伏特效应。
  • 1874年,德国的布劳恩发现某些硫化物的电导与所加电场的方向有关,即它的导电有方向性,在它两端加一个正向电压,它是导通的;如果把电压极性反过来,它就不导电,这就是半导体所特有的第三种特性-半导体的整流效应。同年,舒斯特又发现了铜与氧化铜的整流效应。
  • 1873年,英国的史密斯又发现了硒晶体材料在光照下电导增加。半导体的光电导效应被发现。

半导体特性的发现和应用使得人类从电力时代步入到了信息时代。这为计算机的产生提供了可能,为何这么说?因为半导体被发现的特性使得它能够实现电流的“开、关”功能(原理在下面),这是个很重要的发现。聪明的人类利用半导体材料发明了二极管,而且这个功能是计算机诞生的基石!

二极管的原理

上面说了,半导体可以实现电流“开、关”功能,其基本原理在于PN结,即由带正电的P型半导体(以空穴为载流子)和以带负电的N型半导体(以电子为载流子)形成的界面结。

带正电P型半导体

带负电N型半导体

扩展资料:半导体中的两种载流子-电子和空穴是如何产生和运动的?

  • 如果加P到N的正向电压,则空穴和电子都将流向该区并中和对方的离子使得该区范围不断减小直至导通。


    PN结加正向电压时导通
  • 如果加N到P的方向电压,则空穴和电子朝反方向运动,空间电荷区将不断增大,电流无法通过。
    PN结加反向电压时截止

    现在我们把一个讨厌电子的半导体(N型半导体)和一个喜欢电子的半导体(P型半导体)放在一起的时候,在它们之间就会形成一个空间电荷区,也就是电子趋向的力量(半导体毕竟不是导体,半导体的原子核对电子的束缚力是足够大的,因此虽然有趋向,但不足以让一侧的电子跑到另一侧 --- 这也就是为什么它叫做“半导体”),这就是一个二极管了。当二极管外部有电流通过的时候,如果外部电流内的电子的运动方向顺从了其内部的这种电子的趋向,则这个二极管可以视为接通的导线;而如果外部电流的电子运动方向和二极管内部的电子方向相反的时候,外部电流就无法通过这个二极管(除非加了很高的反向电压 ----- 这样就会将二极管击穿),此时这个二极管可以视为断路。所以在二极管的电路里,根据二极管两端的电压的高低的不同,就能测出某些电阻两端的电压有不同的数值:高或者低。就是说PN结就可以识别正反向电压,或者通过不同方向电压就可以控制电路的通或断,这就实现了“是”与“非”的两个状态并可以对其加以控制,来判断“0”与“1”这种状态,这样就为实现二进制的逻辑运算提供了可能,尽管电子在电路中的运动速率要远小于光速,但是建立电路关联只和电场存在与否相关,电场是以近光速运动的,这就能实现电路中的快速响应也就等效于快速运算,这也是“电脑”能比人脑快得多的原因。
    动态原理图

注意:这里的0和1只是逻辑表现方法,物理表示就是高电位和低电位,比如可以这样规定:1就表示高电压,0就表示低电压。不要把0和1理解为阿拉伯数字中的0和1

扩展阅读:
手电筒原理
继电器原理

二进制的发现

对于二进制,目前世界数学界公认的是德国数理哲学大师莱布尼茨。对,就是那个和牛顿独立发现微积分的大神。实际上,我国古人的智慧成果--八卦,就是使用阴阳两种元素来表示天地万物的。于是我搜了一下还真有资料,这里面还有个故事:

莱布尼茨在20多岁的时候就已经接触到了中国的《易经》,后于1697年发明了二进制,但由于没有强有力的证据,也没多少人理解,一直没发表。后来31岁时又接触到了古老《易经》里伏羲画的六十四卦卦符,发现和二进制的数码相对应,这给了他极大的启发和鼓舞,于1705年发表了二进制。《周易》归妹章就是讲二进制的:二进制码的每位数都是2的n次幂六三辞 “归妹以须,反归以娣,归幂以须,反归一递,归幂以须,数码位数依须要而定,够用为限,多了无益。”这句话对今天的航天仍具指导意义。航天器上的空间比同体积的黄金还贵,储存器的位数决定卫星的造价。反归一递--卦符的上爻位是2的0次幂1的位置,因易的行文顺序是由初爻(下爻,2的5次幂位)开始依次向上降幂:5、4、3、2、1、0 的顺序,故说反归一递。《易经》还记载:易有太级,是分两仪,两仪生四象,四象生八卦,八卦化万物。后来,莱布尼兹也曾预言过八卦会引起世界性的变革!(老祖宗牛逼啊)

放个两个链接:
八卦与二进制的关系
周易中的二进制数学

很可惜的是,二进制作为一种进位制的计数方法,谁发明并没有多大差别,因为它和我们的十进制一样并不是什么很高级的思想。上面讲了,古人就在用进制,它只是人们规定的一种进位方法。对于任何一种进制---X进制,就表示某一位置上的数运算时是逢X进一位。十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,每一种数制都使用位置表示法。处于不同位置的数符所代表的值不同,与它所在位的权值有关。
十进制1234.55可表示为:
1234.55=1×103+2×102+3×101+4×100+5×10(-1)+5×10(-2)。
可以看出,各种进位计数制中权的值恰好是基础的某次幂。因此,对任何一种进位计数制表示的数都可以写成按权展开的多项式,也就说明各种进制之间可以相互转化的。比如十进制中11这个数计算公式就是这样的:1×10^1 + 1×10^0 = 11。二进制1011这个数计算公式就是这样的:1011 = 1×2^3 + 0×2^2 + 1×2^1 + 1×2^0 = 8 + 0 + 2 + 1 = 11,不论是二进制1011还是十进制11,都代表同一个数字11。

二进制作为计数方法确实够简单,目前我们不可能找到比二进制数字系统更简单的数字系统了(如果把数字1去掉,就只剩下0一个数字,只有一个数字0的数字系统是什么都做不成的)。虽然二进制作为计数方法并不惊艳,但二进制数字系统架起了算术与电之间的桥梁,我们把二进制跟布尔代数,跟数字电路联系起来后,则成就了计算机二进制理论,而计算机是影响人类的重大发明。

计算机中的二进制

自18世纪莱布尼兹发现二进制(基数为2,只用0和1两个数表示,“逢二进一”,“借一当二”)后,人类开动自己的联想,产生了很多有趣的关联。比如上面我们了解的开关、电线、灯泡、继电器、二极管等物体都可以表示二进制数0和1:
电线可以表示二进制数字。有电流代表数字1;没有,则代表数字0。
开关可以表示二进制数字。开关闭合,代表数字1;开关断开,代表数字0。
灯泡可以表示二进制数字。灯泡亮,代表数字1;没亮,代表数字0。
电报继电器表示二进制数字。继电器闭合,代表数字1;继电器断开,代表数字0。
受到启发后,聪明的人类将二进制就和二极管产生了联系,再后来又脑洞大开,决定用8个可以开合的晶体管(一个开关,用“开”来表示1,“关”来表示0)来组合成不同的状态,以表达更多的信息。后来,他们看到8个开关运行状态正常后,给这8个状态起了个名字叫--“字节”。再后来,他们觉得8个状态的字节还可以组合出更多状态来,表达更多的信息,于是他们又做了一些可以处理这些“字节”的机器,在看到他们也运转正常后,觉得这一定是上帝的旨意。于是,他们给这个机器又起了一个非常牛逼的名字--“计算机”(当时一台计算机足有一间半教室那么大,六头大象的重量)。所以,我们明白了计算机中的二进制实际上就是一个非常微小的开关,用“开”来表示1,“关”来表示0,0和1各占一个“位”,用8个不同状态的0和1表示“字节”,通过“字节”组合不同的状态来存储和运算不同数据。

扩展阅读:
第一代计算机-电子管计算机
第二代计算机-晶体管计算机

计算机中为什么要用二进制而不是其他进制?

计算机使用二进制是由它的实现机理决定的。上面说过,电脑的基层部件是由集成电路组成的,这些集成电路可以看成是一个个门电路组成,(当然事实上没有这么简单的)。当计算机工作的时候,电路通电工作,于是每个输出端就有了电压。电压的高低通过模数转换即转换成了二进制:高电平是由1表示,低电平由0表示。也就是说将模拟电路转换成为数字电路。这里的高电平与低电平可以人为确定,一般地,2.5伏以下即为低电平,3.2伏以上为高电平。电子计算机能以极高速度进行信息处理和加工,包括数据处理和加工,而且有极大的信息存储能力。数据在计算机中以器件的物理状态表示,采用二进制数字系统,计算机处理所有的字符或符号也要用二进制编码来表示。用二进制的优点主要在于:

  • 技术实现简单,计算机是由逻辑电路组成,逻辑电路通常只有两个状态,开关的接通与断开,这两种状态正好可以用“1”和“0”表示。
  • 简化运算规则:两个二进制数和、积运算组合各有三种,运算规则简单,有利于简化计算机内部结构,提高运算速度。
  • 适合逻辑运算:逻辑代数是逻辑运算的理论依据,二进制只有两个数码,正好与逻辑代数中的“真”和“假”相吻合。
  • 易于进行转换:二进制与十进制数易于互相转换。
  • 抗干扰能力强:因为每位数据只有高低两个状态,当受到一定程度的干扰时,仍能可靠地分辨出它是高还是低,可靠性高。

除了以上优点外,采用二进制还有个客观原因在于,人们知道,具有两种稳定状态的元件(如晶体管的导通和截止,继电器的接通和断开,电脉冲电平的高低等)在那个时代容易找到,而要找到来对应十进制的10个数的具有10种稳定状态的元件在那个时代就非常困难了。这也是计算机为何采用二进制而非人类更熟悉的十进制的原因吧。

为什么又会出现八进制、十六进制?

人类一般思维方式是以十进制来表示的,而计算机则是二进制,但是对于编程人员来说,都是需要直接与计算器打交道的,如果给我们一大串的二进制数。比如说一个4个字节的int型的数据:0000 1010 1111 0101 1000 1111 11111 1111,我想任何程序员看到这样一大串的0、1都会很蛋疼。所以必须要有一种更加简洁灵活的方式来呈现这对数据了。你也许会说,直接用十进制吧,如果是那样,就不能准确表达计算机思维方式了(二进制),所以,出现了八进制、十六进制,其实十六进制应用的更加广泛,就比如说上面的int型的数据,直接转换为八进制的话,32./3 余2 也就是说,我们还要在前面加0,但是转换为十六进制就不同了。32/4=8,直接写成十六进制的8个数值拼接的字符串,简单明了。所以说用十六进制表达二进制字符串无疑是最佳的方式,这就是八进制和十六进制出现的原因。

扩展阅读:各进制之间如何转换

进制间的相互转换问题

  • 八进制、十六进制、二进制转为十进制
    都是按权展开的多项式相加得到十进制的结果。
    比如:
    二进制1010.1到十进制:1×2^3 + 0×2^2 + 1×2^1 + 0×2^0 + 1×2^(-1)=10.5
    八进制13.1到十进制:1×8^1 + 3×8^0 + 1×8^(-1)=11.125
    十六进制13.1到十进制:1×16^1 + 3×16^0 + 1×16^(-1)=19.0625
  • 十进制转为八进制、十六进制、二进制
    都是按照整数部分除以基数(r)取余,小数部分乘以基数(r)取整。
    十进制10.25 到二进制:整数部分除2,一步步取余。小数部分乘2,一步步取整。
    进制转化

    八进制到十进制,十六进制到十进制都是和上面的一样,只不过不在是除2乘2,而是8或者16了,这是根据自己的基数来决定的。
  • 二进制转为八进制、十六进制
    二进制转换成八进制的方法是:从小数点起,把二进制数每三位分成一组,小数点前面的不够三位的前面加0,小数点后面的不够三位的后面加0,然后写出每一组的对应的十进制数,顺序排列起来就得到所要求的八进制数了。
    依照同样的思想,将一个八进制数的每一位,按照十进制转换二进制的方法,变成用三位二进制表示的序列,然后按照顺序排列,就转换为二进制数了。
    二进制数10101111.10111转换为八进制的数:
    (010 101 111.101 110)= 2 5 7.5 6=257.56
    八进制数257.56转换为二进制的数:
    257.56 =(010 101 111.101 110)=10101111.101
    二进制转换到十六进制差不多:从小数点起,把二进制数每四位分成一组,小数点前面的不够四位的前面加0,小数点后面的不够四位的后面加0,然后写出每一组的对应的十进制数,然后将大于9的数写成如下的形式:
    10---->A,11--->B,12---->C,13---->D,14--->E,15--->F
    在顺序排列起来就得到所要求的十六进制了。
    同样,将一个十六进制数的每一位,按照十进制转换二进制的方法,变成用四位二进制表示的序列,然后按照顺序排列,就转换为二进制数了。
    二进制数10101111.10111转换为十六进制的数:
    (1010 1111.1011 1000)=A F.B 8=AF.B8
    十六进制AF.B8转换为二进制:
    AF.B8=(1010 1111.1011 1000)=10101111.10111

计算机中二进制何以表达万物?它是如何读懂二进制码的?

计算机是怎样用二进制0和1来表达信息的,甚至号称可以表达宇宙万物所有信息呢?
我们知道编程语言里的基本数据类型,都是以符合人类世界和自然世界的逻辑而出现的,便于让人更容易理解,可以说,这些数据类型是架通人类思维与计算机的桥梁。
但是我们同时也知道,依照冯诺依曼体系,计算机中并没有这些数据类型,而全部都是0和1表示的二进制数据,并且计算器只能理解这些0和1的数据。既然,所有的数据在计算机里面都是以0和1存储和运算的,那么,符合我们人类思维的数据则都要通过一定的转换才能被正确的存储到计算机中。转化的原理是什么呢?
举个栗子:假如你看上一个学妹,你想表白,所以你送了她一件礼物,如果学妹接受了礼物表示接受你,决绝了礼物表示拒绝你。也就是我们可以用0和1来表达学妹的态度(实际情况可能更复杂)。
再举个栗子:有两个人A和B去看了一个电影,怎么用0和1表示呢?我们用0代表讨厌,1代表喜欢。会出现:
00 = AB都不喜欢这影片
01 = A讨厌它,B喜欢它
10 = B喜欢它,A讨厌它
11 = AB都很喜欢这影片
如果加上我的观点呢,它就变成2^3次方=8种可能了。如:000、001、010、011、100、101、110、111,所以,计算机的二进制虽然无法和人一样理解那么多,但有了这0和1,计算机就可以通过“开”和“关”的状态来做二元逻辑判断(真假、对错、是否等非黑即白的问题)了,不要小看这二元逻辑判断,不客气的说它确实可以表达宇宙万物(道德经:道生一,一生二,二生三,三生万物)!我们不要忘记,发明二进制的莱布尼兹不但是个数学家,还是个牛逼的哲学家啊,他一直试图用数学符号和操作符来表达亚里士多德的逻辑理论,而且他也是最接近这个目标的人,直到后来乔治·布尔在概念上的突破成果--布尔代数的出现。

大部分计算机内部的连接是一样的,都是通过电气信号来进行通信的,也就是电压高低的变化,这些电流会经过 MOSFET 晶体管,晶体管中包含 N 型半导体和 P 型半导体,通过电压就能控制线路开闭,然后这些 MOSFET 构成了 CMOS,接着再由 CMOS 实现「与」「或」「非」等逻辑电路门,最后由逻辑电路门上就能实现加法、位移等计算。

配套逻辑运算:与、或、非。

  • 与:只要有0结果就是0。比如1与0等于0。
  • 或:只要有1结果就是1。比如1与0等于1。
  • 非:翻转。0变1、1变0。
    还有其他逻辑运算如同或、异或等。
    后来,有了算法,而算法中又有三种基本结构方式:顺序、选择、循环。
    而这三种结构可以涵盖所有情况,没有例外。举个栗子:
a---饿了想要吃点儿东西。
b---循坏进食的动作10次。
c---如果吃饱了,到d;否则,到b。
d---结束进食的动作。

如果这还不足以说服你,只能建议你去研究图灵机了。图灵机处理信息远比这个复杂的多。还以上面的举例:

a---饿了想要吃点儿东西。
b---循坏进食的动作10次。(该减肥了?是则挂起,否则继续)
c---如果吃饱了,到d;否则,到b。
d---结束进食的动作。

如果b反复执行到一半的时候,你发现自己是不是该考虑减肥了。如果是,这时候图灵机就会立刻优先对这个新的减肥输入进行处理而会将循环进食的动作挂起,如果不是,则唤醒刚才被挂起的动作,继续进行。也就是说我们每个人都是一个非常非常复杂的图灵机,上面只是一种情况,而人的情况指令要比这个例子复杂的多,这也是为什么人工智能总是达不到真实人的程度,当然,如果这个理论正确,假设我们也是高级文明创造的复杂版的图灵机的话,谁又能保证人工智能不会进化呢?所以,这个宇宙中的一切,都可以抽象成图灵机,二进制可以表达一切!

图灵机简图

电影-图灵

虽然,这个世界上的所有事件,都可以用顺序、选择、循环三种逻辑来表示。虽然二进制可以表达宇宙万物。但是,可以表达一切现象并不代表可以解决一切问题。
例如:显示自己本身的所有代码。
比如有这样一个程序,就一行:

显示(100)

这个程序就做一件事情:显示数字100。
问题来了,你如何把“显示(100)”这两个汉字一组括号和一个数字100显示出来呢?
似乎应该这样:

显示(显示(100))

但是其实当你这样做的时候,你已经改变了程序本身,现在的程序为两个“显示”两组括号和一个100,而你运行程序后只能显示一个“显示”一组括号和一个100,很显然你并没有把这个程序全部显示出来。

类似这样的问题,就是计算机无法解决的问题(好像类似先有蛋还是先有鸡的问题)。

再比如“背包问题-贪心算法”(一种组合优化的NP完全问题):
一个小偷去博物馆偷东西。博物馆内有3件物品。

第一件物品的重量为3, 价值为4;
第二件物品的重量为2, 价值为3;
第三件物品的重量为4, 价值为5;
小偷的背包最多可以承受的重量为7。

请问:应该如何拿物品,可以让背包里的物品总价值最大(不能重复装入)?
这个问题似乎非常简单,稍微比较后就会得出结论:拿第一件和第三件物品。

但是,如果我们扩大一下问题:如果有6件物品、背包可以承受的重量也提高一些,应该怎么拿?这个时候如果让计算机去解决这个问题,我们就会发现当问题的规模扩大1倍后,计算机所需要的时间却扩大了远不止1倍 --- 即解决问题所需时间不再是线性变化了。

就拿这个问题来说,如果物品数量超过十几个,那么计算机要想解决这个问题所需要的时间就已经超过了100年。

这类问题是计算机或者说用那三种逻辑顺序可以解决的、但是所需要的时间是人类无法容忍的。

扩展阅读:
逻辑门电路
逻辑与、或、非
认识图灵机
数学危机与图灵机
图灵机视频
如何自己造一台8位计算机

二进制和编程有什么关系?

计算机读取二进制编码,不是单纯的二进制数,而是严格按照一定的格式来编码的。读指令这些操作都是靠计算机里的CPU来进行的,按照冯诺依曼体系,CPU里有一个控制寄存器(一堆代表0和1的开关)。这样我们可以用一组二进制数来描述这个控制寄存器的状态。开关的多少就是一条指令的长度,我们经常见的32位机,就是一条指令长为32,就是这个控制有32个小开关。同样64位机有64个。在寄存器内,各个位有的是单独工作的,有的是好几个一起工作的,我们把他们分成一个个小组。有的组是表示状态的,只能读不能写,或者说写了也没用。而有的组是可以操作的。而且这些组是可以在一定条件下改变的,用来实现某个功能。对于那些可以操作的寄存器是有特定功能的,比如说有个开关你拨一下,它就伸出个腿什么的,你拨回去它就缩回来。这样我们按照上面的格式写给CPU一串二进制编码,他就能按照我们意愿完成某个动作。这个编码格式其实就跟你喊:电脑,电脑,给我个鸡腿!或者:妈,我饿了!是一样一样的。这些二进制0和1的编码动辄几十位,电脑是好认了,可是让程序员怎么读?既然上面说的格式固定,那就用一些字符来代替寄存器组吧!这样汇编语言就诞生了,当然汇编是按照机器的模式来的,还是不好理解,那C语言等等一些语言就出来了。而我们也有代码写了!程序员干的工作实际上就是,为了让计算机解决某个问题而使用某种程序设计语言编写程序(编译后的代码集合)来和计算机沟通,并最终得到相应结果的过程。程序员写好的程序会按照CPU能认识的格式来生成二进制文件,每条指令的长度固定。这样CPU每次读一点,按照读出的指令来完成相应的动作,然后再读下一条。这样就可以完成程序交给它的任务了。当然不同的CPU的控制寄存器个数不相同,但是大致原理都差不多。也就是说,程序员干的事实际就是在和计算机约会。(程序员没有女朋友难道是这个原因?)

CPU结构

内存是怎么工作的?

  • DRAM基本组成
    内存是由DRAM(动态随机存储器)芯片组成的。DRAM的内部结构可以说是PC芯片中最简单的,是由许多重复的“单元”——cell组成,每一个cell由一个电容和一个晶体管(一般是N沟道MOSFET)构成,电容可储存1bit数据量,充放电后电荷的多少(电势高低)分别对应二进制数据0和1。由于电容会有漏电现象,因此过一段时间之后电荷会丢失,导致电势不足而丢失数据,因此必须经常进行充电保持电势,这个充电的动作叫做刷新,因此动态存储器具有刷新特性,这个刷新的操作一直要持续到数据改变或者断电。而MOSFET则是控制电容充放电的开关。DRAM由于结构简单,可以做到面积很小,存储容量很大。

    内存cell结构

  • 内存地址
    内存中的cell按矩阵形排列,每一行和每一列都会有一个对应的行地址线路(正规叫法叫做word line)和列地址线路(正规叫法是bit line),每个具体的cell就挂接在这样的行地址线路和列地址线路上,对应一个唯一的行号和列号,把行号和列号组合在一起,就是内存的地址。

    SPD dump

    上图是Thaiphoon Burner的一个SPD dump,每个地址是一个字节。不过我们可以把这些数据假设成只有一个bit,当成是一个简单的内存地址表,左边竖着的是行地址,上方横着的是列地址。例如我们要找第七行、倒数第二列(地址为7E)的数据,它就只有一个对应的值:FD。当然了,在内存的cell中,它只能是0或者1。计算机会把所有的信息都给数字化,所以它知道自已把一个数据,一条命令记到了内存中的哪个(些)位置。
    充放电存储


    内存地址是内存当中存储数据的一个标识,并不是数据本身,通过内存地址可以找到内存当中存储的数据。

  • 寻址
    数据要写入内存的一个cell,或者从内存中的一个cell读取数据,首先要完成对这个cell的寻址。寻址的过程,首先是将需要操作的cell的对应行地址信号和列地址信号输入行/列地址缓冲器,然后先通过行解码器(Row Decoder)选择特定的行地址线路,以激活特定的行地址。每一条行地址线路会与多条列地址线路和cell相连接,为了侦测列地址线路上微弱的激活信号,还需要一个额外的感应放大器(Sense Amplifier)放大这个信号。当行激活之后,列地址缓冲器中的列地址信号通过列解码器(Column Decoder)确定列地址,并被对应的感应放大器通过连接IO线路,这样cell就被激活,并可供读写操作,寻址完成。从行地址激活,到找到列地址这段时间,就是tRCD。

    寻址

举例说明

内存地址

扩展阅读:
内存工作原理

计算机如何存储数据的?

数据类型一般分类基本数据类型和复合数据类型,复合数据类型也是由基本数据类型构成的。所以,这里先只讨论基本数据类型(无符号整形,带符号整形,实型,char型)的存储情况。

  • 存储无符号整形:无符号整形在数据中的存储无疑是最方便的,因为没有符号位,只表示正数,所以在存储计算方面都很简单。无符号整形在就是以纯粹的二进制串存储在计算机中的。比如说看下面的例子:

    从输出的十六进制数中可以看出,它就是以直接的二进制数表示的。
  • 存储带符号整形:对于带符号数,机器数的最高位是表示正、负号的符号位,其余位则表示数值。先不谈其他的问题,只谈二进制表达数据的问题,看下面的例子:
    假设机器字长为8的话:
    一个十进制的带符号整形1,表达为二进制就是(0000 0001)
    一个十进制的带符号整形-1,表达为二进制就是(1000 0001)
    那么,两者相加 ,用十进制运算 1+(-1)=0
    在看二进制运算(0000 0010)+(1000 0001)=(1000 0010)这个数转换为十进制结果等于-2。可以发现出问题了,这就是今天所要讲的原码。
    原码
    数值X的原码记为[x]原,如果机器字长为n(即采用n个二进制位表示数据)。则最高位是符号位。0表示正号,1表示负号,其余的n-1位表示数值的绝对值。数值0的原码表示有两种形式:[+0]原=0000 0000,-[0]原=1000 0000。
    例子:若机器字长n=8,则
    [+1]原=0000 0001 [-1]原=1000 0001
    [+127]原=0111 1111 [-127]原=1111 1111
    [+45]原=0010 1101 [-45]原=1010 1101
    可见,原码,在计算数值上出问题了,当然,你也可以实验下,原码在计算正数和正数的时候,它是一点问题都没有的,但是出现负数的时候就出现问题了。所以还需要讲一个问题:反码
    反码
    数值X的反码记作[x]反,如果机器字长为n,则最高位是符号位,0表示正号,1表示负号,正数的反码与原码相同,负数的反码则是其绝对值按位求反。数值0的反码表示有两种形式:[+0]反=0000 0000,-[0]反=1111 1111。
    例子:若机器字长n等于8,则
    [+1]反=0000 0001 [-1]反=1111 1110
    [+127]反=0111 1111 [-127]反=1000 0000
    [+45]反=0010 1101 [-45]反=1101 0010
    在看反码计算的问题:
    1+(-1)=0 |(0000 0001)反+(1111 1110)反=(1111 1111)反=(1000 0000)原=【-0】可以看到,虽然是-0,但是问题还不是很大。
    1+(-2)=-1 |(0000 0001)反+(1111 1101)反=(1111 1110)反=(1000 0001)原=【-1】 可以看到,没有问题。
    -1+(2)=1 |(1111 1110)反+(0000 0010)反=(0000 0000)反=(0000 0000)原=【0】可以看到,问题发生了,因为溢出,导致结果变为0了。
    所以,看以看到,用反码表示,问题依然没有解决,所以,还需要一个东西-补码。
    补码
    数值X的补码记作[x]补,如果机器字长为n,则最高位是符号位,0表示正号,1表示负号,正数的补码与原码反码都相同,负数的补码则等于其反码的末尾加1。数值0的补码表示有唯一的编码:[+0]补=0000 0000,-[0]补=0000 0000。
    例子:若机器字长n等于8,则
    [+1]补=0000 0001 [-1]补=1111 1111
    [+127]补=0111 1111 [-127]补=1000 0001
    [+45]补=0010 1101 [-45]补=1101 0011
    在看补码计算的问题:
    1+(-1)=0 |(0000 0001)补+(1111 1111)补=(0000 0000)补=(0000 0000)原=【0】 可以看到,没有问题。
    1+(-2)=-1 |(0000 0001)补+(1111 1110)补=(1111 1111)补=(1000 0001)原=【-1】可以看到,没有问题。
    -1+(2)=1 |(1111 1111)补+(0000 0010)补=(0000 0001)补 =(0000 0001)原=【1】 可以看到,没有问题。
    通过上面的计算,我们发现,用补码的方式,就不存在在原码和反码中存在的计算问题了。其实,这也是计算机表达带符号整数用补码的原因。补码一定是不会存在原码和反码的问题的。
    讨论下原码反码补码的原理,没兴趣的可以跳过 。
  1. 为什么原码不行?
    ( 1 )10- ( 1 )10 = ( 1 )10 + ( -1 )10 =( 0 )10
    (00000001)原+ (10000001)原 = (10000010)原 = ( -2 ) 显然不正确。
    通过上面原码计算式可以看出,当正数加上负数时,结果本应是正值,得到的却是负值(当然也有可能得到的是正数,因为被减数与减数相加数值超过0111 1111,即127,就会进位,从而进位使符号位加1变为0了,这时结果就是正的了)。而且数值部分还是被减数与减数的和。并且,当负数加上负数时(这里就拿两个数值部分加起来不超过0111 1111的来说),我们可以明显看出符号位相加变为0,进位1被溢出。结果就是正数了。因此原码的错误显而易见,是不能用在计算机中的。
  2. 补码的原理
    既然原码并不能表示负数的运算问题,那么当然要另想他法了。这个方法就是补码,不得不说,这是一个最简洁的方法,当然,也可以用别的更复杂的方法,那就不是我们想要的了。我自己研究补码的时候,也在网上找了些资料,都是到处copy,反正我是看的迷糊了,本人数学功底不怎么样,看不懂那些大神写的,只好,自己理解了下。要谈补码,先看看补数的问题。什么是补数,举个简单的例子,100=25+75。100用数学来说就是模M,那么就可以这样概括。在M=100的情况下,25是75的补数。这就是补数。25是75的补数,这是在常规世界中,在计算机上就不是这样了,因为,在计算机中,数据存在溢出的问题。假设机器字长是8的话,那么能表达的最大无符号数就是1111 1111,在加1的话,就变成1 0000 0000 ,此时因为溢出,所以1去掉,就变成0了,
    也就是说,在计算机中,补数的概念稍微不同于数学之中,25+75=100,考虑计算机中的溢出问题,那么25+75就等于0了。也就是说,25和75不是互为补数了。我觉得用闹钟来比喻这个问题再形象不过了,因为闹钟也存在着溢出的问题,当时间到达11:59 ,在加1分钟的话就变成0:0了,这和计算机的溢出是同一个道理。那么,有一个时钟,现在是0点,我想调到5点,有两种方法,一个是正着拨5,到5点。第二种方法是倒着拨7,也可以到5点。正着拨5记作+5,倒着拨7,记作-7,而闹钟的M是12,也就是说,在考虑溢出的情况下,M=12,5是-7的补数。用个数学等式可以这样表0+5=0+-7,即0+5=0-7,这就是计算机中的数值计算和数学中的计算不同的地方。
    明白了计算机中补数的道理,那么就明白补码的问题了。还是用例子说明:
    在计算机中计算十进制 1+(-2)。
    1的原码是:0000 0001
    -2的原码是:1000 0010
    -2的补码是:1111 1110 这个二进制换做无符号的整数大小就是254,而8位二进制数的M=28=256。(很多文章中把M写成27,这根本就是不对的,根本没有解决符号位的问题)你发现什么了没?当换成补码后,-2和254就是补数的关系。也就是1+(-2)等价于了1+254了。这样做,好处在什么地方,你自己都可以看得到:
    1):利用补数和溢出的原理,减法变成了加法。
    2):符号位不在是约束计算的问题,不会存在原码中的问题了,因为变成补码后,虽然最高位依然是1,但是这个1就不在是做为符号位了,而是作为一个普通的二进制位,参与运算了。
    所以,这就是补码的原理所在,通过补数和溢出,解决了减法和负数问题。
  3. 十进制数求补码,补码求十进制数
    十进制求补码:
    如果是正数,直接求它的原码,符号位为0
    如果是负数,比较好的方法是先求十六进制,在由十六进制求二进制,符号位为1,在除了符号位都取反,在加1,即可得到补码。
    补码就十进制 :
    根据符号位判断,如果符号位是0,表示是正数,就是原码,直接转换就十进制即可。
    如果符号为是1,表示是负数。那么,连符号位在内都取反,在加1,将该二进制转换为十进制,该十进制数即使该负数的绝对值,加个负号-,就得到该负数。
  • 存储带小数点的数
    这个看了很多,还是迷糊,书读的少,只是知道是通过浮点解决的,我就放两个链接吧,感兴趣的自己去研究。
    计算机内存中浮点数的表示
    小数在计算机中的存储形式

  • 存储字符:主要通过字符编码来解决,这也是我们要解决的问题。受限于篇幅,我把字符编码剩下的部分放到第二篇文章来讲了。

点击接着阅读

前端笔记