ps:感谢C语言大佬的文章!

float 和 double 的存储格式:

float的内存存储分布
符号位(1Bit)指数部分(8Bits)尾数部分(23Bits)
double的内存存储分布
符号位(1Bit)指数部分(11Bits)尾数部分(52Bits)

举个例子来一步一步说明float的存储:

二进制形式的浮点数的存储

C语言标准规定,小数在内存中以科学计数法的形式来存储,具体形式为:flt = (-1)^sign × mantissa × base^exponent

  • sign符号,0正数,1负数
  • mantissa 为尾数
  • base代表进制(基数)
  • exponent 为指数
    注:
  • mantissa 表示真实的尾数,包括整数部分和小数部分;mant 表示内存中存储的尾数,只有小数部分,省略了整数部分。
  • exponent 表示真实的指数,exp 表示内存中存储的指数,exponent 和 exp 并不相等,exponent 加上中间数 median 才等于 exp。

具体存储的方法

  1. 符号的存储:符号的存储很容易,就像存储 short、int 等普通整数一样,单独分配出一个位(Bit)来,用 0 表示正数,用 1 表示负数。对于 19.625,这一位的值是 0。
  2. 尾数的存储:当采用二进制形式后,尾数部分的取值范围为 1 ≤ mantissa < 2,这意味着:尾数的整数部分一定为 1,是一个恒定的值,这样就无需在内存中提现出来,可以将其直接截掉,只要把小数点后面的二进制数字放入内存中即可。对于 1.0011101,就是把 0011101 放入内存。
  3. 指数的存储:指数是一个整数,并且有正负之分,不但需要存储它的值,还得能区分出正负号来。

float 的指数部分占用 8 Bits,能表示从 0~255 的值,取其中间值 127,指数在写入内存前先加上127,读取时再减去127,正数负数就显而易见了。19.625 转换后的指数为 4,4+127(median) = 131(exp),131 换算成二进制为 1000 0011,这就是 19.626 的指数部分在 float 中的最终存储形式。

19.625 转换成二进制的指数形式为:
19.625 = 10011.101 = 1.0011101×2^4
此时符号为 0;尾数为 1.0011101,截掉整数部分(mant)后为 0011101,补齐到 23 Bits 后为 001 1101 0000 0000 0000 0000;指数为 4,4+127(median) = 131(exp),131 换算成二进制为 1000 0011。

#include <stdio.h>
#include <stdlib.h>
//浮点数结构体
typedef struct{
    unsigned int nMant : 23;//尾数部分
    unsigned int nExp : 8;//指数部分
    unsigned int nSign : 1;//符号位
} FP_SINGLE;
int main(int argc, const char * argv[]) {
//    char strBin[33] = { 0 };
    float f = 19.625;
    FP_SINGLE *p = (FP_SINGLE*)&f;
    
    // LLVM 编译器严格遵循 C99 标准,无法使用 itoa()
    printf("sign: %X\n", p->nSign);//16进制
    printf("nExp: %X\n", p->nExp);//16进制
    printf("nMant: %X\n", p->nMant);//16进制

//    itoa(p->nSign, strBin, 2);//2进制
//    itoa(p->nExp, strBin, 2);
//    printf("exp: %s\n", strBin);
//    itoa(p->nMant, strBin, 2);
//    printf("mant: %s\n", strBin);
    return 0;
}

  • %f 以十进制形式输出 float 类型;
  • %lf 以十进制形式输出 double 类型;
  • %e 以指数形式输出 float 类型,输出结果中的 e 小写;
  • %E 以指数形式输出 float 类型,输出结果中的 E 大写;
  • %le 以指数形式输出 double 类型,输出结果中的 e 小写;
  • %lE 以指数形式输出 double 类型,输出结果中的 E 大写。
  • %g 和 %lg 分别用来输出 float 类型和 double 类型,并且当以指数形式输出时,e小写。
  • %G 和 %lG 也分别用来输出 float 类型和 double 类型,只是当以指数形式输出时,E大写。
    注:

float 始终占用4个字节,double 始终占用8个字节。
%f 和 %lf 默认保留六位小数,不足六位以 0 补齐,超过六位按四舍五入截断。
将整数赋值给 float 变量时会变成小数。
以指数形式输出小数时,输出结果为科学计数法;也就是说,尾数部分的取值为:0 ≤ 尾数 < 10。
%g 会对比小数的十进制形式和指数形式,以最短的方式来输出小数,让输出结果更加简练。
%g 默认最多保留六位有效数字,包括整数部分和小数部分;%f 和 %e 默认保留六位小数,只包括小数部分。
%g 不会在最后强加 0 来凑够有效数字的位数,而 %f 和 %e 会在最后强加 0 来凑够小数部分的位数。

#include <stdio.h>

