C语言 | 数字(int, float 为例)在内存中的二进制表示与查看

计算机内存中的数字都是二进制表示的。
数字是怎么表示成计算机内存中的二进制的呢?

先说几个常识,以我正在使用的电脑为准(底层概念依赖硬件的实现,可能有差异):

  • 目前我使用的intel电脑,是小端(主流现在都是小端),就是数字的低位也在内存的低位。
    • 例: 1234 这个数字,4是低位(+1改变最小,1),1是高位(+1改变很大,1000),在内存中从地址低到高排列是:4321.
  • 8 bits = 1byte = 1 char 占的空间。
  • 1 int 占了 4 bytes = 32 bits
  • 1 float 占了 4 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后,和正数的原码的和正好是0。相当于把 减法运算 简化为一个 加法运算 。

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 –

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值