斯坦福编程范式第二课笔记(数据类型在内存中的表示)

内存的最小单位是字节,一个字节等于 8 位(bit),每一位要么是 0 要么是 1,也就是用二进制来表示。

一个字节在内存中的表示为:

无符号整数的表示#

无符号二进制转成十进制公式:

  • w:二进制位的长度。
  • i:二进制位从右往左开始的下标,从 0 开始计数。
  • w-1:由于 i 是从 0 开始计数,所以最后一个下标就是 w-1。
  • x(i):第 i 位的值,要么是 0 要么是 1。
  • 2^i:2 的第 i 次幂。

例如:
无符号二进制数 10010 按照公式展开就是:

如果把这个数用 1 个字节在计算机中存储,内存中就表示为:

不足 8 位,左边补 0。

1 个字节的无符号正式能表示 2^8 = 256 个不同的数。能表示最大的数是 8 个二进位全是 1 的数等于 255,也就是求一个公比为2首项是1等比数列前 8 项和。二进制位求和公式为 (2^n) - 1。总结下来一个 n 位的二进制数能表示最大的数是 (2^n) - 1,能够表示 2^n 个不同的数,之所以是 2^n 个不同的数,是因为可以表示 0~(2^n) - 1,从 0 开始的所以还需要 +1 个长度。

Char 在内存中的表示#

Char 类型是用来存储单个字符,在内存中占用 1 个字节的大小,它使用 8 个 bit 来表示 256 个字符。
Char 类型实际存储的是字符的 ASCII 码,由于 ASCII 码是整数。所以 Char 最终在内存中是一个 8bit 的整型。

比如字符 AASCII 码是 65,65 = 2^0 + 2^6,所以在内存中的表示为:

char ch = 'A';
printf("%d", ch); // output is 65

Short 在内存中的表示#

Short 表示的是短整型,一般占用 2 个字节的内存大小。

