对二进制的数字表示 - 原码反码补码:
在C语言当中, 计算机对二进制的数字表示会用到原码, 反码以及补码.
对于有符号的整型来说(int), 32位的第1位是符号位.
原码:将最高位作为符号位(0表示正,1表示负),其它数字位代表数值本身的绝对值的数字表示方式。
反码:正数的反码和原码相同, 负数的话除了最高位的符号位不变, 其他数字位按位取反(1变0, 0变1).
补码:正数的补码和原码相同, 负数的话就是反码+1(从最低位那+1).
例如:
int a = -15;
1000 0000 0000 0000 0000 0000 0000 1111 - 原码
1111 1111 1111 1111 1111 1111 1111 0000 - 反码
1111 1111 1111 1111 1111 1111 1111 0001 - 补码
注意: 正数三码合一(原码反码补码都相同)!!!
原码反码补码的意义:
那么为什么科学家要发明原码反码补码呢?不能全都用原码吗? 原因很简单, 因为计算机没有减法器只有加法器.
0000 0000 0000 0000 0000 0000 0000 0001 为 1的原码
1000 0000 0000 0000 0000 0000 0000 0001 为-1的原码
如果原码相加就变成:
1000 0000 0000 0000 0000 0000 0000 0010 为-2的原码, 明显不符.
所以聪明的计算机科学家发明了补码来用于计算机的计算(计算机存储的是补码, 原码是自己构造的要转换成补码才是计算机用的).
字节序 - 以字节为单位来讨论存储顺序:
字节序是以字节为单位来讨论存储顺序的, 通常有两种存储规则:大端字节序存储和小端字节序存储:
小端字节序存储:
把一个数据的低位字节的内容存放在低地址处, 高位字节的内容存放在高地址处.
大端字节序存储:
把一个数据的低位字节的内容存放在高地址处, 高位字节的内容存放在低地址处.
对于数据来说, 如0x 11 22 33 44中44是低位(小的一端).
一般来说机器是小端字节的多一些.
下面写一个判断机器是小端存储还是大端存储的代码:
#include <stdio.h>
int check() {
int a = 1; // 内存中为0000....0001
// a的地址从低位开始取,用char*指针类型取第一个字节.
// 访问第一个字节判断是0000还是0001.
// 是0000证明高位字节放在了低地址为大端.
// 是0001则是低位字节放在了低地址为小端.
return *((char*)&a); // 强制类型转换再解引用.
}
int main() {
int ret = check();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
signed和unsigned(有符号和无符号)
C语言中整型都有分signed和unsigned, 默认short和int和long和longlong都为signed.
C语言没有明确规定char是signed还是unsigned, vs默认是signed(一般都是).
浮点数在内存中的存储:
浮点数的存储规则: (-1) ^ S * M * 2 ^ E.
其中(-1) ^ S表示符号位, 当S为0代表是正数, 当S为1则为负数.
M代表有效数字, 大于等于1小于2. 例如:1.1, 1.123, 1.666, 1.0等.
2 ^ E代表指数位.
例如:
十进制的5.0, 写成二进制是101.0, 相当于1.01 * 2 ^ 2.
其中S = 0, M = 1.01, E = 2.
十进制的5.5是二进制是101.1, 101是5, 后面的1是2的-1次方分之一就是0.5.
在小数点后的数字每往后就是2的负多少次方, 比如0.11的二进制写成十进制是0.75. 0.101的二进制写成十进制是0.5 * 1 + 0.25 * 0 + 0.125 * 1 = 0.625.
对于float的存储方式(32个bit位), 第1位是符号位S, 后8位是指数位E, 剩下23位是有效数字位M.
对于double的存储方式(64个bit位), 第1位是符号位S, 后11位是指数位E, 剩下52位是有效数字位M.
因为double的有效数字位大于float的, 所以说double的精度比float高.
对于M来说:
因为1 <= M < 2, M可以写成1.xxxxxxxxxx的形式, 所以默认第一位就是1, 故舍去, 只保存xxxxx的部分.
取出来时自动补上1, 这样可以多存一位有效数字.
对于E来说:
E是个无符号整数(unsigned int).
如果E是8位则取值范围为0 ~ 255, 为11位则为0 ~ 2047.
但是科学计数法中E是可以为负数的, 所以存入内存是E的真实值要加上一个中间数, 8位和11位中间数分别是127和1023.
比如2 ^ 10的E是10, 所以保存成32位浮点数时, 必须保存成10 + 127 = 137, 即10001001(二进制).
当E从内存中取出时分三种情况:
①E的二进制不为全0或全1, 则E取出来的值换算成十进制减去127或1023, 得到真实值然后再讲有效数字M前加上第一位的1.
比如0.5的二进制形式为0.1, 由于规定整数部分必须为1, 所以为1.0 * 2 ^ -1,
其阶码为-1 + 127 = 126, E表示为01111110, 尾数1.0去掉整数部分1, 尾部补0到23个0, 则这个浮点数的二级制表示为0 01111110 000...000(23个0).
②当E为全0时,浮点数E等于1 - 127(或1 - 1023), 有效数字M第一位不加1了,改成0, 即还原成0.xxxxx的小数, 这样做为了表示正负0以及接近于0的很小的数字.
③当E为全1, 这是如果有效数字M权威0, 表示正负无穷大(符号取决于s).
下面是代码演示浮点数存储在内存中的原理:
#include <stdio.h>
int main() {
int a = 3;
float* p = (float*)&a; // 强制转换成float类型的指针.
printf("%d\n", a);
printf("%f\n", *p);
*p = 9; // 改变指针解引用的值, 因为p是float型的指针所以按照float的存储规则改变.
printf("%d\n", a);
printf("%f\n", *p);
return 0;
}
运行截图:
a写成二进制是0000 0000 0000 0000 0000 0000 0000 0011.
当以float形式的指针接受a的地址时, 将会按照浮点数的规则计算数值.
所以对于a来说符号位0, 所以是正数, 符号位后8位指数位E全为0, 有效数字又非常小只打印6位小数所以是0.
对于*p更改成9(二进制是1001, (-1) ^ 0 * 1.001 * 2 ^ 3)按照浮点数规则更改则为:
0 10000010 00100000000000000000000, 将其转换成十进制就是1091567616.