int main(int argc, const char * argv[]) {
    // insert code here...
    float a = 0.314;
    double b = 128.101;
    double c = 123;
    float d = 112.64E3;
    double e = 0.7623e-2;
    float f = 1.230000098;
    float g = 1.229338455;
    printf("a=%e\n,b=%f\n,c=%lf\n,d=%e\n,e=%le\n,f=%lE\n,g=%g\n",a,b,c,d,e,f,g);
    return 0;
}

  1. 原码:
    将一个十进制整数转换成二进制,例如short a = 6;a的原码就是0000 0000 0000 0110。
  2. 反码:
    正数,它的反码就是其原码(原码和反码相同);负数的反码是将原码中除符号位以外的所有位(数值位)取反,也就是 0 变成 1,1 变成 0。例如short a = 6; a 的原码和反码都是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的反码是1111 1111 1110 1101。
  3. 补码:
    正数,它的补码就是其原码(原码、反码、补码都相同);负数的补码是其反码加 1。例如short a = 6;,a 的原码、反码、补码都是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的补码是1111 1111 1110 1110。
  4. 总结:
    在计算机内存中,整数一律采用补码的形式来存储。这意味着,当读取整数时还要采用逆向的转换,也就是将补码转换为原码。

  1. %hd用来输出 short int 类型,hd 是 short decimal 的简写;
  2. %d用来输出 int 类型,d 是 decimal 的简写;
  3. %ld用来输出 long int 类型,ld 是 long decimal 的简写。

关于二进制、八进制、十进制的不同进制的形式输出时对应的格式控制符

-shortintlong
八进制%ho%o%lo
十进制%hd%d%ld
十六进制%hx 或者 %hX%x 或者 %X%lx 或者 %lX

注:

  1. 十六进制数字的表示用到了英文字母,有大小写之分,要在格式控制符中体现出来:

    • %hx、%x 和 %lx 中的x小写,表明以小写字母的形式输出十六进制数;
    • %hX、%X 和 %lX 中的X大写,表明以大写字母的形式输出十六进制数。
  2. 八进制数字和十进制数字不区分大小写,所以格式控制符都用小写形式。如果你比较叛逆,想使用大写形式,那么行为是未定义的,请你慎重
#include <stdio.h>

int main(int argc, const char * argv[]) {
    
    //二进制由 0 和 1 两个数字组成,使用时必须以0b或0B(不区分大小写)开头
    short a = 0b101;//换算成十进制为 5
    int b = -0b10001;//换算成十进制为 -17
    long c = 0B100001;//换算成十进制为 33
    printf("a=%hd,b=%d,c=%ld\n",a,b,c);
    
    //八进制由 0~7 八个数字组成,使用时必须以0开头(注意是数字 0,不是字母 o)
    short d = 015;  //换算成十进制为 13
    int e = -0101;  //换算成十进制为 -65
    long f = 0177777;  //换算成十进制为 65535
    printf("d=%hd,e=%d,f=%ld\n",d,e,f);
    
    //十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,使用时必须以0x或0X(不区分大小写)开头
    short g = 0x2A;//换算成十进制为 42
    int h = -0Xa0;//换算成十进制为 -160
    long i = 0xffff;//换算成十进制为 65535
    printf("g=%hd,h=%d,i=%ld\n",g,h,i);

    //不同进制的形式输出时对应的格式控制符
    short j = 0b1010110;//二进制数字
    int k = 02713;//八进制
    long l = 0X1DAB83;//十六进制数字
    printf("j=%ho,k=%o,l=%lo\n",j,k,l);//以八进制形式输出
    printf("j=%hd,k=%d,l=%ld\n",j,k,l);//以十进制形式输出
    printf("j=%hx,k=%x,l=%lx\n",j,k,l);//以十六进制形式输出(字母小写)
    printf("j=%hX,k=%X,l=%lX\n",j,k,l);//以十六进制形式输出(字母大写)
    
    //区分不同进制数字的一个简单办法就是,在输出时带上特定的前缀"#"。
    printf("j=%#ho,k=%#o,l=%#lo\n",j,k,l);//以八进制形式输出
    printf("j=%hd,k=%d,l=%ld\n",j,k,l);//以十进制形式输出
    printf("j=%#hx,k=%#x,l=%#lx\n",j,k,l);//以十六进制形式输出(字母小写)
    printf("j=%#hX,k=%#X,l=%#lX\n",j,k,l);//以十六进制形式输出(字母大写)

    //%d 以十进制形式输出有符号数;
    //%u 以十进制形式输出无符号数;
    //%o 以八进制形式输出无符号数;
    //%x 以十六进制形式输出无符号数。

    //当以有符号数的形式输出时,printf 会读取数字所占用的内存,并把最高位作为符号位,把剩下的内存作为数值位;
    //当以无符号数的形式输出时,printf 也会读取数字所占用的内存,并把所有的内存都作为数值位对待。

    return 0;
}

1个元器件称为1比特(Bit)或1位,8个元器件称为1字节(Byte),那么16个元器件就是2Byte,32个就是4Byte,以此类推:

  • 8×1024个元器件就是1024Byte,简写为1KB;
  • 8×1024×1024个元器件就是1024KB,简写为1MB;
  • 8×1024×1024×1024个元器件就是1024MB,简写为1GB。
    单位换算:
  • 1Byte = 8 Bit
  • 1KB = 1024Byte = 2^10Byte
  • 1MB = 1024KB = 2^20Byte
  • 1GB = 1024MB = 2^30Byte
  • 1TB = 1024GB = 2^40Byte
  • 1PB = 1024TB = 2^50Byte
  • 1EB = 1024PB = 2^60Byte