文章目录
一个展示各种类型的机器数表示的在线网站:http://www.binaryconvert.com/index.html
定点数
定点数的表示
定点小数本质上和定点整数没有区别,这里只讨论定点整数
基本概念:
在计算机中参与运算的机器数有两大类:
-
无符号数:没有符号位,机器数就是真值的二进制表示
-
有符号数:最高位为符号位,0表示正数,1表示负数【移码特殊,1正0负】
原码、反码、补码、移码是计算机对于定点数的几种表示方法
-
原码是真值直观的机器数表示,用机器数的最高位表示该数的符号,其余的各位表示数的绝对值,零表示不唯一。
-
补码是原码全部取反后加一,将减法运算转换成加法运算,零表示唯一。
原码表示法的加减法操作比较复杂,对于两个不同符号数的加法(或同符号数的减法),先要比较两个数的绝对值大小,然后用绝对值大的数减去绝对值小的数,最后还要给结果选择合适的符号。
-
反码是原码全部取反,零表示不唯一
-
移码是用真值 X \rm X X 加上一个常数,这个数通常取 2 n 2^n 2n,通常用来表示浮点数的阶码,只能用于表示整数,零表示唯一
详细见下方 “一点探究”
定点数的运算
移位运算
-
算数移位(针对有符号数)
-
原码:高低位补 0
-
反码:高低位补符
-
补码:高位补符,低位补 0【可见补码右侧(从右往左数第一个1右边)与原码性质相同,左侧与反码性质相同】
-
-
逻辑移位(针对无符号数)
- 全补0
-
循环移位
符号扩展(Type Cast 类型转换)
假设从A类型 -> B类型,如果B类型包含A类型能表示的所有数,简单说就是从“小”转“大”,就可以进行无损(无精度丢失)转换
char -> int -> long -> double,float -> double
虽然书上是说从 float 到 double 转换过程中没有损失/误差,但如果一个小数是没办法有二进制表示出来的时候,这个转化不算是损失,但也是有误差的,不过做题的话还当做没有损失吧
int main() {
float f = 1.25; double d = 1.25;
cout << (d == (double)f) << endl;
float f1 = 1.2;
double d1 = 1.2, d2 = f1;
cout << (d1 == (double)f1) << endl;
cout << (d1 == d2) << endl;
cout << d1 << endl;
cout << d2 << endl;
cout << (f1 == (float)(double)f1) << endl;
return 0;
}
溢出判断
定点数溢出
-
上溢:大于机器所能表示的最大正数
-
下溢:小于机器所能表示的最小负数
浮点数溢出
- 上溢会引发中断
- 下溢做机器零处理
关于浮点数溢出的定义网上群魔乱舞,这里我相信王道资料写的
溢出判断方法
-
参加操作的两个数符号相同,结果与操作数符号不同,即为溢出
-
最高位与次高位进位相同则无溢出,否则溢出
-
双符号位时(变形补码,模四补码),相同无溢出;不同则溢出。此时最高位符号代表真正符号
-
01表示正溢出:两个正数相加导致的溢出
-
10表示负溢出:两个负数相加导致的溢出
-
其它
-
发生进位/借位,标志位
CF = 1
-
发生溢出 标志位
OF = 1
-
无符号数一般不讨论溢出的问题,只有进位/借位问题
-
无符号数需要进位/借位的简单计算方法 P56
加减乘除运算
…
数据的存储和排列
小端存放
将最低有效字节存储在地址编号最小的位置,就是我们感觉上的“倒着存“
比如一个 int
类型的变量机器数为 01234567H
,i
的地址为 00H
,则对应关系如下
00H | 01H | 02H | 03H |
---|---|---|---|
67H | 45H | 23H | 01H |
“倒过来存”是针对最小数据单元,int
整数是符合小端存储的,string
字符串并不符合,因为字符串一个最小数据单元就一个字节,那还往哪儿倒呢(汉字另说)
边界对齐
数据不按边界对齐方式存储时,可以充分利用存储空间,但半字长或字长的指令可能会存储在两个存储字中,此时需要两次访存,并且对高低字节的位置进行调整、连接之后才能得到所要的指令或数据,影响了指令的执行效率。
浮点数
浮点数的表示
普通浮点数格式
N = r E × M N=r^E\times M N=rE×M
-
基数 r r r
- 基数变大,表示范围越大,发生因为对阶或者尾数溢出或者规格化的次数显著减少,运算速度可以提高,但精度变低
-
尾数 M M M
-
阶码 E E E
IEEE754格式(以单精度为例)
( − 1 ) S × 1. M × E − 127 (-1)^{S} \times 1 . M \times^{E-127} (−1)S×1.M×E−127
符号位 S:1位
尾数M:23位(隐藏一位),原码表示
阶码 E:8位,移码表示,偏移值为127(阶码的范围是1~254),一些规定:
-
阶码全0,尾数全0,表示 0
-
阶码全1,尾数全0,表示无穷大(符号位为 0 表示正无穷,为 1 表示负无穷)
-
阶码全0,尾数不为0,表示非规格化数
-
阶码全1,尾数不为0,表示NaN(非数值)
规格化浮点数
-
尾数最高位必须是一个有效值
-
左规与右规
-
原码规格化
- 尾数最高位一定是1(IEEE754浮点数尾数隐藏了一位整数1,所以看起来好像IEEE754不满足规格化)
-
补码规格化
- 尾数最高位一定和符号位相反
浮点数的运算
对阶
先求阶差,然后小阶向大阶看齐
尾数求和
尾数相加
规格化
- 左规
- 右规
要分清楚“对阶”和“右规”,虽然“对阶”也是右移、阶码+1,也要考虑舍入。但它们是两种操作,对阶的右移往往会进行多次,而右规的右移一般只要一次(除非采用“0舍1入”导致再次溢出)
舍入
右规和对阶之后可能需要舍入
-
0舍1入法
- 右规可能需要多次
-
恒置1法
溢出判断
浮点运算中,运算结果超出尾数表示范围却不一定溢出,只有规格化后阶码超出所能表示的范围时,才发生溢出
-
阶码上溢:进入中断处理
-
阶码下溢:按机器0处理
-
溢出
整数运算在除数为0时会报错,而浮点数运算在除数为0时,不会报错,但会返回几个特殊值:
double d1 = 0.0 / 0; // NaN Not a Number double d2 = 1.0 / 0; // Infinity 无穷大 double d3 = -1.0 / 0; // -Infinity 负无穷大
一点问题
对于位数相同的定点数和浮点数,可表示的浮点数个数比定点数个数多吗?
一样多,可表示的数据个数取决于编码所采用的位数。编码位数一定,编码出来的数据个数就是一定的。n位编码只能表示2n个数,所以对于相同位数的定点数和浮点数来说,可表示的数据个数应该一样多(有时可能由于一个值有两个或多个编码对应,编码个数会有少量差异)。
一点探究
关于原码、补码、反码、移码
最开始学计组的时候,总以为是 原码->反码->补码 这个过程,今天看了唐朔飞的《计算机组成原理》,又查了英文才发现不该那么理解
原码 | True Form |
---|---|
补码 | Complement |
反码 | radix-minus-one complement |
base minus one’s complement |
反码的英文直译过来就是 “一个数的补码形式剪去1”,也就是说这个过程应该是 原码->补码->反码
Q1:补码是“补”的什么?
例如,时钟指示6点,想让它指示3点,那么我们可以顺时针转9圈,也可是逆时针转3圈,转换成数学语言就是
−
3
≡
+
9
(
m
o
d
12
)
-3\equiv +9\ (mod\ 12)
−3≡+9 (mod 12)
对于
m
o
d
12
\mod 12
mod12 而言,
−
3
-3
−3 和
9
9
9 互为补数。
这个思想转换到定点数的表示里,就是一个 n n n 为负数可以用它的正补数来代替
如下:
#include <iostream>
using namespace std;
int main(){
short a = -20;
unsigned short b = a;
cout << a + 65536 << endl;//两者是相同的
cout << b << endl;
}
自己的理解,所谓的有符号数,其实是用 [ 0 , 2 n − 1 − 1 ] [0,2^{n-1}-1] [0,2n−1−1] 来表示正数,用 [ 2 n − 1 , 2 n − 1 ] [2^{n-1},2^{n}-1] [2n−1,2n−1] 来表示负数,两个区间都是 2 n − 1 2^{n-1} 2n−1 个数
根据教材补码的定义,换种说法:
对于任何 n 位有符号数
a
a
a ,如果
a
a
a 是负数,
[
2
n
−
1
,
2
n
−
1
]
[2^{n-1},2^n-1]
[2n−1,2n−1] 内一定存在一个数
b
b
b,使得
b
=
a
+
2
n
b=a+ 2^n
b=a+2n
此时 b b b 的原码就是 a a a 的补码形式。
Q2:为什么负数表示的范围比正数 “多一个” ?
还有一个问题,之前也会不理解,为什么负数表示的范围比正数 “多一个” 呢?
例如,int
类型最大值
2147483647
2147483647
2147483647,最小值
−
2147483648
-2147483648
−2147483648
**首先肯定不是多一个,正数区间包含 0 0 0,两个区间数的个数是一样多的。**之前看过一些解释,大概的意思如下
因为补码形式中 + 0 +0 +0 和 − 0 -0 −0 是相同的,为了不浪费,让 − 0 -0 −0 去表示 − 2 n -2^n −2n
其实也可以这么来看,比如八位有符号数的范围是 [ − 128 , 127 ] [-128,127] [−128,127] ,将 a = − 128 a=-128 a=−128 代入上面的式子 b = a + 2 n b=a+ 2^n b=a+2n, b b b 就是 2 7 2^7 27,那么就理所应当用 1000 0000 B 1000\ 0000B 1000 0000B ,也就是 − 0 -0 −0 去表示 − 2 n -2^n −2n
Q3:无符号数与有符号数如何运算?
无符号数与有符号数的运算,会把有符号数视为无符号数,然后进行运算
无符号数与正数运算,与无符号数和无符号数运算无异,除了正数的范围小了一点。
无符号数与负数运算,根据这个定义
对于任何 n 位有符号数 a a a ,如果 a a a 是负数, [ 2 n − 1 , 2 n − 1 ] [2^{n-1},2^n-1] [2n−1,2n−1] 内一定存在一个数 b b b,使得
b = a + 2 n b=a+ 2^n b=a+2n此时 b b b 的原码就是 a a a 的补码形式。
假设有一个无符号数为
A
A
A 和一个负数
a
a
a,现在做运算
A
+
a
A+a
A+a 运算,就等同于
A
A
A 和
a
a
a 的补数
b
b
b 相加,
m
o
d
2
n
\mod 2^n
mod2n 是因为结果可能大于
2
n
2^n
2n (无符号数对于超过最大值的处理方式是直接舍去高位,即运算结果
m
o
d
2
n
\mod 2^n
mod2n )
A
+
a
≡
(
A
+
a
+
2
n
)
m
o
d
2
n
≡
(
A
+
b
)
m
o
d
2
n
A+a\equiv(A+a+2^n)\mod 2^n \equiv(A+b)\mod 2^n
A+a≡(A+a+2n)mod2n≡(A+b)mod2n
Tips1:相反数间补码关系
int x = 10;
int y = ~x + 1;//此时y是-10
对于任何一个数 x x x ,将 x x x 的补码连带符号位取反后 + 1 + 1 +1,是 − x -x −x 的补码。
这个对 0 0 0 也是成立的
Tips2:移补码转换技巧
书上定义对比(当移码常数取
2
n
2^n
2n 时)
[
x
]
补
=
x
(
x
≥
0
)
[
x
]
补
=
2
n
+
1
+
x
(
−
2
n
≤
x
<
0
)
[
x
]
移
=
2
n
+
x
(
−
2
n
≤
x
<
2
n
)
[x]_补= x\ (x \ge 0)\\ [x]_补= 2^{n+1}+x\ (-2^n\le x < 0)\\ [x]_移 =2^n+x\ (-2^n \le x< 2^n )
[x]补=x (x≥0)[x]补=2n+1+x (−2n≤x<0)[x]移=2n+x (−2n≤x<2n)
联立有
[
x
]
补
+
2
n
=
[
x
]
移
(
x
≥
0
)
[
x
]
补
=
2
n
+
[
x
]
移
(
−
2
n
≤
x
<
0
)
[x]_补+2^n= [x]_移\ (x \ge 0)\\ [x]_补= 2^{n}+[x]_移\ (-2^n\le x < 0)\\
[x]补+2n=[x]移 (x≥0)[x]补=2n+[x]移 (−2n≤x<0)
所以移码和补码的符号只差一个符号位。
Tips3:补原码转换技巧
原码到补码(补码到原码做法一模一样)
从右边往左边看,遇见第一个1前照抄,然后后面的除了符号位全部取反
比如:
原码:10001110
补码:11110010
Tips4:补码1的个数与大小关系
根据在Q1中的定义, b = a + 2 n b=a+ 2^n b=a+2n ,对于越小的负数 a a a ,得到的 b b b 也就越小, b b b 的 1 1 1 就越少
补码正数和负数在“1的个数”与“大小关系”上是一样的
正数:0000 (0)最小,0111 (15)最大
负数:1000 (-16)最小,1111 (-1)最大
-
模四补码(变形补码,双符号位)
- 更容易检测加减运算中的溢出问题
- 存储模四补码中需要一个符号位,只有在把两个模四补码的数送至ALU中完成加减、移位运算时,才需要双符号位,即只在ALU中才有双符号位这一说
- 高符号位代表真正的符号,低符号位参与运算来判断是否发生溢出