目录
一、整数在内存中的存储
整数的二进制有原码、反码、补码三种形式,在内存中是以补码的形式存储的。详细讲解请看 http://t.csdnimg.cn/qqGDz 。
二、大小端字节序
(1)什么是大小端字节序
内存中以一个字节为单元,超过一个字节的数据(short、int、long)在内存中存储,必然就有存储顺序的问题。另外,对于大于8位的处理器,例如16位、32位处理器,由于寄存器宽度大于一个字节,也会存在多个字节的存储顺序的问题。
在内存中数据是以二进制存储的,这里为了方便书写和观察,用十六进制表示。比如十六进制 0x11223344,每两个十六进制数,就代表一个字节,可以用以下的顺序在内存中存储:
11 22 33 44、11 33 22 44、11 22 44 33、44 33 22 11……,只要是以一个字节为整体,什么顺序都可以,存进去了能复原就行。但是对于复原,明显是正序的 11 22 33 44 和逆序的 44 33 22 11 从内存中取出来时,更容易复原。因此,就采用了这两种顺序,并取了名字:
- 大端(存储)模式:数据的低位字节的内容存放在内存的高地址处;高位字节的内容存放在内存的低地址处。(正序存放)
- 小端(存储)模式:数据的低位字节的内容存放在内存的低地址处;高位字节的内容存放在内存的高地址处。(逆序存放)
我们常用的X86结构(比如Intel处理器)采用的小端模式;KEIL C51(单片机、嵌入式)采用的大端模式;大部分ARM、DSP 采用小端模式;有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
(2)用程序判断当前机器的字节序
分析:对于数字 0x0001,在小端模式中是以 01 00 00 00 的顺序存储的(最低地址单元存放的是 01);大端模式中是以 00 00 00 01 的顺序存储的(最低地址单元存放的是 00)。因此,我们只需要取出最低地址单元的内容,就能判断是小端还是大端模式。
实际上对于一个 int 类型的变量,对其取地址,这个地址是最低字节单元的地址。然后要把取得的地址强制转换成 char*,这样在解引用的时候,才会得到一个字节的内容,而不是四个字节的内容。
(3)练习题
数据类型的作用:
- 申请内存空间。(比如:char是1字节、short是2字节、int是4字节。)
- 告诉如何看待内存中存放的数据。(比如:int 和 float 都是4个字节,如何区分内存中存放的的二进制数,就看它们的数据类型。)
① 练习1
int main()
{
char a = -1;
// -1 默认为 int 型
// 10000000 00000000 00000000 00000001 -- 原码
// 11111111 11111111 11111111 11111110 -- 反码
// 11111111 11111111 11111111 11111111 -- 补码
// char 只能装 1个字节,截断,保留低位
// 11111111 -- 补码
// VS中默认char就是signed char
signed char b = -1;
unsigned char c = -1;
// 占位符是%d,用有符号整型看待内存中的数
// 首先整型提升
// 11111111 11111111 11111111 11111111 -- 有符号数,高位补符号位 -- 补码
// 00000000 00000000 00000000 11111111 -- 无符号数,高位补0 -- 补码
// 然后把他们看作有符号数
// 11111111 11111111 11111111 11111111 -- 真值为-1
// 00000000 00000000 00000000 11111111 -- 真值为255
printf("a=%d,b=%d,c=%d", a, b, c);
return 0;
}
② 练习2
int main()
{
char a = -128;
// -128 默认为 int 型
// 10000000 00000000 00000000 10000000 -- 原码
// 11111111 11111111 11111111 01111111 -- 反码
// 11111111 11111111 11111111 10000000 -- 补码
// 10000000 -- 补码 -- 截断
// 占位符%u,无符号整型,整型提升
// 整型提升
// 11111111 11111111 11111111 10000000 -- 有符号数,高位补符号位
// 看作无符号数打印
// 真值为 4294967168
printf("%u\n", a);
return 0;
}
int main()
{
char a = 128;
// 00000000 00000000 00000000 10000000 -- 原、反、补码
// 10000000 -- 截断
// %u,无符号整型
// 整型提升
// 11111111 11111111 11111111 10000000 -- 有符号数,高位补符号位
// 看作无符号数
// 4294967168
printf("%u\n", a);
return 0;
}
③ 练习3
int main() {
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i; // -1,-2,-3,-4……-1000,但char的取值范围没这么大
}
printf("%d", strlen(a));
return 0;
}
singed char 在内存中的取值范围:-128~127
unsined char 在内存中的取值范围:
char在VS中默认为 signed char ,因此每次循环内的 -1 - i 的取值是:-1,-2,……,-128,127,126,……,2,1,0(停止符)。字符串长度是:128 + 127 = 255。
④ 练习4
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
因为 i 为 unsigned char 类型,所以 i 的取值范围是 0~255,始终满足循环的判断条件,因此会陷入死循环。
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
因为 i 为 unsigned int 类型,取值始终大于等于0,因此也会陷入死循环。
⑤ 练习5
//X86环境 ⼩端字节序
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
return 0;
}
&a是数组的地址,加1就到了数组结尾的地址,再赋值给ptr1强制转换为 int*,就是整型元素的地址,ptr1-1指向4。
a是数组首元素的地址,强制转换为int,加1就相当于把a这个地址值+1,实际上就是加了一个字节单元。数组a的十六进制小端字节序是 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00,那么 (int)a+1 就会指向01后面的一个字节,再强制转换为 int*,向后读取4个字节的数据为 00 00 00 02,书写的十六进制是 02 00 00 00,这就是ptr2指向的内容。
对于%p,输出的是十六进制地址,不会有0x,但是会保留地址前面的0;对于%x,不会有0x,也不会保留十六进制前面的0;想输出有x0的十六进制,占位符得是%#x。
因此,对于上面的代码,运行结果应该是 4,2000000。
三、浮点数在内存中的存储
浮点数形式:3.14159、1E10(科学计数法)等。
浮点数类型:float、double、long double。
浮点数范围:最小值、最大值、精度,见头文件 float.h。
看以下代码,变量 *pFloat 、n 分别以浮点数、整数的形式存取数据,会发现读取出的数值不同,这是因为它们在内存中的存储方式不同:
浮点数在内存中的表示:使用国际标准IEEE(电气和电子工程协会)754。
(1)国际标准 IEEE 754
① 转换形式
二进制浮点数 V 转换为 的形式。
表示符号位,当S=0, V为正数;当S=1, V为负数。
- M 表示有效数字,1 ≤ M < 2。
表示指数位。
举例:十进制 -5.5, 写成二进制 -101.1, 相当于 - ,改为 V 的格式 ,那么 S = 1,M = 1.011,E = 2。
② 浮点数在内存中的空间分配
float:共32位,符号位S(1位)+ 指数E(8位),有效数字M(32位);
double:共64位,符号位S(1位)+ 指数E(11位),有效数字M(52位)。
③ 有效数字M的特别规定
因为 ,所以 M 可以写成 1.xxxxx 的形式,1 是固定有的,为了保存更多位的有效数字,会省去整数部分的 1,只把后面的小数位保存到内存。从内存读的时候,再把整数 1 加上。例如,M = 1.011,只存储011。
④ 指数E的特别规定
E是一个无符号整数,那么如果 E 为 8 位取值范围是 0~255;E 为 11 位取值范围是 0~2047。但是 E 在科学计数法中存在负数,为了表示负数,E 会加一个中间值,让负的变成正的。如果 E 为 8 位,中间值是 127;E 为 11 位,中间值是 1023。读取的时候,E 加一个中间值即可。例如,8位 E=10,那么存储的值为 10 + 127 = 137,即 10001001。
(2)从内存中读取浮点数
① E不全为0或不全为1
按正常的来,读取到的 E 减去中间值,M 补上整数部分的1。
② E全为0
这时浮点数的真实值为 ± 0 或者很接近 0 的数。E 等于 1-127,M 不再补1,而是 0.xxxx。
③ E全为1
若 M 全0,浮点数真实值表示 ± 无穷;若 M 非全 0,浮点数真实值表示 NaN(非数值,如 0 除以、无穷 - 无穷)。
(3)对开头题目的解析
ps:在内存中整型数据以补码形式存储。
(4)浮点数的比较
二进制小数表示的十进制:
浮点数在计算机中难以精确表示,比如十进制 3.14 = 3 + 0.125 + …… = 11.001……,会发现永远凑不够,并且浮点数的 M 位的存储空间也有限。因此,浮点数的比较不能直接用 == (会让比较结果不准确),如下所示(0.1 和 0.2 的二进制小数位是无限循环的):
更准确的判断方法:将两者做差的绝对值,若绝对值小于一个可容忍的误差值,则视为两者相等,如下图所示: