[C]基础16.数据在内存中的存储
- 博客主页:向不悔
- 本篇专栏:[C]
- 您的支持,是我的创作动力。
文章目录
- 0、总结
- 1、整数在内存中的存储
- 1.1 整数的二进制表示方法
- 1.2 不同整数的表示方法
- 1.3 内存中存储的是补码
- 2、大小端字节序和字节序判断
- 2.1 什么是大小端
- 2.2 为什么有大小端
- 2.3 练习
- 2.3.1 练习1
- 2.3.2 练习2
- 2.3.3 练习3
- 2.3.4 练习4
- 2.3.5 练习5
- 2.3.6 练习6
- 3、浮点数在内存中的存储
- 3.1 练习
- 3.2 浮点数的存储
- 3.2.1 浮点数是什么
- 3.2.2 浮点数存储的标准
- 3.2.3 浮点数的组成
- 1、符号位(S)
- 2. 指数位(E)
- 3. 尾数位(M)
- 3.3 浮点数存储的步骤
- 3.3.1 转换为二进制形式
- 3.3.2 规范化处理
- 3.3.3 确定各部分的值
- 3.3.4 组合成二进制序列
- 3.4 从内存中读取浮点数
- 3.4.1 提取各部分的值
- 3.4.2 还原浮点数
- 3.5 特殊情况
- 3.5.1 浮点数存储中特殊情况的说明
- 3.5.2 特殊情况:指数位E全为0
- 1. 指数的真实值计算
- 2. 有效数字M的处理方式
- 3. 特殊用途
- 3.5.3 特殊情况:指数位E全为1
- 1. 有效数字M全为0
- 2. 有效数字M不全为0
- 3.6 题目解析1:9 -> 0.000000
- 3.7 题目解析2:9.0 -> 1091567616
0、总结
1、整数在内存中的存储
1.1 整数的二进制表示方法
整数的二进制表示方法有三种:原码、反码和补码。这三种表示方法均包含符号位和数值位两部分。符号位用0
表示“正”,用1
表示“负”,数值位的最高位被视作符号位,其余位为数值位。
1.2 不同整数的表示方法
- 正整数:正整数的原码、反码和补码是相同的。
- 负整数:负整数的三种表示方法各不相同。
- 原码:直接将数值按照正负数的形式翻译成二进制得到。
- 反码:将原码的符号位保持不变,其余位依次按位取反。
- 补码:反码加
1
得到补码。
1.3 内存中存储的是补码
对于整数而言,数据在内存中实际存储的是补码。原因如下:
- 使用补码可以将符号位和数值域统一处理。
- 加法和减法也可以统一处理(CPU只有加法器)。
- 补码与原码相互转换的运算过程相同,无需额外的硬件电路。
2、大小端字节序和字节序判断
2.1 什么是大小端
当超过一个字节的数据在内存中存储时,就会涉及到存储顺序的问题,根据不同的存储顺序,可分为大端字节序存储和小端字节序存储,具体概念如下:
- 大端存储模式 :数据的低位字节内容保存在内存的高地址处,数据的高位字节内容保存在内存的低地址处。
- 小端存储模式 :数据的低位字节内容保存在内存的低地址处,数据的高位字节内容保存在内存的高地址处。
如图:
2.2 为什么有大小端
在计算机系统中,存储和处理数据的基本单位是字节,每个地址单元对应一个字节,一个字节为 8bit 位。但在实际编程和数据处理中,除了 8bit的 char 型数据外,还存在 16bit 的 short 型、32bit 的 long型等多种数据类型(具体数据类型所占字节数会因编译器而异)。同时,对于位数大于 8 位的处理器,如 16 位或 32位的处理器,其寄存器宽度大于一个字节,这就面临一个如何将多个字节的数据合理安排存储顺序的问题,大小端存储模式正是为了应对这种情况而产生的两种不同的存储方案。
例如:一个 16bit 的 short 型变量 x,其在内存中的起始地址为 0x0010,x 的值为 0x1122,其中 0x11为高字节,0x22 为低字节。对于大端模式,会将高字节 0x11 放在低地址 0x0010 处,低字节 0x22 放在高地址 0x0011处;小端模式则相反,将低字节 0x22 放在低地址 0x0010 处,高字节 0x11 放在高地址 0x0011处。不同的处理器架构对大小端的支持有所不同,常见的 X86 结构采用小端模式,KEIL C51 采用大端模式,很多 ARM、DSP也采用小端模式,而部分 ARM 处理器还可以通过硬件设置来选择使用大端或小端模式。
大小端存储模式的产生,主要是由于不同的计算机系统设计者在处理多字节数据存储顺序时有不同的思路和考量。大端模式更符合人类阅读和书写数字的习惯,比如我们平时写数字时高位在左,低位在右,大端存储在内存中存储数据的顺序与之类似,便于理解和调试。而小端模式在某些算术运算和数据处理操作中可能会更高效,例如在进行字节寻址或处理低字节优先的数据时,可以直接获取到所需字节的数据,减少了在存储和读取过程中的一些额外操作和转换步骤。因此,不同的应用场景和系统架构需求促使了大小端存储模式的并存与演变。
2.3 练习
2.3.1 练习1
请简述大端字节序和小端字节序的概念,并设计一个小程序判断当前机器的字节序。(百度笔试题)
#include <stdio.h>void check_sys()
{int a = 1;char* b = (char*)&a;if (*b == 1)printf("小端\n");elseprintf("大端\n");
}void check_sys2()
{union{int i;char c;}un;un.i = 1;if (un.c == 1)printf("小端\n");elseprintf("大端\n");
}int main()
{check_sys();check_sys2();return 0;
}
运行:
小端
小端
2.3.2 练习2
#include <stdio.h>
int main()
{char a = -1;signed char b = -1;unsigned char c = -1;printf("a=%d,b=%d,c=%d", a, b, c);return 0;
}
运行:
a=-1,b=-1,c=255
总结:
char
和signed char
是有符号类型,可以直接存储-1
。unsigned char
是无符号类型,不能存储负数。当将-1
赋值给unsigned char
时,会通过模运算(mod)转换为 255,即 (-1 mod 256 = 255)。- 因此,
a
和b
的值为-1
,而 c 的值为255
。 - 这里涉及的关键知识点是模运算(mod),用于处理无符号类型存储负数时的转换。
2.3.3 练习3
#include <stdio.h>
int main()
{char a = -128;printf("%u\n", a);return 0;
}
运行:
4294967168
总结:
char
是有符号类型,默认范围是 -128 到 127。- 当使用
%u
(无符号整数格式化占位符)输出有符号字符变量a
时,a
的值 -128 会被解释为无符号整数。 - 在 32 位系统中,
char
类型的 -128 在内存中的二进制表示为0xFFFFFF80
(补码形式)。当以无符号整数解释时,其值为 4294967168。
#include <stdio.h>
int main()
{char a = 128;printf("%u\n", a);return 0;
}
4294967168
总结:
- 当给
char
类型变量a
赋值 128 时,超出了其表示范围,会发生溢出。在内存中,a
的值会变成 -128(补码形式)。
2.3.4 练习4
#include <stdio.h>
int main()
{char a[1000];int i;for (i = 0; i < 1000; i++){a[i] = -1 - i;}printf("%d", strlen(a));return 0;
}
运行:
255
总结:
char
数组的符号性:char
在多数编译器中默认为signed char
,范围是-128到127。- 赋值逻辑:循环中
a[i] = -1 - i
,当i=255
时,表达式值为-256。由于char
是8位,-256 mod 256 = 0,此时a[255]
被赋值为0。 - 字符串终止符:
strlen
遇到第一个'\0'
(即ASCII 0)时停止计数。数组中第一个0出现在索引255处。 - 结果计算:从
a[0]
到a[254]
共255个字符,故strlen(a)
返回255。
2.3.5 练习5
#include <stdio.h>
unsigned char i = 0;
int main()
{for (i = 0; i <= 255; i++){printf("hello world\n");}return 0;
}
运行:
死循环
总结:
unsigned char
的范围:unsigned char
的取值范围是 0~255(8 位无符号整数)。- 当 i = 255 时,执行 i++ 后,i 会溢出,变成 0(而不是 256,因为
unsigned char
无法存储 256)。
- 循环条件 i <= 255 永远成立:
- 每次 i 达到 255 后,i++ 使其变回 0,循环永远不会终止。
#include <stdio.h>
int main()
{unsigned int i;for (i = 9; i >= 0; i--){printf("%u\n", i);}return 0;
}
运行:
死循环
总结:
unsigned int
的范围:unsigned int
的取值范围是 0 到 4294967295(32 位无符号整数)。- 当 i = 0 时,执行 i-- 后,i 会 下溢,变成 4294967295(即
UINT_MAX
),而不是 -1。
- 循环条件
i >= 0
永远成立:- 由于
unsigned int
永远不会小于 0,循环条件始终为真。 - 当 i 递减到 0 并继续 i-- 时,它会变成最大值,循环永远不会终止。
- 由于
执行流程:
- 输出 9, 8, 7, …, 1, 0(正常部分)。
- 然后 i-- 使 i = 4294967295,继续循环:
- 输出 4294967295, 4294967294, …,无限循环。
2.3.6 练习6
#include <stdio.h>
int main()
{int a[4] = { 1, 2, 3, 4 };int* ptr1 = (int*)(&a + 1);int* ptr2 = (int*)((int)a + 1);printf("%x\n", ptr1[-1]);printf("%x\n", *ptr2);return 0;
}
运行:
4
崩溃
总结:
第一个输出(4)的原因分析
&a
获取的是整个数组的地址,类型为int (*)[4]
(指向长度为4的数组的指针)- 指针运算的单位是所指向类型的大小
解释
&a + 1
:- 跳过了整个数组(4个int,通常16字节)
- 指向数组末尾的下一个内存位置
ptr1[-1]
等价于*(ptr1 - 1)
:- 将
ptr1
回退一个int
(4字节)的位置 - 实际指向
a[3]
,其值为4
- 将
第二个操作(崩溃)的原因分析
- 违反了指针的对齐规则
- 进行了非法的类型转换和指针运算
解释
(int)a
:- 将指针强制转换为整数值(假设32位系统)
(int)a + 1
:- 简单地将地址数值加1(如0x1000 → 0x1001)
- 不是按照int大小增加(不是加4字节)
(int *)
转换回指针:- 新指针指向第一个
int
的中间位置(不对齐) - 在大多数架构上,
int*
必须4字节对齐
- 新指针指向第一个
3、浮点数在内存中的存储
3.1 练习
#include <stdio.h>
int main()
{int n = 9;float* pFloat = (float*)&n;printf("n的值为:%d\n", n);printf("*pFloat的值为:%f\n", *pFloat);*pFloat = 9.0;printf("num的值为:%d\n", n);printf("*pFloat的值为:%f\n", *pFloat);return 0;
}
运行:
n的值为:9
*pFloat的值为:0.000000
num的值为:1091567616
*pFloat的值为:9.000000
3.2 浮点数的存储
3.2.1 浮点数是什么
浮点数就是带有小数部分的数字,比如 3.14、5.0 这样的数。在计算机里,浮点数的存储方式和整数有所不同。
3.2.2 浮点数存储的标准
计算机中浮点数的存储遵循 IEEE 754 标准,这个标准规定了浮点数在内存中的表示方法。
3.2.3 浮点数的组成
一个浮点数在内存中占据一定的字节空间,以 32 位浮点数(float 类型)为例,它由三个部分组成:
1、符号位(S)
- 位置 :最高位(最左边的那一位)。
- 作用 :表示这个浮点数是正数还是负数。
- 约定 :0 表示正数,1 表示负数。
2. 指数位(E)
- 位置 :紧跟在符号位后面的 8 位。
- 作用 :用来表示浮点数的大小范围,相当于科学计数法中的指数部分。
- 特殊处理 :存储时,真正的指数值要加上一个中间数(偏置值),对于 8 位指数位,这个中间数是 127。比如,如果实际指数是 3,那么存储的时候就是 3 + 127 = 130,二进制表示为
10000010
。
3. 尾数位(M)
- 位置 :剩下的 23 位。
- 作用 :表示浮点数的有效数字,决定了浮点数的精度。
- 特殊处理 :根据 IEEE 754 标准,在存储时默认尾数的第 1 位是 1,所以在内存里只保存后面的部分,这样可以节省空间,提高精度。
3.3 浮点数存储的步骤
假设我们要存储一个十进制浮点数 5.0:
3.3.1 转换为二进制形式
- 将 5.0 转换为二进制是
101.0
。
3.3.2 规范化处理
- 把二进制数转换为类似科学计数法的形式,即 1.01×2^2,这样就满足了尾数部分第 1 位为 1 的要求。
3.3.3 确定各部分的值
- 符号位(S) :因为 5.0 是正数,所以 S = 0。
- 指数位(E) :实际指数是 2,加上偏置值 127 后,得到 129,二进制表示为
10000001
。 - 尾数位(M) :尾数部分是 01,后面补 21 个 0 来占满 23 位,即
01000000000000000000000
。
3.3.4 组合成二进制序列
- 按照符号位、指数位、尾数位的顺序组合起来,得到
0 10000001 01000000000000000000000
。
3.4 从内存中读取浮点数
当计算机从内存中读取这个二进制序列并还原成浮点数时,会进行以下操作:
3.4.1 提取各部分的值
- 符号位(S) :0,表示正数。
- 指数位(E) :
10000001
,转换为十进制是 129,减去偏置值 127,得到实际指数 2。 - 尾数位(M) :
01000000000000000000000
,在前面补上默认的 1,得到 1.01。
3.4.2 还原浮点数
- 根据提取的值,还原出浮点数为 (-1)^0 ×1.01×2^2=5.0。
3.5 特殊情况
3.5.1 浮点数存储中特殊情况的说明
在浮点数存储中,除了常规的数值表示外,还有一些特殊情况用于表示一些特殊的数值,比如±0和±∞。
3.5.2 特殊情况:指数位E全为0
当指数位E全为0时,浮点数的表示遵循以下规则:
1. 指数的真实值计算
- 指数E的真实值等于1减去中间数(偏置值),即对于32位浮点数,E的真实值为1 - 127 = -126;对于64位浮点数,E的真实值为1 - 1023 = -1022。
2. 有效数字M的处理方式
- 在这种情况下,有效数字M不再在前面加上默认的1,而是还原为0.xxxxxx的形式,其中xxxxxx表示M的小数部分。
3. 特殊用途
- 这种情况用于表示±0以及接近于0的很小的数字。例如,0可以表示为符号位为0,指数位全为0,尾数位全为0的二进制序列。
具体情况实例:
- 符号位(S)为0表示正数,指数位全为0,尾数位为
00100000000000000000000
,此时指数真实值为-126,有效数字M还原为0.00100000000000000000000,则浮点数为 (-1)^0 × 0.00100000000000000000000 × 2^(-126),即 0.000000117…(十进制表示为一个非常接近0的正数)。
3.5.3 特殊情况:指数位E全为1
当指数位E全为1时,浮点数的表示遵循以下规则:
1. 有效数字M全为0
- 如果有效数字M全为0,则表示±无穷大(正负取决于符号位S)。此时,符号位为0表示正无穷大,符号位为1表示负无穷大。
2. 有效数字M不全为0
- 如果有效数字M不全为0,则表示这不是一个有效的浮点数(Not a Number,NaN)。通常用于表示一些无法计算的结果,如0/0等。
具体实例:
- 符号位(S)为0,指数位全为1(二进制为
11111111
),尾数位全为0,表示正无穷大。 - 符号位(S)为1,指数位全为1,尾数位全为0,表示负无穷大。
3.6 题目解析1:9 -> 0.000000
- 整数 9 的二进制表示 :当我们将整数 9 以整型的形式存储在内存中时,其二进制序列为
00000000 00000000 00000000 00001001
。 - 将 9 的二进制序列按浮点数形式拆分
- 符号位(S) :取二进制序列的最高位,即第 1 位,得到 S = 0,表示这是一个正数。
- 指数位(E) :接下来的 8 位(第 2 位到第 9 位)为指数位。在这里,这 8 位全为 0,即 E = 00000000。
- 尾数位(M) :剩下的 23 位(第 10 位到第 32 位)为尾数位。在这里,尾数位为 000000000000000000000000001001。
- 判断情况并计算浮点数值
- 由于指数位 E 全为 0,因此符合 IEEE 754 标准中 E 为全 0 的特殊情况。
- 根据标准,此时浮点数的指数部分的真实值等于 1 - 127 = -126(对于 32 位浮点数,中间数是 127)。
- 同时,尾数部分不再在前面补 1,而是直接作为小数部分,即 M = 0.000000000000000000001001。
- 因此,浮点数 V = (-1)^S × M × 2^(E 真实值) = (-1)^0 × 0.000000000000000000001001 × 2^(-126) = 1.001 × 2^(-146)。
- 这个值是一个非常接近于 0 的正数,在十进制小数中通常表示为 0.000000。
3.7 题目解析2:9.0 -> 1091567616
- 浮点数 9.0 的二进制表示
- 首先,将 9.0 转换为二进制形式为
1001.0
。为了符合浮点数的存储规范,我们需要将其转换为科学计数法形式,即 1.001 × 2^3。 - 这里的 1.001 是尾数部分,2^3 是指数部分。
- 首先,将 9.0 转换为二进制形式为
- 确定各部分的值
- 符号位(S) :9.0 是正数,所以 S = 0。
- 指数位(E) :实际指数是 3。根据 IEEE 754 标准,存储时需要将指数加上一个中间数(对于 32 位浮点数,中间数是 127),所以 E = 3 + 127 = 130,其二进制表示为
10000010
。 - 尾数位(M) :尾数部分是 001,后面再加 20 个 0 来凑满 23 位,即 M =
00100000000000000000000
。
- 组合成二进制序列
- 按照符号位、指数位、尾数位的顺序组合起来,得到的二进制序列为
0 10000010 00100000000000000000000
。
- 按照符号位、指数位、尾数位的顺序组合起来,得到的二进制序列为
- 将二进制序列当作整数解析
- 这个 32 位的二进制序列被当作一个整数来解析时,其对应的十进制数值可以通过将二进制数转换为十进制数来得到。
- 这个二进制数转换为十进制数后为 1091567616。
完。