案例引入
请看下面一段代码并思考结果:
#define _CRT_SECURE_NO_WARNINGS #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; }
结果如下:
引言
在C语言中,浮点数用于表示实数,尤其是那些带有小数点的数值。浮点数的存储机制复杂,但它是计算机科学中的重要组成部分。本文将详细介绍C语言中的浮点数在内存中的存储方式,基于IEEE 754标准,并涵盖单精度和双精度浮点数的内部表示。
1. 浮点数的基本概念
浮点数的表示由三个主要部分组成:
- 符号位(Sign Bit):表示数值的正负。0表示正数,1表示负数。
- 指数位(Exponent):确定浮点数的范围或数量级。通过调整指数,浮点数可以表示非常大或非常小的值。
- 尾数(Mantissa):也称为有效数字,表示浮点数的精确值。尾数在计算中决定了浮点数的精度。
2. IEEE 754标准
IEEE 754标准是浮点数存储的国际标准,定义了浮点数的表示和运算规则。
根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
• (−1)^S 表⽰符号位,当S=0,V为正数;当S=1,V为负数
• M 表⽰有效数字,M是⼤于等于1,⼩于2的
• 2^E 表⽰指数位
浮点数的存储,实际上存储的就是S、M、E相关的值。
根据IEEE 754标准,浮点数分为单精度(32位)和双精度(64位)两种格式。
2.1 单精度浮点数(32位)
单精度浮点数使用32位存储,其中包括:
- 符号位:1位
- 指数位:8位
- 尾数:23位(实际尾数有24位,因为有一个隐含的1位)
单精度浮点数的存储格式如下:
对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M。
存储示例:
假设我们要存储浮点数 -5.75。首先将 -5.75 转换为二进制格式,并按照IEEE 754标准进行编码。
表示步骤:
符号位:-5.75 是负数,所以符号位是 1。
绝对值转换为二进制:5.75 的二进制表示是 101.11。
标准化:将 101.11 转换为标准化形式 1.0111 × 2^2。
阶码:IEEE 754 使用偏移量为 127 的阶码。在这里,实际阶码是 2,所以存储的阶码是 2 + 127 = 129,其二进制形式是 10000001。
尾数:标准化形式的尾数部分是 0111,补充至 23 位得到 01110000000000000000000。
因此,-5.75的32位表示为:
1 10000001 01110000000000000000000
2.2 双精度浮点数(64位)
双精度浮点数使用64位存储,其中包括:
- 符号位:1位
- 指数位:11位
- 尾数:52位(实际尾数有53位,因为有一个隐含的1位)
双精度浮点数的存储格式如下:
对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M 。
存储示例: 对于浮点数 -5.75
,转换步骤如下:
表示步骤:
符号位:-5.75 是负数,所以符号位是 1。
绝对值转换为二进制:5.75 的二进制表示是 101.11。
标准化:将 101.11 转换为标准化形式 1.0111 × 2^2。
阶码:IEEE 754 使用偏移量为 1023 的阶码。在这里,实际阶码是 2,所以存储的阶码是 2 + 1023 = 1025,其二进制形式是 10000000001。
尾数:标准化形式的尾数部分是 0111,补充至 52 位得到 0111000000000000000000000000000000000000000000000000。
最终,64 位双精度浮点数的表示为:
因此,-5.75的64位表示为:
1 10000000001 0111000000000000000000000000000000000000000000000000
3. 内存中的浮点数存储
浮点数在内存中的实际存储取决于系统的字节序(大端或小端)。例如,在大端系统中,较低位字节存储在较高内存地址。以下是如何查看浮点数在内存中的实际存储示例:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> // 将浮点数以大端格式打印 void print_memory_representation(void* p, size_t size) { unsigned char* byte = (unsigned char*)p; // 大端打印需要从高位字节开始 for (size_t i = size; i > 0; i--) { printf("%02X ", byte[i - 1]); } printf("\n"); } int main() { float f = -5.75; double d = -5.75; printf("Float value: %f\n", f); printf("Double value: %lf\n", d); printf("Float类型内存表示(大端):\n"); print_memory_representation(&f, sizeof(f)); printf("Double类型内存表示(大端):\n"); print_memory_representation(&d, sizeof(d)); return 0; }
在此代码中,print_memory_representation
函数将浮点数在内存中的每个字节打印为十六进制格式。运行代码可以观察到浮点数在内存中的具体存储方式。
4. 精度问题与误差
浮点数表示有限精度的实数,可能导致精度问题。例如,0.1
不能精确地用二进制表示,这会在浮点运算中引入微小的误差。因此,比较浮点数时,通常要考虑误差范围,使用一个接近的值进行比较而非直接等于比较。
题⽬解析
下⾯,让我们分析一开始的案例。
先看第1环节,为什么 9 还原成浮点数,就成了 0.000000 ? 9以整型的形式存储在内存中,得到如下⼆进制序列:
1 0000 0000 0000 0000 0000 0000 0000 1001
⾸先,将 9 的⼆进制序列按照浮点数的形式拆分,得到第⼀位符号位s=0,后⾯8位的指数
E=00000000 , 最后23位的有效数字M=000 0000 0000 0000 0000 1001。 由于指数E全为0,所以符合E为全0的情况。因此,浮点数V就写成:
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
显然,V是⼀个很⼩的接近于0的正数,所以⽤⼗进制⼩数表⽰就是0.000000。
再看第2环节,浮点数9.0,为什么整数打印是 1091567616
⾸先,浮点数9.0 等于⼆进制的1001.0,即换算成科学计数法是:1.001×2^3
所以: 那么,第⼀位的符号位S=0,有效数字M等于001后⾯再加20个0,凑满23位,指数E等于3+127=130, 即10000010
所以,写成⼆进制形式,应该是S+E+M,即 1 0 10000010 001 0000 0000 0000 0000 0000
这个32位的⼆进制数,被当做整数来解析的时候,就是整数在内存中的补码,原码正是
1091567616 。
总结
C语言中的浮点数存储是一个复杂而重要的主题。它涉及到符号位、指数位和尾数的详细布局,以及IEEE 754标准的规范。通过理解浮点数的存储机制,你可以更好地处理浮点数的计算和调试问题。希望本文对你理解C语言中的浮点数存储有所帮助。