计算机内存中的数字都是二进制表示的。 数字是怎么表示成计算机内存中的二进制的呢?
先说几个常识,以我正在使用的电脑为准(底层概念依赖硬件的实现,可能有差异):
- 目前我使用的intel电脑,是小端(主流现在都是小端),就是数字的低位也在内存的低位。
- 例: 1234 这个数字,4是低位(+1改变最小,1),1是高位(+1改变很大,1000),在内存中从地址低到高排列是:4321.
- 8 bits = 1byte = 1 char 占的空间。
- 1 int 占了 8 bytes = 32 bits
- 1 float 占了 8 bytes = 32 bits
- 最高位表示符号,0正数,1负数。
1. 几个概念
原码 :最高位是符号位,0代表正数,1代表负数,非符号位为该数字绝对值的二进制。
反码:正数的反码与原码一致,负数的反码是对原码按位取反,只是最高位(符号位)不变。
补码:正数的补码与原码一致,负数的补码是对原码按位取反加1,符号位不变。
(1). 原码
将一个整数转换成二进制形式,就是其原码。 例如short a = 6; a 的原码就是 0000 0000 0000 0110 ; 更改 a 的值a = -6; 此时 a 的原码就是 1000 0000 0000 0110 。
通俗的理解,原码就是一个整数本来的二进制形式。
原码的缺点是:有2个0, 0…0 和 1…0,分别表示 +0和-0,相当于浪费了一个编码。
(2). 反码
- 对于正数,它的反码就是其原码(原码和反码相同);
- 负数的反码是将原码中除符号位以外的所有位(数值位)取反,也就是 0 变成 1,1 变成 0。
例如short a = 6; a 的原码和反码都是 00000000 00000110 ; 更改 a 的值a = -6; 此时 a 的反码是 11111111 11111001 。
反码的优势:这样-0就不存在了,原来的 1000…0 可以表示 -128了。
(3). 补码
- 对于正数,它的补码就是其原码(原码、反码、补码都相同);
- 负数的补码是其反码加 1。
例如short a = 6; a 的原码、反码、补码都是 00000000 00000110 ; 更改 a 的值a = -6; 此时 a 的补码是 11111111 11111010 。
可以认为,补码是在反码的基础上打了一个补丁,进行了一下修正,所以叫“补码”。
原码、反码、补码的概念只对负数有实际意义,对于正数,原码、反码、补码都是一样的。
补码的优势:取反码+1后,和正数的原码的和正好是1。相当于把加减运算简化为一个加法运算。
2. 查看int的二进制表示
int 占4字节,共32位。 正整数用原码表示。 负整数用补码表示。
#include<stdio.h>
void test1(){
for(int k=7; k>=0; k--){ //k依次等于7,6,5,4,3,2,1,0
int a=1<<k;
printf("[%d]%d\n",k, a);
}
}
/** https://blog.csdn.net/hbsyaaa/article/details/106970226
* Aim: print binary format in memory, for a given int
* input: int
* output: void
*/
void printf_bin(int num){
int i, k;
unsigned char j;
int len=sizeof(int);
//默认 小端,数字的低位放在地址低位
//p先指向num后面的第4个字节的地址(0,1,2,3)
unsigned char *p=(unsigned char*) &num + len-1;
for(i=0; i<len; i++){ //依次处理4个字节(32位)
//取每个字节的首地址,从高位到低位:p, p-1, p-2, p-3;
//然后取值,正好是一个char,8bit
j = *(p-i);
for(k=7; k>=0; k--){ //k依次等于7,6,5,4,3,2,1,0
//把1左移k位,然后和j取 并,如果j的这位上是1,则true
if(j & (1<<k))
printf("1");
else //如果j的这位上为0,则false
printf("0");
}
printf(" ");//每8位加一个空格
}
printf("\n");
}
void show(int a){
printf("%s%d ", a>=0?" ":"", a);
printf_bin(a);
}
int main(){
//查看一个整数的ascii源码
int a=3, b=-3;
unsigned char *pa=(unsigned char*) &a, *pb=(unsigned char*) &b;
printf( "sizeof(int)=%ld, sizeof(char)=%ld\n", sizeof(int), sizeof(char));
// for a: 正数用原码
for(int i=0; i< sizeof(int); i++){
printf("%p[%d]=%x\n", &pa[i], i, pa[i]);
}
printf("\n");
// for b: 负数用补码: 绝对值的原码,取反,再加1
for(int i=0; i< sizeof(int); i++){
printf("%p[%d]=%x\n", &pb[i], i, pb[i]);
}
printf("\n");
// part II
printf("int在内存中的二进制表示: \n");
printf(" %d ", a);
printf_bin(a);
printf("%d ", b);
printf_bin(b);
test1(); //测试 << 运算符
int c=755;
printf("%d ", c);
printf_bin(c);
// 4位2进制 /bit => 1位16进制; 8bit=1byte=1char = 2个16进制位;
printf("10进制: %d, \t8进制: 0%o, \t16进制: 0x%x 0x%X\n", c,c,c, c);
show(6);
show(-6);
return 0;
}
输出结果:
$ gcc -std=c11 16_binary_in_memory_int.c
$ ./a.out
sizeof(int)=4, sizeof(char)=1
0x7ffc9c4339f4[0]=3
0x7ffc9c4339f5[1]=0
0x7ffc9c4339f6[2]=0
0x7ffc9c4339f7[3]=0
0x7ffc9c4339f8[0]=fd
0x7ffc9c4339f9[1]=ff
0x7ffc9c4339fa[2]=ff
0x7ffc9c4339fb[3]=ff
int在内存中的二进制表示:
3 00000000 00000000 00000000 00000011
-3 11111111 11111111 11111111 11111101
[7]128
[6]64
[5]32
[4]16
[3]8
[2]4
[1]2
[0]1
755 00000000 00000000 00000010 11110011
10进制: 755, 8进制: 01363, 16进制: 0x2f3 0x2F3
6 00000000 00000000 00000000 00000110
-6 11111111 11111111 11111111 11111010
3. 查看 float 的二进制表示
float 占4字节。 具体表示方法如下:
float N在内存中的二进制表示,写成: N = (-1)^S * (1.M) * 2^(E-127) ,符号部分S, 阶码部分E,尾数部分M。 对二进制,2^n就是小数点的移位操作
- 符号位s(1 bit): 第31位,0表示正数,1表示负数
- 阶码位E(8 bits): 第30~23位,取值范围 -128~127: 阶码E = 指数e + 127
- 尾数位M(23 bits): 第22-0位,换算成十进制就是 2^23=8388608,所以十进制精度只有6 ~ 7位。小数点前的1省略,相当于23位表示24位的内容
例1: -12.5 的二进制表示
1. 通过反复*2或/2先化成 -12.5 = -1.5625 * 2^3
2. 符号位S: 负数最高位取1
3. 指数e=3,阶码E=e+127=130
3. 尾数部分 M=5625,
0.5625*2=1.125 ~ 整数1
0.125*2=0.25 ~整数0
0.25*2=0.5 ~ 整数0
0.5*2=1.0 ~ 整数1
0 结束计算
尾数部分从高位到低位为 1.1001
4. 转化为IEEE754表示法:
符号位:1
指数位:127(偏移量/表示0)+3 = 130,即二进制10000010
小数位:1001(去除了整数部分),余下的位补0,即10010000000000000000000
5. 拼凑起来:1 10000010 10010000000000000000000
// 例2:
比如:把十进制小数 0.87 转换成二进制,具体怎么操作?
1.通过反复*2或/2先化成 0.87 = 1.74 * 2^-1
2. 符号位 S=0
3. 指数e=-1, 阶码E=-1+127=126, 二进制 01111110
3. 尾数部分M=0.74
0.74*2=1.48 ~ 整数部分是1
0.48*2=0.96 ~ 0
0.96*2=1.92 ~ 1
0.92*2=1.84 ~ 1
0.84*2=1.68 ~ 1
0.68*2=1.36 ~ 1
0.36*2=0.72 ~ 0
0.72*2=1.44 ~ 1
0.44*2=0.88 ~ 0
0.88*2=0.76 ~ 0
...
从高位到低位写 1011110100
5. 拼凑起来: 0 01111110 1011110100...
源代码
#include<stdio.h>
// 浮点数在内存中的二进制形式
void print_red(char arr[]){
printf("\033[31m%s\033[0m", arr);
}
void print_yellow(char arr[]){
printf("\033[33m%s\033[0m", arr);
}
void print(char arr[], int counter){
if(counter<=30 && counter>=23)
print_red(arr);
else
print_yellow(arr);
}
/**
* modify from: https://blog.csdn.net/hbsyaaa/article/details/106970226
* Aim: print binary format in memory of a given float
* input: float
* return: void
*/
void printf_binF(float num){
int i, k;
unsigned char j;
int len=sizeof(float);
//默认 小端,数字的低位放在地址低位
//p先指向num后面的第4个字节的地址(0,1,2,3)
unsigned char *p=(unsigned char*) &num + len-1;
int counter=31;
for(i=0; i<len; i++){ //依次处理4个字节(32位)
//取每个字节的首地址,从高位到低位:p, p-1, p-2, p-3;
//然后取值,正好是一个char,8bit
j = *(p-i);
for(k=7; k>=0; k--){ //k依次等于7,6,5,4,3,2,1,0
//把1左移k位,然后和j取 并,如果j的这位上是1,则true
if(j & (1<<k)){
print("1", counter);
}else{ //如果j的这位上为0,则false
print("0", counter);
}
counter--;
}
printf(" ");//每8位加一个空格
}
printf("\n");
}
// 打印一个 float,及其在内存中的二进制表示
void bin_float(float c){
// c=1.11*2;
printf("%s%f ", c>=0?" ":"", c);
printf_binF(c);
}
int main(){
//查看一个整数的ascii源码
float a=2.42, b=-2.42;
unsigned char *pa=(unsigned char*) &a, *pb=(unsigned char*) &b;
printf( "sizeof(float)=%ld, sizeof(char)=%ld\n", sizeof(float), sizeof(char));
// for a: 正数用原码
for(int i=0; i< sizeof(float); i++){
printf("%p[%d]=0x%x\n", &pa[i], i, pa[i]);
}
printf("\n");
// for b: 负数用补码: 绝对值的原码,取反,再加1
for(int i=0; i< sizeof(float); i++){
printf("%p[%d]=0x%x\n", &pb[i], i, pb[i]);
}
printf("\n");
// part II
printf("float N在内存中的二进制表示,写成: N = (-1)^S * (1.M) * 2^(E-127) ,符号部分S, 阶码部分E,尾数部分M。 对二进制,2^n就是小数点的移位操作\n"
"\t符号位s(1 bit): 第31位,0表示正数,1表示负数\n");
print_red("\t阶码位E(8 bits): 第30~23位,取值范围 -128~127: 阶码E = 指数e + 127 \n");
print_yellow("\t尾数位M(23 bits): 第22-0位,换算成十进制就是 2^23=8388608,所以十进制精度只有6 ~ 7位。"
"小数点前的1省略,相当于23位表示24位的内容\n\n");
printf("\t2.42 = 1.21 * 2^1\n");
printf("1.怎么确定符号位S? \n");
bin_float(a);
bin_float(b);
printf(" => 符号是整个浮点数的符号,仅仅体现在最高位(31th bit): 0正,1 负数。阶码E=1+127=128==2^7=10000000(2)\n");
printf("\n2.怎么计算阶码E?把浮点数反复*2和/2写成1.xx * 2^e的形式,阶码E=e+127\n");
bin_float(a/2);
printf(" => 1.21=1.21*2^0,所以阶码E=0+127=127=2**7-1=01111111(2)\n");
bin_float(0.605);
bin_float(-0.605);
printf(" => 0.605=1.21*2^-1,所以阶码E=-1+127=126=2**7-2=01111110(2)\n");
bin_float(0);
bin_float(0.5);
bin_float(1);
bin_float(2);
bin_float(4);
bin_float(8);
printf(" => 只有指数部分 2^e, 指数e=-1,0,1,2,3, 则阶码E=e+127=126,127,128,1,2\n");
bin_float(0.12);
bin_float(1.92);
printf(" => 0.12 = 1.92 * 2^-4, 阶码 E=-4+127, 就是把2^2扣掉(从右向左第3位)\n");
printf("\n3.怎么计算尾数M? 取出1.M中的M,开始反复 *2并取其整数部分作为2进制的高位\n");
bin_float(2.5);
bin_float(1.25);
printf(" => 1.25=1.25*2^0, E=0+127; M=0.25, 0.25*2=0.5~整数0, 0.5*2=1.0~整数1,写起来就是 1.01,省略1,后面补0\n");
printf(" => 0.87(10) = 1.101111(2) * 2^-1, 反复*2或除以2,写成 1.M * 2^e的形式,E=e+127\n");
bin_float(0.87);
bin_float(1.74);
printf(" =>1.74=1.74 * 2^0; E=0+127; M=0.74,0.74*2=1.48~整数1,0.48*2=0.96~整数0,\n"
"\t0.96*2=1.92~整数1,0.92*2=1.84~整数1,0.84*2=1.68~整数1,0.68*2=1.36~整数1,0.36*2=0.72~整数0,..."
"==>1.1011110\n");
return 0;
}
输出结果:
$ gcc -std=c11 17_binary_in_memory_float.c
$ ./a.out
sizeof(float)=4, sizeof(char)=1
0x7fffad0670f8[0]=0x48
0x7fffad0670f9[1]=0xe1
0x7fffad0670fa[2]=0x1a
0x7fffad0670fb[3]=0x40
0x7fffad0670fc[0]=0x48
0x7fffad0670fd[1]=0xe1
0x7fffad0670fe[2]=0x1a
0x7fffad0670ff[3]=0xc0
后面的输出带有色彩,见截图:
Ref
- https://github.com/miostudio/linux_C/tree/master/base/07_memory
– End –
|