数据的存储(主讲整型和浮点型)
每一小部分后面都有大量的面试题,为了你进行加深巩固知识,另外提醒一下,char其实本质上是整型,因为他的实现是 ASCLL码值
下文有:
整型存储 | 浮点存储 |
---|---|
原码,补码,反码 | 试题精讲 |
面试题和大小端存储 |
无符号数 (一定是大于等于0) 原码 反码 补码相同
有符号数
例如 a = 8, b= -9,问a+b=??? 虽然我们知道是-1 但是怎么来的呢???
a的补码 00000000 00000000 00000000 00001000 (原,反,补相同)
b的原码 10000000 00000000 00000000 00001001(最高位是符号位,1表示负数)
b的反码 11111111 11111111 11111111 11110110(除了最高位,其余按位取反)
b的补码 11111111 11111111 11111111 11110111
所以a的补码加 b的补码
00000000 00000000 00000000 00001000
11111111 11111111 11111111 11110111
11111111 11111111 11111111 11111111 (这个在还原回原码就是 10000000 00000000 00000000 00000001 最高位是1,所以是**-1**)
提醒:
对于32位的整型数字 每4位对应16进制的一位数字
例子:
1101 0010 1010 0001 0110 0111 1111 1000
D 2 A 1 6 7 F 8
所以上面32位换成16进制是 0xD2A167F8
下面我以8位为例子讲解下补码的规律,其16位,32位自推导
可以看到.
如果是有符号数,在右边从 0 1 2 3 …127 后就突然变成 -128 -127 -126 …-1,当-1的补码加一以后变成 1 0000 0000,变成9位,溢出,所以保留后8位 0000 0000 就是0,也就是说 其实计算机的计算是在打转 从 0到127 , -128到0, 0到127…
如果是无符号数,在右边从 0 1 2 3 …127 进去左边后128 129 …255,255的补码1111 1111 加一以后也是 1 0000 0000变成9位,溢出,保留后8位,0000 0000,就是0,也就是说 其实计算机的计算是在打转 0-255 …0-255 …
#include
int main()
{
char a = -1;
signed char b = -1;
unsigned char c= -1;
printf("%d %d %d", a, b, c);
return 0;
}
答案: -1 -1 255
解析:
char只能存储一个字节,并且是有符号位,那么说明 a∈[-128,127]; b∈[-128,127];
c是无符号位,说明c∈[0,255];
-1的补码是 11111111 11111111 11111111 11111111
因为a,b是有符号位,且只能存储一个字节,所以发生 截断,只能存最后8位 11111111
c是无符号位,但是也只能存储一个字节,也会发生截断,也只能存最后8位 11111111
最后输出的时候,%d都是输出整数的格式,所以在输出的时候又会发生整型提升,
因为a,b是有符号数,所以整型提升是按照符号位来的,往前面补符号位1, 变成 11111111 11111111 11111111 11111111
因为c是无符号数,所以整型提升是按照最往前补0,所以变成 00000000 00000000 00000000 11111111
因为a,b都是负数补码,所以还原回去输出, a,b是-1 ,c是无符号数,不用还原,就是该值c是255
(不懂截断和整型提升的,请看这里:操作符)
#include
int main()
{
char a = -128;
printf("%u",a);
return 0;
}
答案是4294967168,请看下面解析:
-128是有符号数,所以我们一步一步的从原码变成补码,看下面:
原码:10000000 00000000 00000000 10000000(最高位是1,代表负号)
反码:11111111 11111111 11111111 01111111
补码:11111111 11111111 11111111 10000000
因为a是有符号整型家族,且a只能存储8位(一个字节),所以会发生截断,只保留补码最后8位,10000000
但是输出的时候,输出格式是%u,即无符号32位,但是a是有符号,且只有8位,所以需要整型提升,有符号数整型提升最高位补符号位,
所以整型提升以后是 11111111 11111111 11111111 10000000,%u输出时候,就把这个32位的数字当成正数,不用还原回原码,直接输出, 而11111111 11111111 11111111 10000000 的值就是 4294967168
#include
int main()
{
char a = 128;
printf("%u ", a);
printf("%d ", a);
return 0;
}
答案: 4294967168, -128请看下面解析
因为128是正数,它原反补相同,在最开始我画的图知道,128的补码是 00000000 00000000 00000000 10000000
a只能存8位,发生截断,所以存入a的是后8位10000000,但是需要输出%u和%d,a是有符号数且只有8位,所以发生整型提升,
最高位补符号位 变成 11111111 11111111 11111111 10000000,
因为**%u要求无符号**,所以把该整型提升后的认作无符号数,直接输出,就是 4294967168
%d要求有符号,所以认为该数是有符号数,就把他还原回原码输出,即10000000 00000000 00000000 10000000 所以输出-128
#include
int main()
{
int i = -20;
unsigned int j = 10;
unsigned int m = -10;
printf("%d ", i+j);
printf("%u ",i+m);
return 0;
}
答案是:-10 4294967266,看解析:
-20的补码是 11111111 11111111 11111111 11101100
10的补码就是原码 00000000 00000000 00000000 00001010
-10的补码是 11111111 11111111 11111111 11110110
int是32位有符号数,所以i完全存入**-20**的补码 11111111 11111111 11111111 11101100
unsigned int 是无符号数,所以j存入的是10的补码: 00000000 00000000 00000000 00001010
m存入的**-10**的补码,但是m就把-10补码当成一个正数了,没有符号位,完全就是一个正数,不会还原回补码
所以i+j(补码加补码)是 11111111 11111111 11111111 11110110,因为%d要求输出有符号,于是认为该数是有符号的,最高位是1,代表符号,还原回原码就是 -10(前面也看到-10的补码就是它)
所以i+m(补码加补码)是 1 11111111 11111111 11111111 11100010 变成33位,溢出舍去最高33位,
剩下11111111 11111111 11111111 11100010 ,而这个正是正数4294967266的补码
因为%u就是要求输出无符号,所以认为这是正数,就输出4294967266
#include
int main()
{
unsigned int i = 9;
for (i= 9;i>=0;i--)
{
printf("%d\n", i);
}
return 0;
}
答案: 9 8 7 …0 死循环,解析:
因为i是无符号的数,我前面画过一张图,就是那个圆圈,如果是无符号的数会从 0到255然后又是0到255,
这里i是32位,同理,会从0到 2^32 - 1又到0,然后2^32 - 1…i就一直这样下去,所以会无限打印.
#include
int main()
{
char a[1000];
int i;
for (i= 0;i<1000;i++)
{
a[i] = -1 - i;
}
printf("%d",strlen(a));
return 0;
}
答案:255,解析:
char数组里面最大只能存放255,我们可以看到的是 依次存放的还是-1 -2 -3 …
这个顺序正是我前面画的那个椭圆的逆时针方向转动,所以会按照这样循环 -1 -2 -3 …-127 -128 127 126 125…3 2 1 0 -1 -2 -3…
但是strlen()查找字符串找到’\0’就停止了,而’\0’的值是0,所以当第一次找到0时,就停止.而 -1到0有255个数字,所以长度是 255
#include
int main()
{
unsigned char i;
for (i= 0;i<=255;i++)
{
printf("%d ",i);
}
return 0;
}
答案:死循环,解析:
i是无符号,而循环条件是255截止,但是i会从255又变会0,(请看前面的圆),所以无限循环
这里解释下什么是大端和小端
大端: 是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端: 是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
我们知道计算机存储数据是二进制,但是我们人类不方便看,于是我们把它转换为16进制显示,以便观察
假设
a = 0Xaf25bd3b
,我以图进行演示大端和小端怎么存储的你可能会问,为什么是两个数字一起一起的存???在最上面我说过,16进制的数字,一个数字代表2进制的4位,2个就是一个字节,
而内存最小空间是一个字节,所以是两个两个的数字存储.
vs演示
可以看到,i的存储是什么模式??? 对,小端模式.
#include
int check_sys()
{
int a = 1;
char* p = (char*)&a; /*之所以选择字符指针是因为它的访问权限是 一个字节*/
if(*p == 1)
return 1;
return 0;
}
/*
上面的函数还可以优化
写成这样:
int a = 1;
char* p = (char*)&a;
return *p; 因为*p的值只有1或者0
而更进一步优化是这样
int a = 1;
return *(char*)&a;
*/
int main()
{
if(check_sys())
printf("小端");
else
printf("大端");
return 0;
}
我们首先来看一个题
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
输出的值是什么??我们思考一下 是不是 9 9.0 9 9.0???
我们看看结果:
好像只有第一个和最后一个对了,怎么回事???我们先不解答,我们先搞懂浮点数的存储模式
(-1)^S * M * 2^E
(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。
什么意思呢???我们以6.0为例 可以表示成二进制 110.0
110.0可以写成1.10*2^2 那么E就是2,M就是1.10
而6.0完整的写法就是(-1) * 0 * 1.10 * 2 ^ 2
所以说可以写成 (-1)^S * M * 2^E,其中s = 0,m = 1.10,e = 2
同理,例如5.0就是101.0 ===> (-1)^1 * 1.01 * 2 * 2 ,其中s = 1,m = 1.01,e = 2
IEEE754规定: 对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
EEE 754规定,在计算机内部保存M时,因为这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。
比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。
以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
首先,E为一个无符号整数unsigned int这意味着,如果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。
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前
加上第一位的1。 比如: 0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,
则为1.0*2^(-1),其阶码e的存储就为-1+127=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位(补0是从最后面补)
00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000
符号s 质数e 科学数M
这时,浮点数的指数E规定等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为
0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);
好了,关于浮点数的表示规则,就说到这里。
说了这么多还不如实际操作一遍,我们存储8.825试试
8.825表示为二进制形式是1000.111,又是正数,所以S= 0
1000.111====>1.000111*2^3 所以M = 1.000111 E = 3
但是存储的时候M只存小数后000111,E的存储值是 3+ 127 = 130 130的二进制是 10000010
所以s存0 E存10000010 M存000111,而M是存在23位空间,后面不足的补0
那么32位上存得就是
01000001000011100000000000000000
表示成16进制是
41 0E 00 00
经过计算,果然是这样
现在回到最开始那个题
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
9的整数二进制是 00000000 00000000 00000000 00001001
第一次打印n,结果是9,正确
第二次打印*pfloat,因为是浮点指针,所以它就把9的二进制认为是浮点存储数据,所以提取出来的就是 s = 0,而e全位0,则e是1-127 = -126,m = 0000000 00000000 00001001,因为e全0,所以还原回去,M整数位就不加1.
写成IEEE标准就是 (-1)^0 * 0.0000000 00000000 00001001 * 2 ^ -126 可以知道这个数值无限小,而float只打印小数位6位,所以
是0.000000
第三次*pfloat等于9.0 1.001 * 2^3 所以s = 0,m = 1.001,e=3
所以s存0 m存001,不足从后补0 e存3+127 = 130 即10000010
0 10000010 00100000000000000000000
打印n的时候,以为是正数,而这个0 10000010 00100000000000000000000等于 1091567616
第四次打印的时候,0 10000010 00100000000000000000000,这个就是浮点存储,所以还原回去就是9.00000000