今天我要讲讲数据在内存中的存储形式,分为三个板块:
1. 整数在内存中的存储
2. ⼤⼩端字节序和字节序判断
3. 浮点数在内存中的存储
整数的二进制的表达方法其实有三种:原码,反码,补码
有符号整数的三种表达方法中均有符号位和数值位,符号位是用0来表示正,1来表示负
最高的一位被当为符号位,其余的都是数值位;
正整数的原码,反码,补码都相同
负整数的原码,反码,补码都各不同
原码:直接将整数按照正负数的形式转换为二进制得到的就是原码;
反码:将原码的符号位不变,数值为都按位取反得到的就是反码;
补码:反码+1,得到的就是补码;
对于整型来说:数据存放在内存中其实存放的是二进制的补码,原因如下:
1.在计算机系统中,数值⼀律⽤补码来表⽰和存储。
2.原因在于,使⽤补码,可以将符号位和数值域统⼀处理;
3.同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,
补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
了解了以上,我们来看这个代码:
#include <stdio.h>
int main()
{
int a = 0x11223344;
return 0;
}这串代码调试后,我们会看到:

可以看到,这个数字是按照字节为单位,倒着存储的,这是为什么呢?
这时候我们就需要知道什么是大小端:
其实超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为⼤端字节序存储和⼩端字节序存储,下⾯是具体的概念:
⼤端(存储)模式:
是指数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存在内存的低地址处。
⼩端(存储)模式:
是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存在内存的⾼地址处。
原因:
因为存储字节的方式有很多,所以就分为两种方式:大端或小端
相当于统一度量衡;
了解了这些之后,接下来我们做几道练习题吧:
练习题1:
#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;
}如图,我们知道signd char的取值范围是-128~127,unsigned char的取值范围是0~255;
题中char a和signed char b其实是等效的,都是有符号型,取值范围-128~127,负一的二进制序列为: 10000000 00000000 00000000 00000001,这是原码;
负数的话,补码就是原码取反,+1
取反:11111111 11111111 11111111 11111110
+1 : 11111111 11111111 11111111 11111111;
然后就得到了补码 由于char类型只能放得下一个字节,会发生截断,取的是低位的字节,即:
11111111;
然后,打印是用%d打印,%d是打印10进制的有符号整型,则会发生整型升级,填充的是符号位的数字,也就是1: 11111111 11111111 11111111 11111111,这就是升级后的补码;
因为是负数,现在需要把补码转化为原码,转化过程为取反,+1:
取反:10000000 00000000 00000000 00000000(符号位不变)
+1: 10000000 00000000 00000000 00000001
这就是最终的原码,则输出的结果就是-1
这就是char a用%d输出的值,同理signed char b也是。
接下来看看unsigned char c:
虽然是unsigned ,无符号型,但是呢,我们强行把-1塞进去,这是改变不了的,所以我们照样先分析-1:
上面写过,所以我们抄下来,-1的补码是:11111111 11111111 11111111 11111111
同理,会截断,则得到: 11111111;
然后%d打印,同样是整型升级。但是注意!这是无符号整型,默认最高位是0,无负数!所以填充时,全部填0,则得到: 00000000 00000000 00000000 11111111;
显然,这是个正数,则原码即补码,所以直接输出值,答案就是:255;
这就是这道题。
练习题2
#include <stdio.h>
int main()
{
char a = -128;
char b = 128;
printf("%u\n", a);
printf("%u\n", b);
return 0;
}我们看看这题。首先,我们计算一下a:
a的原码:10000000 00000000 00000000 10000000;
取反加1后可得到: 11111111 11111111 11111111 10000000;这就是补码;
同样,发生截断,截取低位字节:10000000;
然后用%u打印,%u是打印十进制的无符号整型.char 中存不下128,强制存放会变成-128,所以符号位是1,用%u打印,整型升级填符号位,所有全部填1:
11111111 11111111 11111111 10000000,即一个非常大的正数:4294967168
,同理,char b是-128,和上面一样:11111111 11111111 11111111 10000000,即4294967168
那,为什么128会变成-128呢?看图:

