文章目录
1.整型的储存形式:原码反码补码
一个整数的二进制表示方式有3种。分别是源码,反码和补码。
- 源码是数字本身的二进制序列。例如4的源码就是0100.
- 反码是对源码的除符号位外(序列的第一位是符号位,0表示整数,1表示负数的每一个二进制位进行按位取反。(0变成1,1变成0)
- 补码是对反码的二进制序列+1
由于正数(无符号数)的符号位是0,所以这里有一个结论。正数的源码,反码,补码相同
例子1:
写出4的源码,反码和补码。
分析:由于4是正数,所以4的源码反码和补码相同。4的源码就是它的二进制序列。如下图
例子2:
写出-1的源码,反码和补码
分析:-1不是正数,不能用上面的结论了。
由于-1是负数,所以高位符号位是1。因此源码如图。
反码是源码除符号位外的每一位按位取反,所以反码如图。
补码是反码的二进制序列+1,所以补码如图。
#注意事项
注1:在计算机中,整型的存放是以补码的形式来存放的。当你想要使用计算机内存的特定整型时,它会从补码自动转换成源码供你使用。
举两个例子来说明:
int main()
{
int a = -1;
return 0;
}
在内存中你觉得a的值是什么呢?
答案是
f是16进制,换成二进制就是1111。8个f正好是32个1。这个例子说明了整型是以补码在内存中储存的。
在下面这段代码,毫无疑问会打印-1。
这说明了整型在拿出来的时候会打印它的源码。
int main()
{
int a = -1;
printf("%d", a);
return 0;
}
1.1以补码存放的好处
- 在最高位是符号位的前提下,补码可以解决加减法的问题。用源码是无法计算减法的。(自己可以举个例子试一下)
- 补码和源码相互转换的方式其实是一致的。
补码换成源码可以通过先减一再取反的方式得到源码,也可以通过先取反再加一的方式得到源码。
计算机是用先取反再加一的方式来用补码得到源码的。这样可以保证二者的方式一样,不需要额外的电路硬件
1.2大小端字节序
先说明大小端的定义:
1.大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
2.小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
在每个不同的平台上,存储方式都不同。
简单来说:大端字节序是顺序存储。小端字节序是逆序存储。
举例子说明:
int main()
{
int a = 1;
return 0;
}
这是a的值。但道理a应该是00 00 00 01才对呀?为什么它是01 00 00 00呢?
这证明了vs是用小端字节序存储的。
需要说明的是:字节的高位是第一位。1的补码是00 00 00 01。则高位是00。
我们画图来进一步说明:
需要说明的是:字节的高位是第一位。1的补码是00 00 00 01。则高位是00。上面这个图表示的就是字节的低位存储在低地址处。属于小端储存方式。
上面这个图表示的就是字节的高位存储在高地址处。属于大端储存方式。
1.2.1 如何验证这个平台是大端字节序还是小端字节序呢?
代码如下:
思路就是举一个例子。如果是小端存储则返回的是1,不是则返回0.
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端");
}
else
{
printf("大端");
}
return 0;
}
1.3关于整型数据存储方式的练习题
根据整型储存知识和操作符的知识,我们可以做一些综合的练习题了。
习题1:这段代码输出什么?
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;
}
答案是:-1,-1,255.
这道题结合了整型提升的知识。
-1的补码是1111 1111。由于a,b,c都是char类型的。而打印的时候是用有符号整型来打印的(%d)。则需要进行整型提升。
整型提升的方法为:补最高位的符号位。若最高位为1,则前面补1,直至补齐32位。若最高位为0,则前面补0。(无符号数也补0)
a和b由于是有符号数。最高位是1,则前面补1。
对应的补码为32位1,对应的原码是-1。因此打印-1。
c由于是无符号数,没有符号位,则补0。
对应的补码是11111111,由于c是无符号数,原反补码相同,所以原码也是11111111,打印出来是255。
注意:任何补位操作的对象都是补码,不要对着原码咔咔咔地算个不停。切记要先换成补码。计算机是不认得原码的!!!!!!
附上图解:
习题2:
打印什么呢?
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
答案:4,294,967,168
这道题还是用了整型提升。
-128的补码是10000000。(规定的)这里有一个表格可以让你更好的理解它。
由于要把它以整型的方式打印,因此要整型提升。这里说明一下,char类型整型提升成int,unsigned char才整型提升成unsigned int。
提升后:
11111111 11111111 11111111 10000000
以无符号整型拿出来,因此这段二进制代表的数字是
习题3:
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
答案:和上一题一样。
128是无法正常存入char类型中的,因为char只能表示-128~127的数字(看上面那个表)
这里有一个神奇的规律。整型在越界的情况下,它会循环回到起点。
假如-128是char类型的起点,127是char类型的终点。127往后跨多一步(加一)会变成-128.
图解,看着这个表就很好理解了:
习题4:
打印什么?
int main()
{
int i= -20;
unsigned int j = 10;
printf("%d\n", i+j);
}
答案:-10
- 这道题有一个很简单的理解方法(但计算机不是这么做的)
10是正数,所以unsigned对它没有意义,相当于j也是int类型。于是答案是-10.
2.计算机的做法是常规做法。int和unsigned int相加,由于类型不统一,需要进行算数转换。
注:整型提升和算数转换的区别,整型提升针对的char和short类型的。算数转换是针对int以上精度的类型的(下面的转换成上面的类型)。
- long double
- double
- float
- unsigned long int
- long int
- unsigned int
- int
图解:
习题5:
打印什么?
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}
答案:死循环。且打印9,8,7,…0,4294967295,4294967294,…
这个和习题三是一样道理的。由于unsigned int每一个数字都大于等于0,因此循环永远也不会结束。且会一直绕着那个起点与终点循环。如果不理解的话照着上面的图,自己尝试画一下。
习题6:
长度是多少呢?
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
答案:255.
这道题和习题三也是异曲同工。不懂可以看那个图。
strlen是遇到\0就停下来,不再计数了。
细心就会发现数组是存放着
-1,-2,-3…-128,127,126,125…2,1,0,-1…
0前面刚好255个数字所以答案是255.
习题7:
会打印什么?
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
答案:死循环。
unsigned char可以表示的数字是0~255。到255之后再加一会继续变成0。(和习题三同理),所以会死循环
#总结
- 整型越界后会回到原来的起点。不同类型的起始点和终点都不同,自己可以写出对应的补码来计算。
- 一切操作都是对补码进行操作,不要拿着原码当补码!!!!!
2.浮点数的存储方式
#标准描述
浮点数的存储方式是人为规定的。(IEEE 754标准),有点复杂。
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E
S------Sign,指符号位
M------Mantissa,指尾数,这里的尾数指的是有效数字小数点后的尾数。M的取值范围是1~2,这个后面举例讲
E------Exponent,指幂
(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。
具体方法是:
- 将一个浮点数写成二进制
- 将这个二进制写成科学计数法
- 把对应的S,M,E存入内存当中
举例来说: 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么,按照上面V的格式,可以得出s=0,
M=1.01,E=2。
十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,s=1,M=1.01,E=2。
注:
- 小数点后面的二进制是2的负数次方。如:0.5的二进制是0.1,因为2的负一次方等于0.5**
- 一个十进制小数要能用浮点数精确地表示,最后一位必须是 5(当然这是必要条件,并非充分条件)。规律推演如下面的示例所示:
#内存视角
那S,M,E具体是怎么存进内存当中的呢?
float类型(32位)
一个位存符号位,8个位存指数位,23个位存有效数字位(尾数位)
double类型(64位)
一个位存符号位,11个位存指数位,52个位存有效数字位(尾数位)
2.1M的存放方式
由于一个二进制数字写成科学计数法的时候,小数点前一位肯定是1。因此IEEE754规定默认第一位是1,存储有效数字位的时候就不存1了,只存放后面的尾数。
2.2E的存放方式
E由于可以为负数,因此不能直接存。标准规定要加上一个中间数来存放。float类型加上127,double类型加上1023.
若E=2,则放进内存的时候是放进2+127=129.
2.3 浮点数从内存中取出的方式(3种情况)
下面的E指的是存入内存的E(即加上127或1023后的)
- E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前
加上第一位的1。 - E全为0
E全为0的情况就是原先幂为-127。(-127+127=0)
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为
0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。 - E全为1
E全为1的情况是原先幂为128。(128+127=255,即全一)
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);
(M不是全0也是一个很大的数)
用以上规则举个例子
2.4 应用题
打印什么?
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,0.000000,1,091,567,616,9.000000
要讲的是第二和第三个。
第二个:
由于n是一个整型。
内存中存储的是0 00000000 00000000000000000001001
V=(-1)^0 ×0.00000000000000000001001×2(-126)=1.001×2(-146)
显然,V是一个很小的接近于0的正数,所以用十进制小
数表示就是0.000000。
第三个:
9.0 -> 1001.0 ->(-1)^01.0012^3 -> s=0, M=1.001,E=3+127=130
那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,即10000010。 所以,写成二进制形式,应该是s+E+M,即
0 10000010 001 0000 0000 0000 0000 0000
换成十进制即: