1 整数在内存中的存储

前面讲到,整数在计算机中的存储是以补码形式存储的,其中正数和负数也有些许差别,正数的三码相同,负数的就不相同了,那么这里就涉及原码反码补码。

原码:直接把整数用二进制的方式表达出来的就是原码。

反码:原码除了符号位不变,数值位按位取反就是反码。

补码:反码加1。

对于整型来说,计算机存储的一律是补码,这是因为使用补码可以把符号位和数值位一并处理了。

同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是 相同的,不需要额外的硬件电路。

相互转化我们可以这样理解,比如我们计算3-5,也就是3+(-5)

int main(){//计算3 +-5//3的原码00000000000000000000000000000011//-5的原码10000000000000000000000000000101//-5的反码11111111111111111111111111111010//-5的补码11111111111111111111111111111011//补码相加之后//补码11111111111111111111111111111110//反码11111111111111111111111111111101//原码10000000000000000000000000000010//结果即是-2return 0;}

所以补码和原码相互转化,cpu只需要一个加法器就可以解决完了,不需要额外的减法器之类的。

小tips:补码取反加1就可以直接得到原码,不用挨个挨个倒退回去。


2 大小端字节序和字节序判断

整数在计算机的存储我们现在是了解了,现在我们来看具体细节,均以VS2022为例进行操作。

int main(){int a = 0x11223344;return 0;}

定义一个整型,给它16进制的数字11223344,可能没接触过计算机的人就会认为存进去的时候就是按照从左往右的顺序进行存储的,但是实际上并非如此,由内存中我们可以看到存储是从右往左存储的,所以现在了解什么是大小端字节序(讨论超过一个字节的数据存储)。

大小端:

大端存储模式:是指数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存 在内存的低地址处。

小端存储模式:是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存 在内存的⾼地址处。

像a的11223344,其中11就是高位字节,44就是低位字节,从1到4也就是从高到低,在VS2022中11高位字节存储在高地址处,所以vs2022是小端机器。

那如果让你判断一下该机器是大小端机器的话?如何操作呢?

int main(){int i = 0x1;char* pi = &i;if (1 == *pi){printf("小端\n");}else{printf("大端\n");}return 0;}

如上,我们只需要一个字节的内容,最开始的字节内容或者最后的字节内容就行,所以选择用char类型的指针来判断,如果解引用之后是高字节序的1在高地址处,那么就是小端存储,这是利用指针进行判断的,我们也可以使用联合体进行判断。

union Lab{int a;char b;}num;int main(){num.a = 1;if (1 == num.b){printf("小端");}else{printf("大端");}return 0;}

因为联合体里面的元素是共用空间的,所以我们创建一个整型,一个字符类型,使整型取1,接着就是判断第一个空间是不是1就行了。


3 整数存储练习题

代码1:

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

问:代码的运行结果是什么?

在vs里面char ,signed char类型是一样的所以两个的结果是一样的,因为-1的补码是32个1,截断8个bit位之后,就是11111111,进行整型提升,对符号位进行提升,最后也是32个1,所以a b的结果都是-1,那么,unsigned char类型的没有符号位,全是数值位,截断之后是11111111,提升之后是32个1,32个1也就是2^8 – 1,所以c是255。

代码2:

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

首先我们要知道char类型的数据(有符号的)的数值范围是-128——127,所以char a = 128是超出了数据范围的,那么a实际的值是-128(可以用%d验证一下),如果a是130,那么用%d打印出来就是-126,所以当数据范围超了之后的值其实就像一个轮盘一样,绕圈圈绕回来的。

既然a的数值是-128,占位符是%u,是无符号整型,所以会进行整型提升,因为-128比较特殊,它的二进制原码是1000 0000,所以整型提升之后,原码就是1111 1111 1111 1111 1111 1111 1000 0000,这就是原码,因为是无符号整型,所以三码是一样的,那么打印出来就是一个很庞大的数值,4294967168。
那么如果a是-128呢?因为截断之后的原码都是1000 0000,整型提升的结果也是一样的,所以最后的结果没有改变,都是4294967168。

代码3:

int main(){char a[1000];for (int i = 0; i < 1000; i++){a[i] = -1 - i;}printf("%zd", strlen(a));return 0;}

strlen,就是用来找0的,如果里面有个0,那么就停止了。数值的读入可以认为是一个循环,char类型的循环可以认为是-1 ……-128 127 126 ……0,可以认为是这样的一个循环,所以char a[1000]里面的数值也是这样的,那么从-1 到 0 一共有255个数,所以打印的结果就是255。

代码4:

unsigned char i = 0;// 0 -> 255int main(){for (i = 0; i <= 255; i++){printf("Hello world\n");}return 0;}

unsigned char的范围是0 – 255,那可能就容易以为循环次数是256次,就打印256个Hello world,但是实际上并非如此,如果要结束循环,那么i 的值就应该是256,但是i到256的时候,就变成了0,数值循环嘛,所以造成了死循环。

代码5:

int main(){for (unsigned int i = 9; i >= 0; i--){printf("%u\n", i);}return 0;}

有了代码4的理解,这段代码同理可得是一个死循环,因为unsigned int的值不可能为负数,所以循环会一直走下去。


4 浮点数在内存中的存储

int main(){int n = 9;float* pf = (float*)&n;printf("n的值为:%d\n", n);printf("*pf的值为:%f\n", *pf);*pf = 9.0;printf("n的值为:%d\n", n);printf("*pf的值为:%f\n", *pf);return 0;}

让我们带着问题去解决这个知识点——为什么差别会那么大?

首先我们要知道浮点数在内存中怎么存储的。

根据国际标准IEEE(电气电子工程师学会)754,任意一个二进制浮点数V可以表示为下面的形式:

V = (-1)^S * M * 2^E

其中(-1)^S表示符号位,当S等于1是,V就是负数,S为0时,V就是为整数,这其实和整数存储的符号位是很像的,1是负数,0是整数。M表示有效数字,M是大于等于1,小于2的(有点类似于科学计数法),2^E表示指数位。

举个栗子,十进制的5.0,二进制表示就是101.0,相当于1.01 * 2 ^ 2,按照V的格式,可以得出S = 0,M= 1.01,E= 2,有点像科学计数法吧?指数位的2^2就像是科学计数法里面的10的几次方一样,这就是二进制的科学计数法。十进制的-5.0,S = 1,M = 1. 01,E= 2。

根据IEEE 754规定,对于32位的浮点数,最高位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M,

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

当然,存储并不是那么简单就完成了的,IEEE 754对于有效数字M和指数E还有一些特别的规定。

因为M>=1&&M<2,也就是说M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分,IEEE 754规定,在计算机中存储M的时候,默认这个数第一位总是1,因此可以被舍去,只保留小数部分,比如保留1.01的时候,只保留01,等到读取的时候在把第一位加上去,这样做的好处是可以节省1位有效数字,以32位浮点数为例,留给M只有23位,但是舍去1之后,等于可以保留24位有效数字

关于E,首先它是一个无符号整数,这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我 们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存⼊内存时E的真实值必须再加上 ⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。⽐如,2^10的E是 10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

那么中间数存在的意义是什么呢?像前面讲char取值一样,数值取值就是一个循环,加一个中间数就可以表示负数了。

浮点数取的过程还分为3种情况:
E不全为0或全为1,比如表达0.5在内存中的存储,因为0.5的二进制位表达是0.1,所以是1.0*2^(-1),E是126,0111 1110,整数部分是0,补齐就是24个0,所以二进制表示就是

0 0111 1110 0000 0000 0000 0000 0000 0000。

E全为0,这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字。

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

ok关于浮点数的存储就说到这里,我们来看题目

为什么9用浮点数指针接收的时候,打印出来的结果就是0.000000了,9的二进制序列是

0000 0000 0000 0000 0000 0000 0000 1001

那么把它按照浮点数形式拆分的话,符号位为0,指数为0000 0000,剩下的23为有效数字是000 0000 0000 0000 0000 1001,这是指数全为0的情况, 所以V = (-1)^0 *000 0000 0000 0000 00001001*2^(-126) = 1.001 * 2 ^ (-146),这是一个接近于0的数,所以用十进制小数表示就是0.000000。

那么为什么用整数打印浮点数9.0是1091567616,首先浮点数9.0二进制是1001.0,即是1.001*2^3,所以9.0 = (-1)^0 * (1001) * 2^3,S = 0,M = 00100000000000000000000,E = 130,即使10000010,最后表达出来就是01000001000100000000000000000000,这是个32位的二进制的整数时,解析出来就是1091567616.

Tips : 不是所有的浮点数都可以表达出来的,君可自行实验。