系列文章
文章目录
2.2整数表示
- 两种表示方法:无符号表示与补码表示
- 很多语言比如Java 就只支持有符号数,C语言的默认类型是有符号数
2.2.1 整数数据类型
- C语言中的整数类型和定义的每种数据类型所能表示的最小范围。
- 所以在16位机器下,int的大小可能是两字节
- C语言标准定义的数据范围是对称的,实际情况通常是负数范围比正数范围大1
2.2.2 无符号数的编码
- 如果一个整数类型有4个位,我们将其看作是一个4位的位向量
比如:1011这个四位整数
1101 —> [1,1,0,1] = 1 x 2 ^ 3 + 1 x 2 ^ 2 + 0 x 2 ^ 2 + 1 x 2 ^ 0
= 8 + 4 + 0 + 1 = 13;
- 无符号表示、补码表示与数据的映射都是双射,即一一对应。
2.2.3 补码编码
- 补码的定义实际就是将符号位解释为负权。
- 比如:1011这个四位整数
1101 —> [1,1,0,1] = -1 x 2 ^ 3 + 1 x 2 ^ 2 + 0 x 2 ^ 2 + 1 x 2 ^ 0
= -8 + 4 + 0 + 1 = -3;
其实唯一变化的就是第一位数的含义。 - 无符号表示、补码表示与数据的映射都是双射,即一一对应。
2.2.4 有符号数和无符号数之间的转换
- C语言中,这些转换大多是从位级角度而不是数的角度。
所以在有符号数与无符号数之间进行强制类型转换的结果是保持位值不变,只改变解释位的方式。
- 补码 x 转无符号x(x是一个w位的数)
x >= 0时,值不变
x < 0,转换后的值为 2 ^ w + x(也就是一个很大的正数,因为有符号数中的负标志位变成了2 ^ w) - 无符号数 x 转补码
x < 2^(w-1),值不变
x >= 2^(w-1),转换后的值为 x - 2 ^ w
这种情况下,x 的第一位值是1,转换后这个很大的数变成负数.
2.2.5 C 语言中的有符号数和无符号数
- 有符号数
有符号数可以表示正数、负数和零。C 语言中的有符号整数类型包括 signed char、short、int、long 和 long long。(char是模糊的,所以加signed,2.1中有提过)
有符号整数的范围取决于位宽和补码表示。对于一个
𝑤位的有符号整数,其范围是:
−2 ^ (𝑤−1) 到 2 ^ (𝑤−1) − 1(最大负数:1000 0000) - 无符号数
无符号数只能表示非负整数。C 语言中的无符号整数类型包括 unsigned char、unsigned short、unsigned int、unsigned long 和 unsigned long long。
𝑤位的无符号整数,其范围是:
0 到 2 ^ 𝑤 − 1 - C语言的计算规则
整型提升:如果操作数的类型小于 int,提升到 int 或 unsigned int。
无符号数优先:如果一个操作数是无符号的,另一个是有符号的且无符号数的类型大于或等于有符号数的类型,那么有符号数会被转换为无符号数。
2.2.6 扩展一个数字的位表示
- 无符号数的零扩展(Zero Extension),即在最高位前加 0。
- 有符号数的符号扩展(Sign Extension),即在最高位前加最高有效位的值(符号位)。如果这个数本身是正数,符号位也就是0,也就和无符号一样了。
2.2.7 截断数字
- 截断无符号整数:直接丢弃高位的比特。
- 截断有符号整数:截断操作同样是丢弃高位的比特,但需要注意截断后的符号位应与截断前的一致。对这个数取余(模运算)。
2.2.8关于有符号数和无符号数的建议
1.大多数时候,数值运算中很多的隐性转换难以察觉,决不使用无符号数
2. 另一方面,仅仅将字看作位的集合时,无符号数是非常有用的比如Linux系统中的信号就是位图(当作布尔值),一些数学上的模运算等等。
2.3整数运算
2.3.1 无符号加法
- 结果(正)溢出或正常
- C 语言不会将溢出作为错误发出信号
- 溢出的结果:结果会模上 2 ^ w ,最终结果小于两个加数
- 两个无符号整数 𝑎和 𝑏,如果 𝑎+𝑏 小于 𝑎或𝑏,则发生了溢出。
if (result < a || result < b)
2.3.2 补码加法
- 结果正溢出,正常,负溢出
- 如果两个正数相加结果为负数,则发生正溢出。
如果两个负数相加结果为正数,则发生负溢出。
2.3.3 补码的非
- 非(NOT)运算是一种简单的按位取反操作,非运算会将其二进制表示的每一位从0变为1,1变为0。
- 补码非的位级表示:对每一位求补(就是取反,0变1,1变0),结果再加 1
- 计算补码非的第二种方法:假设 k 是最右边的 1 的位置,对 k 左边的所有位取反
2.3.4 无符号乘法
- 结果需要2w位来表示才能不溢出
- C语言中无符号乘法的结果 (x * y) %(2 ^ w)(发生截断,2.2.7.2)
2.3.5 补码乘法
- 确定操作数的符号:检查两个操作数的符号位。符号不同,结果为负;符号相同,结果为正。
- 将操作数转换为正数:如果操作数是负数,将其转换为正数(取补码:取反并加1)。
- 执行无符号乘法:对转换后的正数进行无符号乘法。
- 处理结果的符号:根据1确定的结果符号,如果结果应为负数,将乘积取补码。
- 可以认为补码乘法和无符号乘法的位级表示是一样的
2.3.6 乘以常数
- 大多数机器上,整数乘法需要 10 个或更多的时钟周期,而加法、减法、位级运算和移位只需要 1 个时钟周期
- 编译器尝试用移位和加法或减法运算的组合来代替常数因子的乘法,以提高效率。
左移 k 位等于乘以 2^k 的无符号乘法
x * 14 = (x<<3) + (x<<2) + (x<<1)
y * 14 = (x<<4) - (x<<1)
x * 14 = (x <<3)+(x<<2)+(x<<1) = (x<<4) -(x<<1)
判断如何移动的方式很简单:14 的位级表示为 1110,所以分别左移 3,2,1。或者取反14表示为0001,左移4减去左移1。
2.3.7 除以 2 的幂
- 大多数机器上,整数除法更慢,需要 30 个或更多的时钟周期。
- IEEE754标准的四种舍入模式
就近舍入:四舍五入4.4 = 4,-4.6 = -5。
朝0舍入:直接截尾即可,4.5=4,-4.5=-4。
朝正无穷舍入:正数进位,负数截位。4.1=5,-4.9=-4。
朝负无穷舍入:负数进位,正数截位。4.9=4, -4.1=-4;
-
无符号数的右移运算
逻辑右移操作将数的所有位向右移动,并在左边补 0。
例如5>>1 (0000 0101>>1) = 0000 0010 = 2;
对于正数,算术右移和逻辑右移的效果是相同的,因为高位补 0 不影响结果。但对于负数,算术右移保留了符号位(即最高位),这意味着结果将保持负数的性质,并且向下舍入(和5/2的结果一致)。 -
有符号数的算术右移运算
算术右移操作将数的所有位向右移动,并在左边补上符号位(即原来的最高位)。对于有符号数,算术右移的结果相当于进行除法运算后向下舍入(即向负无穷舍入)。
假设我们有一个负数 x=−5:
x=−5 的补码表示是1111 1011
x>>1(算术右移 1 位):1111 1011→1111 1101 结果是十进制的 -3,正好是 并向下舍入。 -
向零舍入:使用公式
(x+(1<<k)−1)>>k 可以实现除法运算并向零舍入。
这个公式的作用是将数加上一个偏移量,然后再进行右移操作,从而实现向零舍入的效果。这个偏移量是 (1<<k)−1,它在右移操作之前调整了数值,使得结果向零舍入。
假设我们有一个有符号数
x=−5 并且 k=1:
(x+(1<<k)−1)>>k=(−5+2−1)>>1=−4>>1
−4 的补码表示是 1111 1100
−4>>1(算术右移 1 位): 1111 1100→1111 1110 结果是十进制的 -2,这与
−5÷2并向零舍入的结果一致。
2.3.8 关于整数运算的最后思考
- 补码使用了与无符号算术运算相同的位级实现,包括加法、减法、乘法甚至除法。都有完全一样或非常类似的位级行为
- 不同语言对整数的定义并不完成相同,比如JAVA中没有无符号的整数,python和c在负数除法的取整方式不同,使用过程中需要注意消除这些隐性的差别带来的不确定性结果。