【C语言】数据在内存中的存储:从整数到浮点数的奥秘
前言
在计算机的世界里,数据的存储和表示是编程的基础。今天,我们就来深入探讨一下数据在内存中的存储方式,包括整数和浮点数的存储细节,以及大小端字节序的奥秘。这些内容不仅对理解计算机系统至关重要,还能帮助我们在实际编程中避免一些常见的陷阱。
一、整数在内存中的存储
整数的二进制表示方法有三种:原码、反码和补码。对于有符号整数,这三种表示方法都包含符号位和数值位两部分。符号位用 0 表示正数,用 1 表示负数,最高位是符号位,其余位是数值位。
正整数:原码、反码和补码都相同。
负整数:三种表示方法各不相同。
原码:直接将数值按正负数的形式翻译成二进制。
反码:原码的符号位不变,其他位按位取反。
补码:反码加 1。
在计算机系统中,整数是以补码的形式存储的。这是因为补码可以将符号位和数值位统一处理,同时加法和减法也可以统一处理(CPU 只有加法器)。此外,补码与原码之间的转换过程相同,不需要额外的硬件电路。
int main()
{//-1 10000000 00000000 00000000 00000001// 11111111 11111111 11111111 11111110// 11111111 11111111 11111111 11111111 -a补码char a = -1;signed char b = -1;// 11111111 11111111 11111111 11111111// 00000000 00000000 00000000 11111111// 00000000 00000000 00000000 11111111// 00000000 00000000 00000000 11111111unsigned char c = -1;printf("a = %d , b = %d , c = %d", a, b, c);return 0;
}
int main()
{char a = -128;//10000000 00000000 00000000 10000000//11111111 11111111 11111111 01111111//11111111 11111111 11111111 10000000//char占八个bit位,故截取8位,再补上符号位printf("%u\n", a);return 0;
}
int main()
{unsigned int num = -10;//10000000 00000000 000000000 00001010//11111111 11111111 111111111 11110101//11111111 11111111 111111111 11110110//1111 1111 1111 1111 1111 1111 1111 0110printf("%d\n", num);//-10printf("%u\n", num);//4294967286return 0;
}
二、大小端字节序和字节序判断
(一)什么是大小端?
对于超过一个字节的数据,存储顺序是一个关键问题。根据不同的存储顺序,分为大端字节序存储和小端字节序存储。
大端(存储)模式:数据的低位字节内容保存在内存的高地址处,高位字节内容保存在内存的低地址处。
小端(存储)模式:数据的低位字节内容保存在内存的低地址处,高位字节内容保存在内存的高地址处。
(二)为什么会有大小端?
在计算机系统中,数据是以字节为单位存储的,每个地址单元对应一个字节。对于位数大于 8 位的处理器(如 16 位或 32 位处理器),寄存器宽度大于一个字节,因此必然存在如何安排多个字节的问题。这导致了大端存储模式和小端存储模式的出现。
例如,一个 16 位的 short 类型变量 x,在内存中的地址为 0x0010,x 的值为 0x1122。其中,0x11 是高字节,0x22 是低字节。在大端模式下,0x11 放在低地址 0x0010 中,0x22 放在高地址 0x0011 中;而在小端模式下,顺序则相反。
常见的处理器架构中,X86 架构是小端模式,而 KEIL C51 是大端模式。许多 ARM 和 DSP 处理器是小端模式,有些 ARM 处理器还可以由硬件选择大端或小端模式。
(三)如何判断当前机器的字节序?
我们可以通过编写程序来判断当前机器的字节序。以下是两种常见的方法:
方法 1:通过指针类型转换
#include <stdio.h>int check_sys() {int i = 1;return (*(char *)&i);
}int main() {int ret = check_sys();if (ret == 1) {printf("小端\n");} else {printf("大端\n");}return 0;
}
方法 2:使用联合体
#include <stdio.h>int check_sys() {union {int i;char c;} un;un.i = 1;return un.c;
}int main() {int ret = check_sys();if (ret == 1) {printf("小端\n");} else {printf("大端\n");}return 0;
}
三、浮点数在内存中的存储
浮点数是计算机中表示实数的一种方式,常见的浮点数类型包括 float、double 和 long double。浮点数的表示范围在 <float.h> 中定义。
(一)浮点数的存储格式
根据 IEEE 754 标准,任意一个二进制浮点数 V 可以表示为:
V=(−1) ^S ×M×2 ^ E
其中:
S 表示符号位,S = 0 时为正数,S = 1 时为负数。
M 表示有效数字,范围为 [1,2)。
E 表示指数位。
对于 32 位浮点数:
最高 1 位存储符号位 S。
接下来的 8 位存储指数 E。
剩下的 23 位存储有效数字 M。
对于 64 位浮点数:
最高 1 位存储符号位 S。
接下来的 11 位存储指数 E。
剩下的 52 位存储有效数字 M。
(二)浮点数存储过程中的特殊规定
有效数字 M:由于 1 ≤ M < 2,M 可以表示为 1.xxxxxx 的形式。IEEE 754 规定,在计算机内部保存 M 时,默认第一位总是 1,因此可以省略,只保存小数部分。这样可以节省一位有效数字。
指数 E:E 是一个无符号整数。为了表示负指数,IEEE 754 规定在存储时,E 的真实值必须加上一个中间数。对于 8 位的 E,中间数是 127;对于 11 位的 E,中间数是 1023。
(三)浮点数取值过程
从内存中取出指数 E 时,有以下三种情况:
E 不全为 0 或不全为 1:
浮点数采用规则:指数 E 的计算值减去中间数(127 或 1023),得到真实值,再将有效数字 M 前加上第一位的 1。
例如:0.5 的二进制形式为 0.1,转换为 1.0 × 2^(-1),阶码为 -1 + 127 = 126,表示为 01111110,尾数为 0,补齐到 23 位。
E 全为 0:
浮点数的指数 E 等于 1 - 127(或 1 - 1023),有效数字 M 不再加上第一位的 1,而是还原为 0.xxxxxx 的小数。这种情况用于表示 ±0 和接近于 0 的很小的数字。
E 全为 1:
如果有效数字 M 全为 0,表示 ±∞(正负取决于符号位 S)。
(四)浮点数存储的实例解析
示例 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("n的值为:%d\n", n);printf("*pFloat的值为:%f\n", *pFloat);return 0;
}
解析:
9 以整型形式存储在内存中,二进制序列为 0000 0000 0000 0000 0000 0000 0000 1001。
按照浮点数格式拆分,符号位 S = 0,指数 E = 00000000,有效数字 M = 000000000000000000001001。
由于指数 E 全为 0,浮点数 V = (-1)^0 × 0.00000000000000000001001 ×
2^(-126) ≈ 0.000000。
示例 2:浮点数转换为整数
浮点数 9.0 等于二进制的 1001.0,即科学计数法 1.001 × 2^3。
符号位 S = 0,有效数字 M = 00100000000000000000000,指数 E = 3 + 127 = 130,即 10000010。
二进制形式为 0 10000010 00100000000000000000000,作为整数解析时,结果为 1091567616。
四、总结
今天我们一起探讨了数据在内存中的存储方式,包括整数的原码、反码和补码,大小端字节序的原理和判断方法,以及浮点数的存储格式和转换规则。这些知识是计算机科学的基础,也是每一位程序员都应该掌握的内容。希望这篇文章能帮助你更好地理解数据存储的奥秘,让你在编程的道路上更加得心应手!
如果你对这些内容还有疑问,或者有更多想要了解的知识,欢迎在评论区留言,我们一起交流探讨!