如图。128就在-128的位置,这是char类型的变化,同理,129就是-127了,255就是-1,256就是0,数字继续增大,就会继续循环。知道这个后,我们来看看下面这练习题3:
int main()
{
char a[1000];
int i;
for(i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}如图,i 会随着for循环一直增大直到999,既然知道了上面的变化规律,那么这题就会变得很简
单,当i增大到254时,即arr[253],也就是第255个元素,得到了-256,-256的规律无非就是反过来
绕圈,那么结果还是0,意思就是在第255个元素时,里面填的是\0,那么后面的元素都不用看了,
srelen读取到\0就停止,那么输出的结果就是255,这就是这道题。
接下来,看这道最难的练习题:
#include <stdio.h>
//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;
}如图,32位环境下,小端字节序:
我们直接看这句:int *ptr1 = (int *)(&a + 1); &a代表取了整个数组的地址,那么&a+1就是代表跳过了整个数组,指向了最后一个元素的下一个元素:

如图所示,指向4后面。因为是数组的地址,是int (*)[4]类型的,所以要强制类型转换为int*,然后让指针ptr1指向这个地址。
那么打印时用%x,即以十六进制形式打印无符号整数,打印的是ptr1[-1],我们知道,这个的意思是
*(prt1-1),那么就是指针向前走一位置,指向4的开头,然后解引用指针,指针向后访问,得到4;4用%x打印就是0x4(或者0x 00 00 00 04),最后输出结果省略0x,那么结果就是4.
再看:int *ptr2 = (int *)((int)a + 1); 老样子,先从内层看起,a是数组名,将a强制类型转换为int类型,然后+1,意思就是:首元素地址(假设为0x11223344),转化为int类型,那么+1就是直接在地址的基础上+1,那么就得到了0x11223345,意思是指向了首元素的第2个字节,而非第2个元素,如图:

(int)a+1指向了首元素的第二个字节,因为是小端字节序存储,所以00 00 00 01会反过来排列:01 00 00 00 ,那么这时候再*ptr2,就会在这个基础上向后访问四个字节,即:00 00 00 02
那么最后输出的结果就是:00000002;
这就是所有的练习题,接下来我们来讲浮点数:
根据国际标准IEEE(电⽓和电⼦⼯程协会)754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
V = (−1) S ∗ M ∗ 2 E(其中,S,E都是指数)(1<=M<2)
• (−1) S 表⽰符号位,当S=0,V为正数;当S=1,V为负数
•
M 表⽰有效数字,M是⼤于等于1,⼩于2的
•
2 E 表⽰指数位
举例子:5.0的二进制表示就是101.0,,相当于1.01* 2 2,可以得出S=0,M=1.01,E=2
反过来:⼗进制的-5.0,写成⼆进制是 -101.0 ,相当于-1.01* 2 2 ;那么,S=1,M=1.01,E=2
注意:
对于32位的浮点数(float),最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数(double),最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
M:
由于m大于等于1小于2,所以存储浮点数时,默认这个数字第一位总是1,因此可以被舍去,只保留小数位数,等到计算完所有后再加上那个1。这样做的目的是:节省1位有效数字;
以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字,精度更高了
E:
⾸先,E为⼀个⽆符号整数.他的代表指数位。如果E为8,则取值范围就是0-255,E为11,就是
0-2047。因为科学计数法,指数位是可以为负的,但是负数不好以二进制形式存入内存,于IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
⽐如, 2 10 的E是10,所以保存成32位浮点数(8位放E)时,必须保存成10+127=137,即10001001。
这样的浮点数存储⽅式很巧妙,但是我们也要注意到有的浮点数是⽆法精确保存的。⽐如:1.2,我们可以在VS上调试看⼀下,我们发现会有些许误差:

这是为什么呢?因为1的二进制序列就是1,1.5就是 1.1, 1.25的二进制就是1.01 那1.225就是 1.001,我们发现,无论后面接多少个1,这个二进制序列永远表达出1,2,那么到最后就会出现一个非常接近1.2的数字,这就是误差的来源。
E全为0:
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字

图中有解释。
E全为1:
这时,如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s)

解释如图
E不全为0或1:
这是常规情况
这时,浮点数就采⽤下⾯的规则表⽰,即指数E的计算值减去127(或1023),得到真实值,再将
有效数字M前加上第⼀位的1。⽐如:0.5 的⼆进制形式为0.1,由于规定正数部分必须为1,即将⼩
数点右移1位,则为1.0* ,其 阶码为-1+127(中间值) = 126,表⽰为01111110,⽽尾数1.0去掉整数
部分为0,补⻬0到23位 0000000000000000000000,则其二进制码为:
1
0 10000010 00100000000000000000000