它的取值范围是 (-2)^15~(2^15)-1 包含 0。最大值这里是 (2^15)-1,是因为 short 有符号位,需要用最高位(用从左到右第一位)来表示符号,0 表示正数,1 表示负数。 最大值的二进制表示为 0111111111111111(16 个二进制位),十进制就是 (2^15)-1。 之所以是 (2^15)-1,也是之前说的求和公式 ((2^n)-1

实现加减法#

二进制加减法和十进制一样,把对应相加,大于 1 就向前进位。例如 0111 + 1 = 1000

如果想要把 7 和 - 7 相加使结果等于 0。按照在计算机中使用二进制的最高位来当做符号位的,0 表示正数,1 表示负数。那么 7 表示为 0000111,-7 就表示为 1000111 。0000111 + 1000111 按照二进制先前的加法法则得出来是 1001110,结果不是我们想要的 0。

怎么才能让 2 个二进制数相加得到 0 呢?

想要得到 0,就需要利用进位,比如在 11111111(8 个 1)的基础上加 1 就可以得到 100000000(一共 9 位,左边第一位是 1,后面 8 个 0) ,舍掉最左边的那个 1 就得到了 8 个 0 最终结果就等于 0。把原码按位取反然后与原码相加就可以得到全 1 的二进制数。比如 0000111 按位取反就是 1111000,他们俩相加得到 11111111。 再把它加 1 就得到最后的结果 0。整个过程需要 3 步,我们把最后两步合并成一个步骤,也就是把按位取反和加 1 合并到一起,其实就是把原码的反码加 1。如 1111000 加 1 得到 1111001。最后这两步合在一起叫做取原码的补码。最后得到的 1111001 就叫做 0000111 的补码。

  • 正整数的补码是其本身。
  • 负整数的补码是把它对应的正整数二进制码按位取反,也就做原码的反码然后再加 1。

比如正整数 7 的二进制码是 0000111,它的补码还是它本身。再比如 -7 对应的正整数二进制码是 0000111,它的反码就是 1111000(把原码按位取反)。然后再加1 就得到 11110011111001 就是 -7 的补码。我们再次把 11110010000111 按照二进制加法法则相加刚好得到 0。这里需要注意的是,这里左边会产生一个溢出位,这个溢出位是去掉不要的,得到结果就是 0。

-1 的补码全是 1,因为它加上 1 之后就变成了 0。

计算机系统都是用补码来表示二进制码,这样的好处之一就是可以让加减法运算统一处理。

位模式拷贝#

当把 char 类型的变量赋值给 short 类型的变量时,会把 char 的 8 个 bit 放在 short 的低八位(从右往左第一个字节)上。

例如:

char ch = 'A'; // 'A' ASCII:65 内存表示为 01000001
short s = ch; // 内存表示为 00000000 | 01000001

一个特殊的情况就是当把一个 short-1 赋值给一个 int 变量的时候,并不会得到 00000000 | 00000000 | 11111111 | 11111111,因为如果这样的话表示的值就不是 -1 了。所以正确的做法就是把所有的1 全部拷贝给 int
例如:

short s = -1; // 内存表示为 11111111 | 11111111
int i = s; // 内存表示为 11111111 | 11111111 | 11111111 | 11111111

相反如果把 short 类型的变量赋值给 char 类型的变量时,会把 short 的低八位(从右往左第一个字节)放在 char 仅有的一个字节上。会把多的字节自动剔除。
例如:

short s = 65; // 内存表示为 00000001 | 01000001
char ch = s; // 内存表示为 01000001

浮点数的表示#

我们已经知道无符号二进制转成十进制公式为:

这里的 i 是从 0 开始的也就是从右边的第一位是 2^0,如果我们从一个负整数开始的话,就会存在负整数次幂,那么也就会出现小数部分了。
例如有一个 16 位的二进制数 000000011 | 11000000 用它的前八位来表示整数部分,后八位来表示小数部分,就也可以这样表示 000000011.11000000。这样后八位也就不再是整数次幂了,而是从左到右每一位分别是 2^(-1)~2^(-8)。这个数就可以表示成:

这是其中一种浮点数表示方法,这种方法表示的浮点数会出现精度不够,表示的数值区间比较小,所以计算机实际并没有用该方法来表示浮点数。

下面这种方法就是计算机内部真实表示浮点数的方法。

我们先来看下十进制的科学计数法,用科学计数法表示 123.45 的话就是 1.2345 * 10^2。其中 1.2345 为尾数,10 为基数,2 为指数。计算机在表示浮点数的时候,也借用了十进制的科学计数法的思想,只不过基数为 2 了。

例如 1000.01 可以表示成 1.00001 * 2^3,几次幂,小数点就向右移动几位。

32位float 来举例,首位是符号位 S,紧跟后面 8位是指数位 E,最后 23位称为尾数位 M

计算公式:

  • S:符号位

    S 为 0 时刚好是正数,为 1 时是负数。

  • M:尾数部分

    它的取值范围是 1≤M<2,取值方式是从左到右每一位分别表示的是 2^-1~2^-23,值就是然后对各个位的表示值求和,这里跟先前浮点数表示的办法一致,都是从负整数次幂开始。由于尾数的整数部分始终都是 1,所以这个 1 可以被省略,这样就可以多出一位来提升精度。

  • E:指数部分

    减去 127 是因为偏移量是 127。

例如 0 | 10000010 | 11110000000000000000000 的每一部分别是:

  • S:0

    表示整数。

  • M:11110000000000000000000

    这里需要再加 1,因为为了提升小数精度省略了 1,所以要加回来。所以完整的尾数部分应该是 1.1111(省略了后面的 0)。 2^0 + 2^-1 + 2^-2 + 2^-3 + 2^-4 = 1.9375

  • E:10000010

    2^7 + 2^1 = 130

分别带入公式得:

二进制形式:

十进制形式:

详细过程:
1 * 1.1111 * 2^(130-127) => 1 * 1.1111 * 2^3 => 1 * 1111.1(几次幂,小数点就向右移动几位) => 1 * (2^3 + 2^2 + 2^1 + 2^0 + 2^-1) => 1 * (8 + 4 + 2 + 1 + 0.5) => 15.5

浮点数与整数相互赋值#

当我们在把浮点数与整数相互赋值的时候,并不会直接拷贝 bit 位,而是重新计算出在新的类型中的位模式。
例如:

int i = 5; // 内存表示 00000000 | 00000000 | 00000000 | 00000101
// 重新计算5在float中的表示方式
float f = i; // 内存表示 0 | 00000000 | 00000000000000000000101
printf("%f", f) // output is 5.0

来一点更刺激的!!!

// 2^30
int i = 1073741824; // 内存表示 01000000 | 00000000 | 00000000 | 00000000
// 这里就不会重新计算在float中的表示方式了,而是直接把bit位拷贝过去。用float的解析方式去解析int的那块内存。
float f = *(float *)&i; // 内存表示 0 | 10000000 |00000000000000000000000

// 1 * 2^(128-127) * 1 = 2
printf("%f", f) // output is 2.0

这里就不会重新计算 1073741824 在 float 中的表示方式了,而是直接把 intbit位拷贝过去。用 float 的解析方式去解析int的那块内存

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。