天才般的设计:整数在内存中是如何存储的

c语言中的整数

整数是编程中常用的一种数据类型,C 语言使用 int 定义整数。

int 一般占用 4 个字节「Byte」,一共 32 bit。如果不考虑符号位:

  • 00000000 00000000 00000000 00000000 : 最小值,数字 0
  • 11111111 11111111 11111111 11111111 : 最大值, 2 32 − 1 2^{32}-1 2321

使用 4 个字节保存较小的整数绰绰有余,甚至会空闲出 2-3 个字节来,这些字节就白白浪费掉了。反过来, 2 32 − 1 2^{32}-1 2321 虽然很大,但要表示全球人口还是不够,必须要让整数占用更多的内存,才能表示更大的值,比如 6 个字节或 8 个字节。

现在个人电脑内存都比较大了,浪费一些内存也不会带来明显的损失;而在 C 语言诞生早期,或者在单片机和嵌入式系统中,内存都是非常稀缺的资源

让整数占用更少的内存可以使用 short,让整数占用更多的内存可以使用 long

short a = 10;

long n = 7000000000;

这样,a 只占用 2 个字节内存,而 m 可能占用 8 个字节内存

整型的长度

一种数据类型占用的字节数,称为该数据类型的长度

例如:short 占用 2 个字节,那么它的长度为 2

C 语言标准并没有严格规定 short、int、long 的长度,只做了宽泛限制:

  • short 至少 2 个字节
  • int 建议为一个机器字长。32 位环境下为 4 个字节,64 位环境为 8 个字节
  • short 长度不能大于 int,long 的长度不能小于 int

总结如下:

2 ≤ s h o r t ≤ i n t ≤ l o n g 2 \leq short \leq int \leq long 2shortintlong

一般情况下:

操作系统「32位」shortintlong
Windows244
类Unix系统244
操作系统「64位」shortintlong
Windows244
类Unix系统248

正负数

数学中,数字有正负之分;C 语言中也是一样,short、int、long 都可以带上正负号:

如果不带负号,默认就是正数

int a = -10;
int b = +10;

符号只有 正、负 两种情况,用 1 bit 表示即可

C 语言规定,把内存的最高位作为符号位:

  • 0:表示正数
  • 1: 表示负数

以 int 为例,它占用 32 位内存,最高位表示正负号,其余位表示数值位。

在这里插入图片描述

short、int、long 默认都是带符号位的,符号位以外存储的才是数值位。很多情况下,我们确定某个数字只能是正数,这时候符号位就是多余的了,还不如删除符号位,这样能表示的数值范围更大「大一倍」

C 语言允许我们这么做,如果不希望设置符号位,可以在数据类型前加上 unsigned 关键字:

unsigned int a = 1000;
unsigned long p = 7000000000;

所有位都用来表示数值,正数的取值范围更大了。这也意味着,不能再表示负数了!

总结:

将一个数字分为 符号 和 数值两部分:

  • 加 unsigned 称为 无符号数,只能表示正数
  • 不加 unsigned 默认为 有符号数,能表示 正数和负数

整数如何存储的

加法和减法是计算机中最基本的运算,由硬件直接支持。为了提高加减法的运算效率,硬件电路要设计得尽量简单。

目前存在两个问题:

  • 有符号数,内存中同时存储符号位和数值位,运算时就不能像无符号数那样直接按位加减了;如果可以把符号位当做数值位一起参与运算,就不用设计额外的电路了
  • 加法和减法可以合并为加法运算,5 - 3 等价于 5 + (-3)

如果能实现以上两个目标,只要设计一种简单的,不用区分符号位和数值位的加法电路,就能同时实现 加法和减法 运算!

实际上,在真正的计算机硬件电路中,这两个目标都实现了。然而,简化硬件电路是由代价的,这个代价就是有符号数和无符号数在存储和读取时都需要进行转化

转化过程是怎么呢?首先需要了解以下几个概念:

正数的 原码、反码、补码 相同,只有负数才需要互相转化

  • 原码:一个数的二进制形式
  • 反码:负数的反码是将原码中符号位以外其它位取反
  • 补码:负数的补码 = 反码+1

在这里插入图片描述

在计算机内存中,整数一律采用补码的形式存储

当从内存中读取整数时,需要将 补码 ⇒ 原码,方法也很简单:

  1. 逆向运算
  2. 重复 原码⇒ 补码 步骤

在这里插入图片描述

补码如何简化运算

假设 6 和 18 都是 short 类型的,现在要计算 6-18 的结果,直接采用 原码 进行计算:

在这里插入图片描述

直接用 原码 表示整数,同时让符号位也参与运算,结果显然是不正确的。于是人们开始探索,不断试错,后来设计出了反码。下面是反码的计算过程:

在这里插入图片描述

这样一来,计算结果正确了。然后,这样还不算万事大吉,我们不妨将 减数 和 被减数 交换一下位置,计算一下 18 - 6 的结果:

在这里插入图片描述

红色的 1 是加法过程中的进位,它溢出了,直接被截掉

按照反码计算的结果是 11,与真实的结果 12 相差了 1。因此,按照反码来计算:

  • 小数减大数没有问题
  • 大数减小数始终少 1

相差的 1 要进行纠正,同时不能影响小数减去大数。人们绞尽脑汁又设计出了补码,给反码打一个补丁,终于把相差的 1 纠正过来了:

在这里插入图片描述

总结:

  • 小数减去大数,先+1「原码⇒补码」,结果为负数,后-1「补码⇒原码」,正好抵消
  • 大数减去小数,先+1「原码⇒补码」,结果为正数,不再减去「补码==原码」,相当于给结果多+1

补码这种天才般的设计,一举达成了上文提到的两个目标,简化了硬件电路!

取值范围

