整数以及浮点数在内存中的存储
一.整数在内存当中的存储
数据在内存中是以十六进制补码的形式进行存储的。
原码表示法简单易懂,适用于乘法,但用原码表示的数进行加减运算比较复杂,当两数相加时,如果同号则数值相加,但是进行减法时要先比较绝对值的大小,然后大数减去小数,最后还要给结果选择恰当的符号。
而负数用补码表示,加法运算只需要一个加法器就可以实现了,不用再配减法器,可以将符号位和数值域统一处理,此外补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
而1个十六进制可以表示4个二进制位,在内存中查看变量时,只用看(32/4)8位二进制代码即可
#include<stdio.h>
int main()
{
int hh = 15;
return 0;
}
再来看一下负数在内存中的情况
#include<stdio.h>
int main()
{
int hh = -15;
return 0;
}
此时显示的变量hh在内存中的情况为ff ff ff f1
原码hh=-15为10000000 00000000 00000000 00001111 负数最高符号位为1
反码为11111111 11111111 11111111 11110000
补码为11111111 11111111 11111111 11110001
按照一个十六进制位等于4个二进制,将补码可转变为 ff ff ff f1
大小端字序存储
什么是大小端字序存储
大小端字序存储其实就是字节在内存中存储时的存储顺序。
如果数据的低位字节内容保存在内存的高地址处,而高字节内容保存在内存的低地址处,那么就是大端存储模式
如果数据的高位字节内容保存在内存的高地址处,而低字节内容保存在低地址处,那么就是小端存储模式
比如15,它的十六进制补码为00 00 00 0f
而在vs这个编译器中存储的情况是这样的
编译器里默认左边是低地址,右边是高地址,而存储为00 00 00 00 0f,其实是按照低字节存低地址的规则来进行的,所以是小端存储。
代码判断大小端存储
#include<stdio.h>
int check_sys()
{
int i = 1;
int ret = (*(char*)&i);//先把i的地址转变为char*,然后解引用会得到内存中存储的十六进制字节序
return ret;//char *只能一个字节一个字节访问,此时访问的是第一个内存中的字节
}
int main()
{
int ret = check_sys();
if (ret == 1)//小端存储低字节存低位
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
整型提升
#include<stdio.h>
int main()
{
unsigned char a = 200;
unsigned char b = 100;
unsigned char c = 0;
c = a + b;
printf("%d %d", a + b, c);
return 0;
}
为什么这代码结果会不一样呢,这就涉及到了整型提升
C语⾔中整型算术运算总是⾄少以缺省整型类型的精度来进⾏的。
为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执⾏,CPU内整型运算器(ALU)的操作数的字节⻓度⼀
般就是int的字节⻓度,同时也是CPU的通⽤寄存器的⻓度。?
因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准⻓
度。
通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为int或unsigned int,然后才能送⼊CPU去执⾏运算。
再回到上面那个代码
c=a+b
a里面放的是200,200的原码本来应该是32位的00000000 00000000 00000000 11001000
正数原反补码相同都为00000000 00000000 00000000 11001000
放进只有8位的容器char中,发生截断就为1100 1000
b里面放的是100,100的原码本来应该是00000000 00000000 00000000 01100100
正数原反补码相同都为00000000 00000000 00000000 01100100
放进只有8位的容器char中,发生截断就为0110 0100
b+c直接相加为100101100,这个得到的是补码
此时以%d(32位有符号十进制原码)打印,要先把b+c补码的结果先填满为32位,00000000 00000000 00000001 00101100,然后再转换为原码,此时最高位为0默认为正数,原反补码都相同,所以补码也为00000000 00000000 00000001 00101100,转换为十进制为300,所以最后打印为300
再来考虑c=a+b,八位a和八位b直接相加为100101100,这是九位,要强行放进只有八位的容器c当中,所以会发生截断,最高位1会被截断了。所以c补码最后为00101100
现在要以%d(32位有符号十进制原码)打印,要先把c的补码填满为32位,00000000 00000000 00000000 00101100,正数转原码为000000000 00000000 00000000 00101100,转换为十进制为44,所以结果为44.
有符号的例子
#include<stdio.h>
int main()
{
char a;
char b = 1;
char c = -1;
a = b + c;
printf("%d %d", b+c, a);
}
结果是一样的,但是先别急,试着用上面的方法来分析一下
-1的原码为10000000 00000000 00000000 00000001
-1的反码为11111111 11111111 11111111 11111110
-1的补码为11111111 11111111 11111111 11111111
1的原码是00000000 00000000 00000000 00000001
这是32位整型的情况,而char类型只能放8位,所以就会发生截断,char c实际存放的是11111111
1是正数,原反补码都一样,都为00000000 00000000 00000000 00000001
这是32位整型的情况,而char类型只能放8位,所以就会发生截断,char b实际存放的是00000001
b+c直接以十进制原码的情况打印出来,所以b和c都要重新填满32位然后相加,这时候要去考虑有符号或者无符号数的情况:无符号数整型提升(填满32位),高位全补0;有符号数用符号位填满剩下的位数。
b为有符号数,补码正数符号位0填满为00000000 00000000 00000000 00000001
c为有符号负数,补码符号位1填满为11111111 11111111 11111111 11111111
补码直接相加为 100000000 00000000 00000000 00000000,是33位,%d打印要求32位整型打印,所以依旧会截断,最高位1截断了,得00000000 00000000 00000000 00000000,此时这得到的是补码,而此时最高位为0,所以是正数,正数原反补码相同,最后十进制打印就为0;
再来考虑一下a=b+c打印的情况,如上面说的char b实际存的是8位00000001,char c存的是8位11111111,直接b+c为100000000,九位数要放进只有8位的char a容器里,直接最高位截断了,即得a=b+c=00000000
而此时要把a以十进制原码打印出来,a不足32位,所以要填满32位,a为有符号数,最高符号位位为0,补满32位为00000000 00000000 00000000 00000000,最高位为0默认为正数,所以原码十进制为0,最后打印也为0;
无符号有符号整型提升串用的例子
#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);
}
同样的分析方式,a,b为有符号数,c为无符号数,打印结果为有符号十进制整型,%u是打印十进制无符号整型
-1的原码为10000000 00000000 00000000 00000001
-1的反码为11111111 11111111 11111111 11111110
-1的补码为11111111 11111111 11111111 11111111
而char a只能放8位,所以实际char a补码放的是11111111。此时要求输出十二位十进制原码,所以a需要整型提升填满,a是有符号数,此时最高位为1,补码填满为11111111 11111111 11111111 11111111,原码为10000000 00000000 00000000 00000001(符号位不变其余位取反加一),转换成十进制输出就是-1。
b同a一样,也是-1。
而char c补码实际存放的是11111111,c是无符号数,无符号数整型提升填满32位是高位全补0,即为00000000 00000000 00000000 11111111,此时最高位为0,要求打印%d类型的数据,会自动把c的最高位认为符号位,正数原反补码相同,所以原码为00000000 00000000 00000000 11111111,转换成十进制打印就是255.
有符号char和无符号char范围
无符号char最高位为有效位,最高位参与计算,所以范围为0到255(二进制为1111 1111)
也许有人疑惑,1000 0000反码不是1111 1111,加1原码为9位,截断一位应该为1000 0000啊,最高位为-1,结果不应该为-0吗
虽然8位二进制中,存在-0(1000 0000)和0(0000 0000),实际生活中0又没有正负的,-0不也是0吗,但是它却有两种补码表示,。为了将补码与数字一一对应起来,就认为把原码-0的补码强行人为表示为-128,所以八位二进制-128是没有反码和原码的。看见补码1000 0000,不用去按照负数求原码规则取反加一去计算,它直接表示就是-128,这是人为规定的规则
总结有符号char 的范围为-128到127
如果有符号数最大的正数数127(0111 1111)再加上1,会得到-128(1000 0000),而最大的负数-1(1111 1111)再加1其实得到0000 0000,形成一个环形
同样无符号数最大的数255加1就得到最小的数0,其实也是个环形
练习
#include<stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
32位-128,原码10000000 00000000 00000000 100000000
反码为11111111 1111111 1111111 01111111
原码为11111111 11111111 11111111 10000000
char a是个8位的容器,32位放进去会截断1000 0000,而接下来要以32无符号整型输出,8位数要重新填满32位,即为11111111 11111111 11111111 10000000,要以无符号数十进制输出,所以最高位1不是符号数,也要参与转换计算。即为4294967168
二.浮点数在内存中的存储
IEEE754标准
根据IEEE(国际电气和电子工程协会)754标准,任意一个浮点数可以写成下面的形式
十进制的浮点数5.0,写成二进制为101.0,相当于1.01 x 2^2
按照上面的格式,可得s=0,m=1.01,E=2;
IEEE754规定
对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
浮点数存的过程
IEEE754对有效数字M和指数E,还有⼀些特别规定。
前⾯说过M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表⽰⼩数部分。IEEE754规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。这样做的⽬的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字。
⾄于指数E,情况就⽐较复杂⾸先,E为⼀个⽆符号整数(unsigned int)这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。⽐如,2^10的E是 10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
举个存储的例子
0.5存储,二进制为0.1,规定正数部分必须为1,所以右移操作得1.0*2^(-1),这是正数所以s=0,
E的存储要加127(1023),-1+127=126,二进制表示为0111 1110.
此时M为1.0,按照规则要舍去1,只保留0,而M要存23位,bu'qi为
浮点数取的过程
浮点数取的过的过程可以分为三种情况
E的表示不全为0或者不全为1,这也是最常见的情况
这是浮点数取出E的规则是E的计算值减去127(或者1023),就是把前面存的时候加上的中间数减去,还原原数
E全为0的情况
这时,浮点数指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样是为了表示+-0,以及接近0的很小的数字
E全为1的情况
这时,如果有效数字M全为0,表示+-无穷大(正负取决于符号位s)
相关浮点数存储的例题
#include <stdio.h>
int main()
{
int n = 5;
float* p= (float*)&n;
printf("n的值为:%d\n", n);
printf("p的值为:%f\n", *p);
*p = 5.0;
printf("num的值为;%d\n", n);
printf("*p的值为;%f\n", *p);
}
为什么结果会这么奇怪
首先整数5的二进制型式为 00000000 00000000 00000000 00000101
这时候取地址然后强制转换为float *类型,并用浮点数类型指针p去保存它,此时解引用实际上是把上面那一串二进制数以浮点数的形式取出来
而浮点数形式要考虑占8位的E和占23位的M的值
实质就是以下图V的形式进行输出的
5的32位整数二进制形式 00000000 00000000 00000000 00000101
s是二进制序列中的第一位,所以s=0;
E为00000000 ;
M所属的剩下的23位为00000000 00000000 0000101
还原成V的形式,E在32位电脑下要先减去127,所以指数E为-127
而此时E全为0,所以M的1可以不用加
v=(-1)^0 x 0.00000000 00000000 0000101 x 2^(-127)=(-1)^0 x 1.01 x2^(-148)
此时V是非常接近0的数,极限逼近0,所以输出为0.000000
然后第二环节*p=5.0,是直接把n改写成浮点数的形式,此时要以整数的形式把它输出
浮点数5.0的二进制形式为0101.0,换算成科学计数法形式为1.01 x 2^(2)
按上图理解 s=0,M为1.01,E为2
E占8位,且要先+127,所以E表示为100000001
M的1要省略,有效数字为01,要补满23位,所以M为 01000000 00000000 0000000
写成二进制形式S+E+M形式
0 10000001 01000000 00000000 0000000
转换为十进制为1084227584
总的来说,如果一开始n为整数,而现在要用浮点数形式打印出来,实际是浮点数取的规则
如果一开始就为浮点数,那么以32位整数的形式打印出来,实际使用的是浮点数存的规则