信息存储
电路只有开关,分别以通电与不通电表示开关,由于这种特性,使得计算机能很简单的表示二进制,所以现在计算机及相关存储介质都是用二进制存储数据。一切的信息都会被翻译为二进制在计算机中存储并运算,简单的说,计算机就是一个存储器加计算器,先存储程序再交由运算器计算得出结果
一个二进制被称为一 位 , 一字节等于八位 ,单个的位并不具有太大意义,当足够多的位则能表示足够多的可能性,就具有了意义。字节是最小的可寻址的存储器单位,即计算机不能访问到位。
机器级程序将存储器视为一个很大的字节数组,通过访问数组下标来访问到每个字节,该下标就是地址,所有可能的地址集合,就称为 虚拟地址空间。这个虚拟地址空间只是一个概念性的映射,具体还是要靠随机访问存储器等等来实现映射
编译器和运行时系统的一个任务,就是将这个存储器单元分为更可管理的单元,来存放不同的程序对象
对于一个很长的位,计算机能够通过机械运算理解它的意义,但是人类却很难理解,需要一种计算机能够理解的位和人类能够读懂的语言之间的转换,这就是编码。编码就像古时密码本,编译使用编码来做对照翻译,将人类能够读懂的语言翻译为二进制语言,不同的编译技巧会带来不同的效果,我们下面会看到这些神奇的技巧。
字
每台计算机都有一个字长,指明整数和指针数据的标称大小。因为虚拟地址是以一字表示的,所以系统参数的字长,就代表了虚拟地址空间的范围。即,对于一个字长 n 的机器,地址表示的范围则是 0 ~ 2^n-1 ,则程序最多访问到 2^n 字节。例如:32位计算机的虚拟地址范围是 0 ~ 2^32,即,最多能访问到 2^32 字节(因为每个地址都必须是不同的),约为 4g
数据大小
计算机和编译器使用不同的方式编码数字,从而支持多种数据格式。
以 c 语言为例,语言的类型应该具有跨平台性,但是 c 的 long、int、*char 类型在 32 位机器是用 4 字节表示,在 64 位机器 long、*char 使用 8 字节表示,在原来的 32 位机器,使用 int 可以存储一个指针类型,但是这放在 64 位机器就会出错,而在 64 位平台上编译的代码更不能在 32 位上执行
寻址和字节顺序
对于跨越多字节对象的存储,一般都存储位连接的字节序列,并使用字节序列中的最小地址作为对象的地址,比如一个 int 类型的地址位 &001,它的字节序列位 &001、&002、&003、&004。对于字节序列的表示,有两种方法,将最低有效字节放在前称为小端法,Intel的机器大多采用,将最高有效字节放在前称为大端发,IBM机器大多采用
以十六进制 0x01234567 为例,小端法 67 45 32 01、大端法 01 23 45 67,我们书面语法就是大端法
为了避免在不同机器上产生错误,在网络上传输二进制时,必须按照网络规定转换为网络标准,机器从网络接受后再转化为机器标准
整数表示
在整数的编码中,常有两种形式,无符号整数和有符号整数,他们的编码方式不同,运算的属性也不同
无符号
无符号最简单粗暴,直接对数进行编码,空的地方补 0
有符号
有符号数有多种编码方式,这里介绍最常用而且神奇的补码方式
二进制数 | 十进制数 |
---|---|
00000000 | 0 |
00000001 | 1 |
… | … |
01111111 | 127 |
10000000 | -128 |
10000001 | -127 |
… | … |
11111111 | -1 |
从本质上讲,计算机只能做加法运算,所有的减法和乘除都是通过加法运算间接模拟出来的,以这种编码方式,便于通过加法模拟出减法,例如:1 - 127 编码: 00000001 + 10000001 = 10000002 解码:-126,只需要一次加法就完成了减法运算
可以看出,从正数得到负数的方法,求其补码 + 1
注意:在一些语言中,有符号数强制转换为无符号数时,是直接将有符号数的二进制码以无符号数的方式解码,如果原来的数是正数,数值不会改变,如果是负数,则会得到一个特别大的正数
var a int = -1
var b int = -2
fmt.Println(uint(a)) //18446744073709551615
fmt.Println(uint(b)) //18446744073709551614
扩展一个数字的位表示
不同长度的数据之间可能会进行转换,将大类型转换位小类型不具有太大意义,容易造成数据丢失,而将小类型转为大类型,又分为两种。无符号数据在最高为补0即可,而补码表示的有符号数据在最高为补原来最高为的值,这样就能维持值不变
隐式类型转换总是会给你意想不到的惊喜,所以选择 go 吧,绝对没有任何隐式转换,防止你的程序被低级而隐蔽的错误入侵
整数运算
无符号数加法
两个太大的无符号数相加可能反而会得到一个比他们小的数,这是因为最高为溢出了,例如:11111111 + 00000001 = 00000000,因为字长的限制,他们反而会得到个 0
补码的加法
两个太大的正数相加可能会得到一个负数,而两太小的负数相加也可能会得到一个正数,例如:01111111 + 00000001 = 10000000 解码得 -128
整数乘法
在计算机内部,n*m 实质是将 n 做 m 次的累加,所以乘法是很耗费 cpu 的,即使你的编译器会帮助你做乘法的优化,但是不要过多相信编译器,能用位移运算或加法,就不要用乘法。最关键的是,小心溢出
使用位移运算,向左移动一位(丢弃最高为,最低位补0),等同于 *2,*2^n 等同于左移 n 位
二进制小数
在计算机中并不能精确的表示所有浮点数,而是得到近似值,例如 101.11 如果以二进制表示,则得到 1*2^2 + 0*2^1 + 1*2^0 + 1*2^-1 + 1*2^-2,除以 2 相当于将小数点向后移动一位。
并不是所有的数都能以 2 的次方相加表示,这时我们只能得到他的近似值,提高小数点后的位数可以提高精度
我们从将十进制数转为二进制数来展开浮点数的细节
-235.125 转为二进制 -100100101001.001 = -1.00100101001001×2^11
-1.00100101001001×2^11 分为三个部分,符号位 - 或 +,尾数 1.00100101001001,阶码 11,在 IEEE 标准中规定了单精度和双精度的长度
类型 | 长度 | 符号 | 阶码 | 尾数 | 指数偏移 |
---|---|---|---|---|---|
单精度 float | 32 | 31~30 | 30~23 | 22~0 | 127 |
双精度 double | 64 | 63~62 | 62~52 | 51~0 | 1023 |
V = (-1)^符号 * 尾数 * 2^阶码