上文已经了解到,short、int、long 都只能存储有限的数值,当数值过大或过小时,超出的部分会被直接截掉,数值就不能正确存储了,这种现象称为 溢出「Overflow」

要想知道数值什么时候溢出,就要先知道各种整数类型的取值范围

  1. 无符号数

    计算无符号数「unsigned」的取值范围很简单,将内存中所有 bit 都置为 1 就是最大值,都置为 0 则是最小值。

    unsigned char 类型为例,它的长度为 1,占用 8 个bit:

    • 所有位置都是 1 时,值为 2 8 − 1 = 255 2^8-1=255 281=255

    • 所有位置都是 0,值为 0

    • 最大值计算方式

      将 unsigned char 所有 bit 置为 1,内存中为 1111 1111,直接的计算方法是:

      2 0 + 2 1 + 2 2 + 2 3 + 2 4 + 2 5 + 2 6 + 2 7 = 255 2^0+2^1+2^2+2^3+2^4+2^5+2^6+2^7 = 255 20+21+22+23+24+25+26+27=255

      这种方法虽然直接,如果是 8 个字节的 long 类型,就非常麻烦了。

      我们不妨换一种思路,先给 1111 1111 加上 1,然后再减去 1,这样一增一减正好抵消,不会影响最终值。

      // +1
      0b1111 1111 + 1 = 0b1 0000 0000 = 2^8 = 256
      
      // -1
      256 - 1 = 255
      

      当内存中所有位都是 1 时,这种 凑整 的技巧非常实用

  2. 有符号数

    以 char 为例,我们将补码从大到小排列:

在这里插入图片描述

黄色背景的一行需要单独说明,按照传统的由补码计算原码的公式,1000 0000 是无法计算的,计算反码需要 -1,此时就需要向最高位借位,而最高位是符号位,不能借出去,所以这就很矛盾!

int、long 同理

然而,与其把 1000 0000 当做无效的补码丢弃,还不如作为特殊值,这样还能多存储一个数字。于是直接规定,把这个特殊的补码表示 -128,因此我们也得到了 char 类型的取值范围为:-128~127。

  • 关于零值

    如果直接采用原码存储,0000 00001000 0000 将分别表示 +0 与 -0

    仔细看上表可以发现,char 取值范围只有一个 0 ,没有 +0-0 的区别,并且多存储了一个特殊值 -128,这也是采用补码小小的优势。

溢出

当数值超出取值范围时,就会发生溢出。溢出时,结果往往变得奇怪:

unsigned int a = 0x100000000;
int b = 0xffffffff;
printf("a=%u, b=%d", a, b);

// a=0, b=-1
  • a 为 unsigned int 类型,长度 4 个字节,能表示最大值为 0xFFFFFFFF,而 0x100000000 = 0xFFFFFFFF + 1,占用 33 位,超出 a 最大范围,导致最高位 1 被截断,剩下 32 位都是 0。也就是 a 存储到内存中就变成了 0

  • b 为 int 类型有符号数,最高位视为符号位,0xffffffff 的补码为 01111...1111,占 33 位,超出 int 范围,导致最高位 0 被截断,于是内存中实际存储为 1111 .... 1111

    当 printf 以 %d「有符号整型」打印 b 时,由于最高位是 1,判定为负数,随后从补码 ⇒ 原码:

    在这里插入图片描述

类型转换

转换规则

  • 小类型 ⇒ 大类型
    • 无符号数:高位补 0
    • 有符号数:高位补充符号位
  • 大类型 ⇒ 小类型:直接截断

类型转换导致的奇怪结果

char a = 128;
printf("%u", a);  // 4294967168

解析:

  1. 128 的补码为:00000000 00000000 00000000 10000000

  2. 存储到 char 中,发生截断,实际存储为:10000000

    注意:这里把符号位截断了,数据位 1 变成了新的符号位

  3. %u 打印,相当于转换为 unsigned int

  4. a 为有符号数,高位补充符号位,提升后:11111111 11111111 11111111 10000000

  5. 无符号数原码、补码相同,因此结果为 4294967168

自动类型转换

编译器隐式的,自动完成的数据类型转换

  1. 将一种数据类型赋值给另一种类型时会自动发生类型转换

    float f = 100;
    printf("%f", f);   // 100.000000
    
    int a = 3.14;
    printf("%d", a);   // 3
    
  2. 在不同类型混合运算中,编译器会将参与运算的各方转换为同一数据类型,然后再运算:

    转换按照数据长度增加的方向进行,以保证数据不失真

    1. int、long 参与运算时,先把 int 转换为 long 再运算
    2. char、short 参与运算时,必须先转换为 int
    3. 浮点运算都是以双精度进行「即使运算双方只有 float,也要先转换为 double」

    在这里插入图片描述

整型提升

整型的运算会在 CPU 内执行,CPU内整型运算器「ALU」的操作数的字节长度一般就是 int 的字节长度,同时也是 CPU 通用寄存器的长度。

通用 CPU「general-purpose cpu」难以直接实现两个 8bit 长度数据相加运算「虽然机器指令有这种字节相加运算」。

于是 C 语言标准规定:占用内存小于 int 的类型「 short,char」,在运算时首先需要提升为 int 类型,如果 int 类型不足以表示的话,就提升为 unsigned int,然后再执行表达式的运算

注意:发生的类型转换只是为了本次运算而进行的临时性转换,转换的结果会保存到临时的内存空间,不会影响数据本身的类型或值

char a = 10;

// 类型提升为 int,占用 4 个字节
printf("%zu", sizeof(+a));  // 4
char a = 30, b = 40, c = 10;

// a * b = 1200, 似乎 char 溢出
// 但由于整型提升的存在,仍得到了正确的结果

char d = a * b / c;

printf("%d", d);  // 120
  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值