进制转换
十六进制/十进制/二进制的互转方式如下
-
二进制和十进制的互转
十进制转二进制采用除2取余数,逆序排列的方法
二进制转十进制每遇到一个二进制的1,乘以相应的阶数(阶数等于这一位后有多少二进制位)最后相加得到结果
-
二进制和十六进制的互转
每四个二进制位代表一个的十六进制位,二进制转十六进制每四位合并为一位,十六进制转二进制每一位展开为4个二进制位
这里的转换表经常记不住,可以用一个简单的方法,记住 a 和 c 对应的二进制数
a(hex) = 1010(BIN) 类似于 a 就是一个眼睛
c(hex) = 1100(BIN) 类似于 c 就是一个门
-
十进制和十六进制互转
十进制转十六进制,除以16,取得到的余数,最后逆序排列
十六进制转十进制,每遇到一个16进制位,乘以相应的阶数(阶数等于这一位后有多少二进制位),最后相加得到结果
正数和负数的二进制表示
原码:如果想要表示有符号整数,就要将最前面一个二进制位作为符号位,即 0 代表正数,1代表负数,后面 7 位为数值域
反码:正数的反码与其原码相同;负数的反码是对其原码逐位取反,但符号位除外
补码:正数的补码与其原码相同,负数的补码是在其反码的末位加1
引入补码的原因:
如果负数和正数都采用原码的方式,仅使用符号位区分,数学上,1 + (-1)=0,换算成在二进制中00000001+10000001=10000010,换算成十进制为-2,显然出错了,所以原码的符号位不能直接参与运算,必须和其他位分开,需要设计两种加法电路加以区分,这就增加了硬件的开销和复杂性
所以引入补码是为了解决计算机中数的表示和数的运算问题,使用补码,可以将符号位和数值域统一处理
+1 的二进制原码00000001 -1 二进制补码 11111111,相加之后的和为 0,符合数学运算的结果
负数的补码要保证和对应的正数相加结果为0,包括符号位
负数的补码是在其反码的末位加1是补码的一个重要性质
另外,补码下的 0 就只有一个表示方式,因此在判断数字是否为 0 时,只要比较一次即可
负数二进制补码转换成十进制的
给出负数补码 1000 1000,如何计算其十进制表示?
定义法
对补码先减去1,然后再取反
比如:1000 1000 减一结果1000 0111,取反后0111 1000,0111 1000 转换成十进制2^6 + 2^5 + 2^4 + 2^3 = 120,,所以为1000 1000 表示 -120相加为 0 法
负数的补码和对应的正数补码(原码)相加为 0
1000 1000 对应的相加为 0 的原码为 0111 1000,转换成十进制2^6 + 2^5 + 2^4 + 2^3 = 120,所以为1000 1000 表示 -120乘以阶数相加法
符号位对应的结果为乘以阶数,符号位负
1000 1000 1 作为符号位,对应 -2^7 = -128,准换成十进制 - 2^7 + 2^3 = -128 + 8 = -120
注意 定义法和相加为 0 法有一个bug,比如带符号的八位二进制数 1000 000,表示的十进制数是多少?
结果是 -128,这是一个标准定义,计算机中8bit 有符号数的计算规律如下所示:
signed char 表示的数据范围
char 型变量占用 8 个位,对于 signed char 类型,最高位表示符号位,此时有 7 个位用于表示数值。按照数学中的排列组合,7 个位能够表示 2^7 也即 128 个不同的数,若考虑正负号,signed char 类型最多也能表示 2*128 = 256 个不同的数
但是,如果 signed char 类型能够表示的数值范围是 -127 到 127,那么能够表示的只有 255 个不同的数字了,与理论最大能够表示的不同数字数 256 相比,少了一个,这是因为 -0 和 +0 其实是同一个数字,也即 0b10000000 和 0b00000000 是同一个数字 0
这对于计算机来说很不友好,同样的一个数字有两种二进制码,在处理时会显得很麻烦
因为 -0 和 +0 其实是同一个数字,因此原码中 1000 0000 和 0000 0000 都表示数字 0,现在补码下的 0 只有一个表示方式:
0000 0000,二进制码 1000 0000 就多余出来了,在此定义为 -128
在C语言中,signed char 型二进制码 0b10000000 的补码仍然为 0b10000000,因此它是“数字a的补码为 -a”原则的例外
模运算
对于 8 位字长的有符号整数类型,以 2^8 即 256 为模,对于其加减法运算
-128 = 128 (mod 256) -127 = 129 (mod 256)...-2 = 254 (mod 256) -1 = 255 (mod 256)
所以模 256 下的加减法,用 0, 1, 2,…, 254,255 表示其值,或者用 −128, −127,…, −1, 0, 1, 2,…,127 是完全等价的
−128与128,−127与129,…,−2与254,−1与255 可以互换而加减法的结果不变,需要的 CPU 加法运算器的电路实现与 8 位无符号整数并无不同
所以负数采用补码表示,等价于去除符号位的正数,-1 可以理解为 255,这样减法运算用加法运算器的电路就可以实现
8位有符号的运算 -1 + 10 等等价于 255 + 10 二进制表示为 1111 1111 + 0000 1010
得到结果 1 0000 1001,溢出的进位被自动舍弃,得到结果 0000 10001
相应的16位整形数的二进制加减法运算,等同于模 65536(2^16)的加法运算
32位整形数的二进制加减法运算,等同于模 2^32 的加法运算
浮点数的二进制表示
浮点数的定义
浮点数是用科学计数法表示的,这种方式下小数点的位置是漂浮不定的,所以命名为浮点数
25.125
25.125 = 0.25125 * 10^2;
25.125 = 2.5125 * 10^1
25.125 = 25.125 * 10^0
25.125 = 251.25 * 10^-1
25.125 = 2512.5 * 10^-2
同样的方法,二进制也可以用科学计数法表示,只是基数从 0 换成 2 而已
浮点数表示数字的方法
浮点数用科学计数法表示的数字的格式如下:
上图中各个变量的含义如下:
- S:符号位,0 表示正数,1 表示负数
- M:尾数,用小数表示,例如 3.254*10^-2 中的 3.254 就是尾数
- R:基数,表示十进制的 R 就是 10,表示二进制的的 R 就是 2
- E:指数,用整数表示,3.254*10^-2 中的 -2 就是指数
如果要用 32bit 表示一个浮点数,则需要把以上的变量,填充到对应的 bit 上就可以了:
一般用 32bit 表示一个 float 类型的浮点数,填充规则如下:
浮点数的二进制表示
将 25.125 转换为浮点数,过程如下图所示:
用二进制科学计数法表示 25.125
25.125(D) = 11001.001(BIN) = 1.1001001 * 2^4(BIN)
按照 32bit 表示浮点数的规则:
- 符号位 S = 0
- 尾数 M = 1001001(取小数点后的部分,即1.1001001 的1001001部分 )
- 基数 R = 2
- 指数 E = 4 (100)
浮点数的 IEEE754 标准表示
浮点数因为定义规则的不同,导致范围和精度都是不一致的:
- 指数位越多,则尾数位越小,表示的范围越大,但是精度也会变低
- 指数位越少,则尾数位越多,表示的范围越小,但是精度也会变高
而早期各计算机厂商(如 IBM、微软)都会定义自己的一套浮点数规则,就会导致同一个程序在不同厂商的计算机下做浮点数运算时,必须「先转换」成此厂商规定的浮点数格式,才能计算,则加重了计算成本
因此业界迫切需要统一的浮点数标准,1985年 IEEE 组织提出了 「IEEE754 浮点数标准」,其统一定义了浮点数的表示形式:
单精度浮点数 float:32 位,符号位 S 占 1 bit,指数 E 占 8 bit,尾数 M 占 23 bit
双精度浮点数 float:64 位,符号位 S 占 1 bit,指数 E 占 11 bit,尾数 M 占 52 bit
为了让其表示范围,精度最大化,还对指数和底数做了下面的规定:
- 因为尾数 M 的第一位总是 1(因为 1 <= M < 2),因此这个 1 可以省略不写,它是个隐藏位,这样单精度 23 位尾数可以表示了 24 位有效数字,双精度 52 位尾数可以表示 53 位有效数字
- 因为指数 E 是个「无符号」整数,表示 float 时,一共占 8 bit,所以它的取值范围为 0 ~ 255,但因为指数可以是负的,所以规定在存入 E 时在它原本的值 [ 加上一个中间数 127],这样 E 的取值范围为 -127 ~ 128,表示 double 时,一共占 11 bit,存入 E 时加上中间数 1023,这样取值范围为 -1023 ~ 1024(可以理解为指数位表示的值,需要减去127 和 1023)
除了规定尾数和指数位,还做了以下规定,如下图:
- 指数 E 非全 0 且非全 1:规格化数字,按上面的规则正常计算
- 指数 E 全 0,尾数非 0:非规格化数,尾数隐藏位不再是 1,而是 0(M = 0.xxxxx),这样可以表示 0 和很小的数
- 指数 E 全 1,尾数全 0:正无穷大/负无穷大(正负取决于 S 符号位)
- 指数 E 全 1,尾数非 0:NaN(Not a Number)
当指数全部为 1 的时候,尾数全 0 表示正负无穷大,尾数不全为 0 表示一个非法的数
指数全部为 0 时,此时尾数的基数变为 0,表示一个很小的数
注意上面规则中,没有明确的规则表示 0 的 float 值,可以认为浮点类型可以从正负方向无限接近于0,但是不能明确表示出0
将100.125 转换为浮点数的二进制表达示例:
使用程序验证:
bool printfloatBinary() {
union floatUnion {
float a;
uint32_t value;
};
union floatUnion fv;
fv.a = 100.125;
std::cout << std::showbase << std::setprecision(6) << "float a:" << fv.a << " binary:" << std::hex << fv.value << std::endl;
fv.a = 123.456;
std::cout << std::showbase << std::setprecision(6) << "float a:" << fv.a << " binary:" << std::hex << fv.value << std::endl;
fv.a = 100.12;
std::cout << std::showbase << std::setprecision(6) << "float a:" << fv.a << " binary:" << std::hex << fv.value << std::endl;
return true;
}
结果如下:
float a:100.125 binary:0x42c84000
float a:123.456 binary:0x42f6e979
float a:100.12 binary:0x42c83d71
浮点数为什么有精度损失
如果想要用浮点数表示小数 0.2 ,会发生循环的现象
所以 0.2(D)= 0.00100...(B)
因为十进制的 0.2 无法精确转换为二进制小数,而计算机在表示一个数字的时候,宽度是有限的,无限循环的小数存储在计算机中的时候,会被截断,所以会产生小数精度发生损失的情况
浮点数表示的精度范围是多大
单精度浮点数(32bit)可以表示的最大数的二进制形式 1.111111(小数点后23个1)* 2^127,二进制的 1.111111 约等于 2,所以float 能表示的最大数是 3.4* 10^38,即 32bit float 的表示范围是 -3.4 * 10^38 ~ 3.4 * 10^38
它能表示的精度有多小呢?
float 能表示的最小二进制数为 0.0000….1(小数点后22个0,1个1),用十进制数表示就是 1/2^23
1/2^23 = 0.00000011920928955078125 约等于0.00000012 也就是小数点后 7 位
用同样的方法可以算出,double 能表示的最大二进制数为 +1.111…111(小数点后52个1) * 2^1023 ≈ 2^1024 = 1.79 * 10^308,所以 64bit double 能表示范围为:-1.79 * 10^308 ~ +1.79 * 10^308
double 的最小精度为:0.0000…1(51个0,1个1),用十进制表示就是 1/2^52
整数值可以和 0 直接比较,浮点类型无法和 0.0f 直接比较,浮点类型只是从正负方向无限接近 0 值,故可以设置一个精度范围,该范围内都可以认为是0值
const float ESP = 1E-6f;
if( x >= -ESP && x <= ESP) {
// x equal to 0
}
浮点类型的存储存在精度损失,所以和浮点字面量比较的时候,要特别注意字面值,浮点数不加后缀默认是 double 类型,加上f表示 float 类型,加上L表示 long double 类型
参考博客:
https://blog.csdn.net/jiaoyangwm/article/details/129296459
https://blog.csdn.net/weixin_46039719/article/details/122903907