信息的表示(二)
整数的扩展与截断
编写C语言的代码时常见的问题之一就是不同字长的整数相互转换直接容易引发潜在的错误。例如低字长整数转换为高字长整数会发生隐式位扩展,而高字长整数转换为低字长整数会发生隐式位截断。可能你会疑惑为什么这里只有整数而没有浮点数,这是因为浮点数在位模式上采用了与整数不同的表示方式,这在之后会讨论。
位扩展
在对整数进行位扩展时,对无符号数进行的是零扩展,对有符号数进行的是符号扩展。
无符号数
无符号数进行的是零扩展,顾名思义就是往高位添加0,例如将8位扩展至16位有如下表现:
数字 | 扩展前 | 扩展后 |
---|---|---|
7 | 0000 0111 | 0000 0000 0000 0111 |
39 | 0010 0111 | 0000 0000 0010 0111 |
132 | 1000 0100 | 0000 0000 1000 0100 |
200 | 1100 1000 | 0000 0000 1100 1000 |
有符号数
有符号数进行的是符号扩展,也就是将最高位使用符号位进行扩展,例如将8位扩展至16位有如下表现:
数字 | 扩展前 | 扩展后 |
---|---|---|
7 | 0000 0111 | 0000 0000 0000 0111 |
39 | 0010 0111 | 0000 0000 0010 0111 |
-124 | 1000 0100 | 1111 1111 1000 0100 |
-56 | 1100 1000 | 1111 1111 1100 1000 |
位截断
在对整数进行位截断是,无符号数和有符号数具有相同的表现,只是对有符号数我们仍然需要将最高位看作符号位。
无符号数
将无符号数截断采用的是直接截断,例如将16位无符号数截断位8位无符号数有如下表现:
数字 | 截断前 | 截断后 |
---|---|---|
7 | 0000 0000 0000 0111 | 0000 0111 |
39 | 0000 0000 0010 0111 | 0010 0111 |
132 | 0000 0000 1000 0100 | 1000 0100 |
200 | 0000 0000 1100 1000 | 1100 1000 |
有符号数
对有符号数截断也同样是直接截断,但仍然需要将最高位看作符号位,例如将16位有符号数截断位8位有符号数有如下表现:
数字 | 截断前 | 截断后 |
---|---|---|
7 | 0000 0000 0000 0111 | 0000 0111 |
39 | 0000 0000 0010 0111 | 0010 0111 |
-124 | 1111 1111 1000 0100 | 1000 0100 |
-56 | 1111 1111 1100 1000 | 1100 1000 |
你可能会发现以上的截断均保持了数字大小的不变,这是因为选择的数字比较合理,你可以轻松的尝试出几个截断后会造成数据大小改变的例子,例如:
数字 | 截断前 | 截断后 |
---|---|---|
-2048 | 1111 1000 0000 0000 | (0000 0000)\(_{2}\) 0\(_{10}\) |
1904 | 0000 0111 0111 0000 | (0111 0000)\(_{2}\) 112\(_{10}\) |
770 | 0000 0011 0000 0010 | (0000 0010)\(_{2}\) 2\(_{10}\) |
32767 | 0111 1111 1111 1111 | (1111 1111)\(_{2}\) -1\(_{10}\) |
整数运算
加法
对无符号数和有符号数执行加法时,它们在位模式上有相同的表现,同样对于有符号数要将最高位看作符号位。对\(-2^{w-1} \le x,y \le 2^{w-1}-1\)的两个数字进行相加是,显然可能造成溢出,对于溢出的解决办法是将溢出部分截断,仅仅保留低\(w\)位。例如对4位数的加法有如下表现:
注:括号下标为2表示是二进制数,下标为t表示是补码,下表为u表示是无符号数
x | y | x+y截断前 | x+y截断后 |
---|---|---|---|
(1000)\(_2\) (-8)\(_t\) (8)\(_u\) | (1011)\(_2\) (-5)\(_t\) (11)\(_u\) | (10011)\(_2\) (-13)\(_t\) (19)\(_u\) | (0011) (3)\(_t\) (3)\(_u\) |
(1000)\(_2\) (-8)\(_t\) (8)\(_u\) | (1000)\(_2\) (-8)\(_t\) (8)\(_u\) | (10000)\(_2\) (-16)\(_t\) (16)\(_u\) | (0000) (0)\(_t\) (0)\(_u\) |
(1000)\(_2\) (-8)\(_t\) (8)\(_u\) | (0101)\(_2\) (5)\(_t\) (5)\(_u\) | (11101)\(_2\) (-3)\(_t\) (29)\(_u\) | (1101) (-3)\(_t\) (13)\(_u\) |
(0010)\(_2\) (2)\(_t\) (2)\(_u\) | (0101)\(_2\) (5)\(_t\) (5)\(_u\) | (00111)\(_2\) (7)\(_t\) (7)\(_u\) | (0111) (7)\(_t\) (7)\(_u\) |
(0101)\(_2\) (5)\(_t\) (5)\(_u\) | (0101)\(_2\) (5)\(_t\) (5)\(_u\) | (01010)\(_2\) (10)\(_t\) (10)\(_u\) | (1010) (-6)\(_t\) (10)\(_u\) |
减法与加法相同,只是要将减数当成负数做加法即可。只要从位的模式上来看待数字的加法,就不容易因为各种溢出的情况而计算出错。
求反
对无符号数求反我们有如下定义:
对满足\(TMin_w \le x \le TMax_w\)的\(x\),其补码求反我们有如下定义:
看上去两个定义差别很大,但实际上对于无符号数和补码求反在位模式上是相同的,均为对每一位求反再加1。例如以下:
无符号数
\(\vec x\) | ~\(\vec x\) | \(incr(\) ~ \(\vec x)\) |
---|---|---|
0101 5 | 1010 10 | 1011 11 |
0111 7 | 1000 8 | 1001 9 |
1100 12 | 0011 3 | 0100 4 |
0000 0 | 1111 15 | 0000 0 |
1000 8 | 0111 7 | 1000 8 |
有符号数
\(\vec x\) | ~\(\vec x\) | \(incr(\) ~ \(\vec x)\) |
---|---|---|
0101 5 | 1010 -6 | 1011 -5 |
0111 7 | 1000 -8 | 1001 -7 |
1100 -4 | 0011 3 | 0100 -4 |
0000 0 | 1111 -1 | 0000 0 |
1000 -8 | 0111 7 | 1000 -8 |
乘法
对于无符号和补码的乘法来说,位模式的表现都是一样的,对一个字长为\(w\)的数字\(x\),结果均只保留低\(w\)位,高位直接截断,但是对于位模式来说,直接乘法运算要比加法运算消耗更多的时间,因此在许多的编译器中,乘法运算都将会被优化为移位运算。例如\(x *8\)将会被编译器优化为\(x \ll 3\) ,即表示\(x * 2^3\),同理\(x*15\) 将会被优化为\(x\ll3 + x \ll 2 + x \ll 1 + x \ll 0\),即表示\(x*2^3+x*2^2+x*2^1+x*2^0\)。有一下示例:
模式 | \(x\) | \(y\) | \(x*y\) | 截断后的\(x*y\) |
---|---|---|---|---|
无符号 | 5 101 | 3 011 | 15 001111 | 7 111 |
补码 | -3 101 | 3 011 | -9 110111 | -1 111 |
无符号 | 4 100 | 7 111 | 28 011100 | 4 100 |
补码 | -4 100 | -1 111 | 4 000100 | -4 100 |
无符号 | 3 011 | 3 011 | 9 001001 | 1 001 |
补码 | 3 011 | 3 011 | 9 001001 | 1 001 |
对于除法,与乘法类似,例如乘法在位模式上是往左移位,而相似的除法就是向右移位。