c语言进阶 深度剖析数据在内存中的存储
深度剖析数据在内存中的存储
- 数据在内存中的存储深度剖析
- 1. 数据类型介绍
- (1)类型的基本归类
- ① 整形家族(32位环境)
- ② 浮点数家族
- ③ 构造类型
- 空类型
- 2. 整形在内存中的存储方式(32位环境)
- (1)原码、反码、补码
- ① 正整数的原反补码
- ② 负整数的原反补码
- ③ 为什么用补码存储?
- 3. 大小端存储
- (1)大小端定义
- (2)为什么存在大小端?
- (3)char类型与大小端
- 4.题目(补充加深了解)
- 1. 有符号与无符号char输出分析
- 2. char边界值%u输出
- 3. char正数溢出分析
- 4. 混合类型运算
- 5. 无符号循环陷阱
- 6. 字符数组与strlen
- 7. 无符号char死循环
- 8. 无符号数比较陷阱
- 5. 浮点数在内存中的存储
- (1)浮点数存储的例子。
- (2)IEEE 754浮点数存储规则
- (3)IEEE 754浮点数取出规则
- 6.示例程序解析
- **存储方式差异解析**:
- 一、为什么 0x00000009 还原成浮点数是 0.000000?
- 1. 整数 9 的二进制存储形式
- 2. 按 float 类型拆分 32 位二进制(IEEE 754 标准)
- 3. 依据 IEEE 754 规则计算浮点数
- 4. 浮点数实际值计算
- 5. 打印为 0.000000 的原因
- 二、为什么浮点数 9.0 存储后被解析为整数 1091567616?
- 1. 9.0 的二进制及规范化表示
- 2. 计算存储用指数 E
- 3. 有效数字 M 的存储形式
- 4. 组合 32 位二进制
- 5. 解析为整数的结果
数据在内存中的存储深度剖析
1. 数据类型介绍
前面我们已经学习了基本的内置类型:
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
- 如何看待内存空间的视角。
(1)类型的基本归类
① 整形家族(32位环境)
- 有符号整形:
int
(4字节)、short
(2字节)、long
(4字节)、long long
(8字节) - 无符号整形:
unsigned int
、unsigned short
、unsigned long
、unsigned long long
- 字符型(本质是整形):
char
(1字节,可表示signed char
或unsigned char
,取决于编译器)
② 浮点数家族
float
(4字节,单精度)double
(8字节,双精度)long double
(10/16字节,扩展精度,取决于编译器)
③ 构造类型
- 数组类型:
int[5]
、char[10]
(元素类型+长度共同构成类型) - 结构体类型:
struct Student
(自定义成员集合) - 枚举类型:
enum Color
(离散值集合) - 联合类型:
union Data
(共享内存的成员集合)
空类型
- void 表示空类型(无类型)
- 通常应用于函数的返回类型、函数的参数、指针类型
2. 整形在内存中的存储方式(32位环境)
(1)原码、反码、补码
在32位系统中,整数的存储使用补码形式。下面以20和-10为例:
① 正整数的原反补码
正整数的原码、反码、补码完全相同,最高位(符号位)为0。
示例:int a = 20
- 原码:
00000000 00000000 00000000 00010100
(二进制) - 反码:
00000000 00000000 00000000 00010100
- 补码:
00000000 00000000 00000000 00010100
- 十六进制:
0x00000014
② 负整数的原反补码
负整数的符号位为1,反码是原码除符号位外取反,补码是反码+1。
示例:int b = -10
- 原码:
10000000 00000000 00000000 00001010
(符号位1,数值位10的二进制) - 反码:
11111111 11111111 11111111 11110101
(原码除符号位外取反) - 补码:
11111111 11111111 11111111 11110110
(反码+1) - 十六进制:
0xFFFFFFF6
③ 为什么用补码存储?
-
统一零的表示:原码/反码中+0和-0表示不同,补码中只有一种(
0x00000000
) -
简化运算:补码让减法可转化为加法(
a - b = a + (-b)的补码
),硬件只需加法器即可// 例:计算 3 - 2 = 3 + (-2)
// 3的补码:00000000 00000000 00000000 00000011// -2的补码:11111111 11111111 11111111 11111110// 和的补码:00000000 00000000 00000000 00000001(结果1,正确)
3. 大小端存储
(1)大小端定义
- 大端模式:数据的低位字节存于内存高地址,高位字节存于内存低地址
- 小端模式:数据的低位字节存于内存低地址,高位字节存于内存高地址
示例1.:32位整数0x12345678
的存储
内存地址(低→高) | 小端模式 | 大端模式 |
---|---|---|
0x00000000 | 0x78 | 0x12 |
0x00000001 | 0x56 | 0x34 |
0x00000002 | 0x34 | 0x56 |
0x00000003 | 0x12 | 0x78 |
实例2. 画图解释 :
(2)为什么存在大小端?
为什么会有大小端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
总结来说
- 硬件设计差异:不同CPU架构(如ARM默认小端,PowerPC默认大端)对字节序的选择不同
- 历史原因:早期硬件存储能力有限,小端更便于CPU从低位开始处理数据(如累加运算)
- 验证当前环境字节序的代码:
int check_endian() {int a = 1; // 0x00000001return *(char*)&a; // 小端返回1,大端返回0}
(3)char类型与大小端
char
类型占1字节,不存在字节序问题(无高低位拆分),大小端对其存储无影响。
4.题目(补充加深了解)
1. 有符号与无符号char输出分析
#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
:有符号类型,8位存储- 范围:-128~127
- 32位-1(为整形)赋值给char:(会发生整形提升再截断)
- 原32位补码:11111111 11111111 11111111 11111111
- 截断为8位(首位为符号位):11111111
- -1别解释为-1存储为补码:11111111 (这是补码进行打印是会转为源码)
unsigned char
:无符号类型,8位存储- 范围:0~255
- 32位-1赋值给unsigned char:
- 原32位补码:11111111 11111111 11111111 11111111
- 截断为8位:11111111
- -1被解释为255(因为没有符号位了),存储为:11111111
- 整型提升过程:
-
有符号类型(char/signed char):
- 补码:11111111
- 提升规则:高位补符号位1
- 提升结果:11111111 11111111 11111111 11111111 (0xFFFFFFFF)
- 转换为原码:10000000 00000000 00000000 00000001 → -1
-
无符号类型(`unsigned char):
- 补码:11111111
- 提升规则:高位补0
- 提升结果:00000000 00000000 00000000 11111111 (0x000000FF)
- 直接解释:255
-
32位-1赋值给char:
- 原32位补码:11111111 11111111 11111111 11111111
- 截断为8位:11111111
-
%d打印时的完整过程:
- 有符号提升:补1 → 0xFFFFFFFF → 转原码 → -1
- 无符号提升:补0 → 0x000000FF → 直接解释 → 255
-
2. char边界值%u输出
int main()
{char a = -128;printf("%u\n",a);return 0;
}
输出结果:4294967168
详细解析:
-
-128的32位补码表示:
原码:10000000 00000000 00000000 10000000
补码:11111111 11111111 11111111 10000000 (0xFFFFFF80) -
char截断存储:
保留低8位:10000000 -
在以无符号整数%u打印发生整型提升:
有符号char,高位补1:
10000000 → 11111111 11111111 11111111 10000000 (0xFFFFFF80)
又因为是无符号原反补相同所以源码也为:
1111111 11111111 11111111 10000000 (0xFFFFFF80) -
%u解释:
直接解释为无符号整数:
2^32 - 128 = 4294967296 - 128 = 4294967168 -
如果按照%d的形式打印
要转换成源码 10000000 00000000 00000000 10000000 所以 结果是-128
3. char正数溢出分析
#include <stdio.h>
int main()
{char a = 128; printf("%u\n",a);return 0;
}
输出结果:4294967168
详细解析:
-
128超出signed char范围(-128~127):
二进制表示为10000000,与-128存储形式相同 -
存储和提升过程:
- 内存存储:10000000
- 整型提升:11111111 11111111 11111111 10000000 (0xFFFFFF80)
-
输出解释:
%u格式将0xFFFFFF80解释为4294967168
若用%d输出则显示-128
关键点:
- 128和-128在char存储中的二进制表示完全相同
- 输出结果取决于解释方式
4. 混合类型运算
int i = -20;
unsigned int j = 10;
printf("%d\n", i+j);
输出结果:-10
详细解析:
-
补码表示:
- -20的补码:
原码:10000000 00000000 00000000 00010100
补码:11111111 11111111 11111111 11101100 (0xFFFFFFEC) - 10的补码:00000000 00000000 00000000 00001010 (0x0000000A)
- -20的补码:
-
二进制加法:
11111111 11111111 11111111 11101100 (0xFFFFFFEC)
+
00000000 00000000 00000000 00001010 (0x0000000A)11111111 11111111 11111111 11110110 (0xFFFFFFF6)
-
结果转换:
补码:11111111 11111111 11111111 11110110
转原码:10000000 00000000 00000000 00001010 → -10
关键说明:
- 有符号和无符号数运算时,会先转换为无符号数运算
- 但用%d输出时,仍按有符号解释
5. 无符号循环陷阱
unsigned int i;
for(i = 9; i >= 0; i--) {printf("%u\n",i);
}
输出特征:
9
8
...
0
4294967295
4294967294
... (无限循环)
详细解析:
-
当i=0时:
- 执行i–操作
- 无符号数0-1=4294967295 (0xFFFFFFFF)
-
内存表示:
- 0的二进制:00000000 00000000 00000000 00000000
- -1的补码:11111111 11111111 11111111 11111111
- 无符号解释:4294967295
-
循环继续条件:
i >= 0 对于无符号数永远成立
解决方案:
使用有符号int:for(int i=9; i>=0; i–)
6. 字符数组与strlen
int main() {char a[1000];for(int i=0; i<1000; i++) {a[i] = -1-i;}printf("%d",strlen(a));return 0;
}
输出结果:255
详细解析:
因为strlen求的是字符串中\0之前有多少个字符 而这道题开始循环时过程是这样的 第一次循环 a[i]=-1第二次循环是 -2 一直到循环 a[i]等于-128时也就是 第127次循环的时候这时 a[i] 的二进制序列为 10000000 在进行下一次循环的时候又因为 是char类型存储范围是-128 到 0 到 127 所以 128次循环的 -1-i的结果-129又是因为-1是int类型的所以计算的时候会发生整形提升 所以计算的结果是 10000000 00000000 00000000 10000001 在准换成补码 11111111 11111111 11111111 01111111 在把这个数赋给a[i]时发生截断 所以实际存储的值是01111111而char是有符号的会认为最高位0是符号位所以实际存储的值是127 让后按照这样的方式一直计算下一个数是126一直到 0 而我们知道\0的arcII值也是0所以strlen只会计算到这里 所以结果为255 因为是-1到-128 在从127 到 0 ,最后我们这道题总结了一个规律 在有符号char类型的情况下-128-1为127 127+1是-128 0-1 是-1 -1加1是0
-
数值变化过程:
- -1 → -2 → … → -128 → 127 → 126 → … → 0
- -128(10000000)的下一个数是127(01111111)
-
二进制转换:
- -129的计算:
原码:10000000 00000000 00000000 10000001
补码:11111111 11111111 11111111 01111111
截断:01111111 (127)
- -129的计算:
-
strlen终止条件:
- 遇到第一个0值(‘\0’)
- 计算:128个负数 + 127个正数 = 255
内存布局示例:
a[0]到a[127]:-1到-128
a[128]到a[254]:127到1
a[255]:0
7. 无符号char死循环
unsigned char i = 0;
for(i = 0; i<=255; i++) {printf("hello world\n");
}
输出特征:
无限打印"hello world"
详细解析:
-
类型范围限制:
- unsigned char范围:0~255
- 当i=255时:11111111
-
溢出行为:
- i++操作:11111111 + 1 = 00000000 (溢出归零)
- 循环条件i<=255永远成立
-
二进制过程:
255: 11111111
256: 100000000 → 截断为00000000
正确写法:
使用int类型:for(int i=0; i<256; i++)
8. 无符号数比较陷阱
c#include <stdio.h>
#include <string.h>
int main() {if(strlen("abc")-strlen("abcded")>=0)printf(">\n");elseprintf("<\n");
}
输出结果:>
因为strlen返回的结果是无符号整形 两个无符号整形相减的结果还是无符号整形 又因为无符号整形的恒大于等于0所以结果是>
详细解析:
-
strlen返回值:
- “abc”:3 (0x00000003)
- “abcded”:5 (0x00000005)
-
减法运算:
- 3-5的补码表示:
00000011 (3)- 00000101 (5)
= 11111110 (-2的补码)
- 00000101 (5)
- 3-5的补码表示:
-
无符号解释:
- 11111110 → 0xFFFFFFFE → 4294967294
- 任何无符号数>=0恒成立
-
正确比较方式:
if(strlen(“abc”) > strlen(“abcded”))
或强制转换为有符号数:
if((int)(strlen(“abc”)-strlen(“abcded”))>=0)
关键点:
- 无符号数运算结果仍为无符号数
- 负数的补码被解释为很大的正数
5. 浮点数在内存中的存储
(1)浮点数存储的例子。
#include <stdio.h>
int main() {int n = 9;float *pFloat = (float *)&n;printf("n的值为:%d\n", n); // 输出:9printf("*pFloat的值为:%f\n", *pFloat); // 输出:0.000000*pFloat = 9.0;printf("num的值为:%d\n", n); // 输出:1091567616printf("*pFloat的值为:%f\n", *pFloat); // 输出:9.000000return 0;
}
为什么会出现一些我们不知道怎么计算的值呢?先不用慌张我们先了解一些浮点数的存储规则到最后我们会详细讲解这道题。
(2)IEEE 754浮点数存储规则
num
和 *pFloat
在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。
详细解读:
根据国际标准IEEE
(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:
任意二进制浮点数V可表示为:
V=(−1)S×M×2EV = (-1)^S \times M \times 2^EV=(−1)S×M×2E
- (−1)S(-1)^S(−1)S:符号位,S=0时V为正数,S=1时V为负数
- MMM:有效数字,满足1≤M<21 \leq M < 21≤M<2,形式为1.xxxxxx1.xxxxxx1.xxxxxx(xxxxxx为小数部分)
- 2E2^E2E:指数位,决定数值的数量级
二进制小数的位权规则
二进制小数的每一位对应的位权(数值权重)从左到右依次为 2−1,2−2,2−3,...,2−n2^{-1}, 2^{-2}, 2^{-3}, ..., 2^{-n}2−1,2−2,2−3,...,2−n,即:
- 小数点后第1位:2−1=0.52^{-1} = 0.52−1=0.5
- 小数点后第2位:2−2=0.252^{-2} = 0.252−2=0.25
- 小数点后第3位:2−3=0.1252^{-3} = 0.1252−3=0.125
- 小数点后第n位:2−n=1/(2n)2^{-n} = 1/(2^n)2−n=1/(2n)
示例:
二进制小数 0.101
转换为十进制的计算过程:
0.101(二进制) = 1×2⁻¹ + 0×2⁻² + 1×2⁻³
= 1×0.5 + 0×0.25 + 1×0.125
= 0.5 + 0 + 0.125
= 0.625(十进制)
示例1:十进制5.0
- 二进制:101.0101.0101.0
- 规范化:1.01×221.01 \times 2^21.01×22
- 解析:S=0S=0S=0,M=1.01M=1.01M=1.01,E=2E=2E=2
示例2:十进制-5.0
- 二进制:−101.0-101.0−101.0
- 规范化:−1.01×22-1.01 \times 2^2−1.01×22
- 解析:S=1S=1S=1,M=1.01M=1.01M=1.01,E=2E=2E=2
示例3:9.5f(32位float)
- 整数部分9的二进制:100110011001
- 小数部分0.5的二进制:0.10.10.1(因为0.5=1×2−10.5 = 1 \times 2^{-1}0.5=1×2−1)
- 完整二进制:1001.11001.11001.1
- 规范化:1.0011×231.0011 \times 2^31.0011×23
- 解析:S=0S=0S=0,M=1.0011M=1.0011M=1.0011,E=3E=3E=3
示例4:9.6f的不精确性
-
整数部分9的二进制:100110011001(计算过程:9÷2=49 ÷ 2 = 49÷2=4余111,4÷2=24 ÷ 2 = 24÷2=2余000,2÷2=12 ÷ 2 = 12÷2=1余000,1÷2=01 ÷ 2 = 01÷2=0余111,倒序取余得100110011001)。
-
小数部分0.6的二进制转换(核心原理:通过乘2取整数部分,逐步逼近小数位):
-
第一步:0.6×2=1.20.6 \times 2 = 1.20.6×2=1.2
- 计算目的:提取小数点后第一位的二进制数。
- 结果分析:乘积整数部分为111,故二进制小数第一位为111;剩余小数部分为0.20.20.2(用于计算下一位)。
- 当前二进制小数:0.10.10.1
-
第二步:0.2×2=0.40.2 \times 2 = 0.40.2×2=0.4
- 计算目的:提取小数点后第二位的二进制数。
- 结果分析:乘积整数部分为000,故二进制小数第二位为000;剩余小数部分为0.40.40.4。
- 当前二进制小数:0.100.100.10
-
第三步:0.4×2=0.80.4 \times 2 = 0.80.4×2=0.8
- 计算目的:提取小数点后第三位的二进制数。
- 结果分析:乘积整数部分为000,故二进制小数第三位为000;剩余小数部分为0.80.80.8。
- 当前二进制小数:0.1000.1000.100
-
第四步:0.8×2=1.60.8 \times 2 = 1.60.8×2=1.6
- 计算目的:提取小数点后第四位的二进制数。
- 结果分析:乘积整数部分为111,故二进制小数第四位为111;剩余小数部分为0.60.60.6(与初始小数相同,循环开始)。
- 当前二进制小数:0.10010.10010.1001
-
循环延续:
后续计算将重复上述步骤,得到二进制小数部分为0.1001100110011...0.1001100110011...0.1001100110011...(循环节100110011001)。
-
-
结论:0.6的二进制表示是无限循环小数,而float类型仅能存储23位有效数字(含小数部分),因此9.6f存储时会被截断,导致精度损失(实际存储值与理论值存在微小偏差)。
示例5:double类型的存储限制
- 64位double类型仅能存储52位有效数字(加隐含的1共53位)
- 对于需超过53位二进制才能精确表示的小数(如某些无理数的近似值),即使理论上可精确表示,也会因存储空间限制被截断,导致精度损失
IEEE 754存储结构
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
类型 | 总位数 | 符号位S(1位) | 指数E | 有效数字M |
---|---|---|---|---|
float | 32 | 第31位 | 第30-23位(8位) | 第22-0位(23位) |
double | 64 | 第63位 | 第62-52位(11位) | 第51-0位(52位) |
有效数字M的特别规定
- 由于1≤M<21 \leq M < 21≤M<2,也就是说,M可表示为1.xxxxxx1.xxxxxx1.xxxxxx,存储时仅保留小数部分xxxxxx(舍去整数位1) IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
- 所以32位float的M实际精度为24位(23位存储+1位隐含),64位double为53位(52位存储+1位隐含)
至于指数E,情况就比较复杂。
指数E的特别规定
- E为无符号整数:这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
- 偏移值:存入内存时E的真值需加偏移值(float加127,double加1023)
- 示例:float类型2102^{10}210的E真值为10,存入内存为10+127=13710 + 127 = 13710+127=137(二进制100010011000100110001001)
- 示例:double类型2−52^{-5}2−5的E真值为-5,存入内存为−5+1023=1018-5 + 1023 = 1018−5+1023=1018(二进制111111101011111110101111111010)
浮点数的大小端存储(小端示例)
浮点数在存储的时候也存在大小端的存储问题
- float类型9.5f的二进制存储序列:0100000100011000000000000000000000 10000010 001100000000000000000000010000010001100000000000000000000(十六进制0x411800000x411800000x41180000)
- 按字节拆分:0x410x410x41、0x180x180x18、0x000x000x00、0x000x000x00
- 小端存储(地址低→高):0x000x000x00、0x000x000x00、0x180x180x18、0x410x410x41
(3)IEEE 754浮点数取出规则
指数E从内存中取出还可以再分成三种情况:
指数E的三种情况
-
E不全为0且不全为1(正常情况)
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。- 真实指数 = E存储值 - 偏移值
- M = 1.xxxxxx1.xxxxxx1.xxxxxx(补回隐含的1)
- 示例:float类型0.5
- 二进制:0.1=1.0×2−10.1 = 1.0 \times 2^{-1}0.1=1.0×2−1
- E存储值 = −1+127=126-1 + 127 = 126−1+127=126(二进制011111100111111001111110)
- M存储:000000000000000000000000000000000000000000000000000000000000000000000(即1.01.01.0的小数部分)
- 存储序列:001111110000000000000000000000000 01111110 0000000000000000000000000111111000000000000000000000000
-
E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。- 真实指数 = 1−偏移值1 - 偏移值1−偏移值
- M = 0.xxxxxx0.xxxxxx0.xxxxxx(不补回1)
- 用途:表示±0及接近0的极小值
- 示例:float类型2−1492^{-149}2−149(接近0)
- E存储值 = 0
- 真实指数 = 1−127=−1261 - 127 = -1261−127=−126
- M存储:000000000000000000000010000000000000000000000100000000000000000000001(即0.000000000000000000000010.000000000000000000000010.00000000000000000000001)
- 数值 = 1.001×2−1461.001 \times 2^{-146}1.001×2−146(极小值)
-
E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)- 若M全为0:表示±无穷大(由S决定)
- 示例:float正无穷大
- 存储序列:011111111000000000000000000000000 11111111 0000000000000000000000001111111100000000000000000000000
- 示例:float正无穷大
- 若M不全为0:表示NaN(非数值,如0/00/00/0)
- 示例:float的NaN
- 存储序列:011111111000000000000000000000010 11111111 0000000000000000000000101111111100000000000000000000001
- 示例:float的NaN
- 若M全为0:表示±无穷大(由S决定)
6.示例程序解析
存储方式差异解析:
-
整型视角看浮点:
- 整型9的二进制:
0000 0000 0000 0000 0000 0000 0000 1001
- 按浮点解析:
- 符号位S=0
- 指数E=00000000(全0)
- 尾数M=00000000000000000001001
- 根据IEEE 754规则,E全0时表示非常接近0的数
- 实际值 ≈ 1.001×2−1461.001×2^{-146}1.001×2−146 ≈ 0.000000
- 整型9的二进制:
-
浮点视角看整型:
- 浮点9.0的存储:
- 二进制:1001.0 → 1.001×231.001×2^31.001×23
- 内存表示:
0 10000010 00100000000000000000000
- 转为整型直接解释:1091567616
- 浮点9.0的存储:
-
关键结论:
- 整型和浮点型虽然都占4字节,但二进制解释规则完全不同
- 直接类型转换会导致位模式被错误解释
- 浮点存储包含符号位、指数域和尾数域的特殊结构
示例程序解析
一、为什么 0x00000009 还原成浮点数是 0.000000?
1. 整数 9 的二进制存储形式
整数 9 的 32 位二进制表示为:
00000000 00000000 00000000 00001001
(计算依据:正数的原、反、补码相同,通过除 2 取余法得二进制 1001,高位补 0 至 32 位) 对应的十六进制为 0x00000009
。
2. 按 float 类型拆分 32 位二进制(IEEE 754 标准)
- 符号位 s(第 31 位):取最高位,值为
0
(表示正数,即 (−1)s=1(-1)^s=1(−1)s=1) - 指数 E(第 30-23 位):取第 30 至 23 位,共 8 位,值为
00000000
(全为 0) - 有效数字 M(第 22-0 位):取剩余 23 位,值为
00000000000000000001001
3. 依据 IEEE 754 规则计算浮点数
因指数 E 全为 0,触发特殊解析规则(用于表示接近 0 的极小值):
- 真实指数计算:E 全为 0 时,真实指数 = 1 - 偏移值(32 位 float 偏移值为 127),即 1−127=−1261 - 127 = -1261−127=−126
- 有效数字 M 处理:E 全为 0 时,M 不补回整数位 1,直接为
0.xxxxxx
形式,此处 M=0.00000000000000000001001M=0.00000000000000000001001M=0.00000000000000000001001
4. 浮点数实际值计算
根据公式 V=(−1)s×M×2E真实V=(-1)^s \times M \times 2^{E_{\text{真实}}}V=(−1)s×M×2E真实:
V=1×0.00000000000000000001001×2−126V = 1 \times 0.00000000000000000001001 \times 2^{-126}V=1×0.00000000000000000001001×2−126
化简得:V=1.001×2−146V=1.001 \times 2^{-146}V=1.001×2−146(极小值,约 10−4410^{-44}10−44)
5. 打印为 0.000000 的原因
该值远小于 %f
默认显示精度(6 位小数),故舍入为 0.000000
。
二、为什么浮点数 9.0 存储后被解析为整数 1091567616?
1. 9.0 的二进制及规范化表示
- 十进制 9.0 的二进制为
1001.0
(整数部分 9=8+1=2³+2⁰,故二进制 1001) - 规范化为 IEEE 754 形式:9.0=(−1)0×1.001×239.0=(-1)^0 \times 1.001 \times 2^39.0=(−1)0×1.001×23
其中:s=0s=0s=0(正数),M=1.001M=1.001M=1.001(有效数字),E真实=3E_{\text{真实}}=3E真实=3(指数)
2. 计算存储用指数 E
32 位 float 的指数需加偏移值 127(将负数指数转为无符号整数):
E存储=3+127=130E_{\text{存储}}=3 + 127=130E存储=3+127=130
130 的 8 位二进制为 10000010
(计算:130=128+2=2⁷+2¹,故二进制 10000010)
3. 有效数字 M 的存储形式
因 1≤M<21 \leq M < 21≤M<2,M 的整数位必为 1,可省略存储,仅存小数部分:
M 的存储值为 001
(1.001 的小数部分),补 0 至 23 位得 00100000000000000000000
4. 组合 32 位二进制
浮点数 9.0 的 32 位二进制为:
0 10000010 00100000000000000000000
(符号位 s + 指数 E + 有效数字 M)
5. 解析为整数的结果
该 32 位二进制作为整数的十进制值:
230+227+225=1073741824+134217728+33554432=10915676162^{30} + 2^{27} + 2^{25} = 1073741824 + 134217728 + 33554432 = 1091567616230+227+225=1073741824+134217728+33554432=1091567616