整数以及浮点数在内存中的存储

一.整数在内存当中的存储

数据在内存中是以十六进制补码的形式进行存储的。

原码表示法简单易懂,适用于乘法,但用原码表示的数进行加减运算比较复杂,当两数相加时,如果同号则数值相加,但是进行减法时要先比较绝对值的大小,然后大数减去小数,最后还要给结果选择恰当的符号。

而负数用补码表示,加法运算只需要一个加法器就可以实现了,不用再配减法器,可以将符号位和数值域统一处理,此外补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

而1个十六进制可以表示4个二进制位,在内存中查看变量时,只用看(32/4)8位二进制代码即可

#include
int main()
{
	int hh = 15;
	return 0;
}

整数以及浮点数在内存中的存储_第1张图片

整数以及浮点数在内存中的存储_第2张图片

再来看一下负数在内存中的情况

#include
int main()
{
	int hh = -15;
	return 0;
}

整数以及浮点数在内存中的存储_第3张图片

此时显示的变量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这个编译器中存储的情况是这样的

整数以及浮点数在内存中的存储_第4张图片

编译器里默认左边是低地址,右边是高地址,而存储为00 00 00 00 0f,其实是按照低字节存低地址的规则来进行的,所以是小端存储。

代码判断大小端存储

#include
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;
}

整数以及浮点数在内存中的存储_第5张图片

整型提升

#include
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;
	
}

整数以及浮点数在内存中的存储_第6张图片

为什么这代码结果会不一样呢,这就涉及到了整型提升

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
int main()
{
	char a;
	char b = 1;
	char c = -1;
	a = b + c;
	printf("%d %d", b+c, a);
}

整数以及浮点数在内存中的存储_第7张图片

结果是一样的,但是先别急,试着用上面的方法来分析一下

-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
int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("a=%d,b=%d,c=%d", a, b, c);
}

整数以及浮点数在内存中的存储_第8张图片

同样的分析方式,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范围

整数以及浮点数在内存中的存储_第9张图片

无符号char最高位为有效位,最高位参与计算,所以范围为0到255(二进制为1111 1111)

整数以及浮点数在内存中的存储_第10张图片

也许有人疑惑,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,形成一个环形

整数以及浮点数在内存中的存储_第11张图片

同样无符号数最大的数255加1就得到最小的数0,其实也是个环形

整数以及浮点数在内存中的存储_第12张图片

练习

#include
int main()
{
	char a = -128;
	printf("%u\n", a);
	return 0;
}

整数以及浮点数在内存中的存储_第13张图片

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标准,任意一个浮点数可以写成下面的形式

整数以及浮点数在内存中的存储_第14张图片

十进制的浮点数5.0,写成二进制为101.0,相当于1.01 x 2^2

按照上面的格式,可得s=0,m=1.01,E=2;

IEEE754规定
对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M

整数以及浮点数在内存中的存储_第15张图片

对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M

整数以及浮点数在内存中的存储_第16张图片

浮点数存的过程


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 
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);
}

整数以及浮点数在内存中的存储_第17张图片

为什么结果会这么奇怪

首先整数5的二进制型式为 00000000 00000000 00000000 00000101

这时候取地址然后强制转换为float *类型,并用浮点数类型指针p去保存它,此时解引用实际上是把上面那一串二进制数以浮点数的形式取出来

而浮点数形式要考虑占8位的E和占23位的M的值

实质就是以下图V的形式进行输出的

整数以及浮点数在内存中的存储_第18张图片

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)

整数以及浮点数在内存中的存储_第19张图片

按上图理解 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位整数的形式打印出来,实际使用的是浮点数存的规则

你可能感兴趣的:(c语言,算法)