题目出自:《深入理解计算机系统》
【题目内容】
写出具有如下原型的函数的代码:
/* Addition that saturates to TMin or TMax•/
int saturating_add(int x, int y);
同正常的补码加法溢出的方式不同,当正溢出时,饱和加法返回 TMax,负溢出时,返回 TMin。饱和运算常常用在执行数字信号处理的程序中。
在程序中,你可以使用整型常数 INT_MAX 和 INT_MIN 来代表 TMax 和 TMin。
程序的其他部分已经为你准备好。函数应该遵循位级整数编码规则。
位级整数编码规则:
假设:
- 整数用补码形式表示;
- 有符号数的右移是算术右移;
- 数据类型 int 是 w 位长的。对于某些题目,会给定 w 的值,但是在其他情况下,只要 w 是 8 的整数倍,你的代码就应该能工作。你可以用表达式 sizeof(int) << 3 来计算 w。
禁止使用:
- 条件语句(if 或者 ?:)、循环、分支语句、函数调用和宏调用;
- 除法、模运算和乘法;
- 相对比较运算(>、<、≤ 和 ≥)。
允许的运算:
- 所有的位级和逻辑运算;
- 左移和右移,但是位移量只能在 0 到 w−1 之间;
- 加法和减法;
- 相等(==)和不等(!=)测试。(在有些题目中,也不允许这些运算);
- 整型常数 INT_MAX 和 INT_MIN;
- 对 int 和 unsigned 进行强制类型转换,无论是显式的还是隐式的。
题目分析:
我们要实现饱和加法,要知道饱和的情况。我们知道,当两个很大的int相加时,结果可能会超出int范围,变成一个负数,同理两个很小的负数相加也可能会变成一个正数,int的范围是-2147483648~2147483647(即-2^31 ~ 2^31 - 1),int是一个循环,超出了INT_MAX就从INT_MIN继续往上数。
正溢出的情况是:正 + 正 = 负,负溢出:负 + 负 = 正
正数符号位是0, 负数符号位是 1。所以可以先求出x, y , x + y 的符号位,来判断释放有溢出,但是又不能使用分支语句,只能想办法用位运算来实现。所以可以要想办法构造两个掩码,一个标记正溢出,一个标记负溢出,掩码是全1或者全0,这样与INT_MAX,INT_MIN 按位与之后就可以要么不变,要么变为0,再加上未溢出的情况,就有三个结果,它们要么是不变要么是0,把这三个数按位或上就可以得到最终答案。
return (mask_pos & INT_MAX) | (mask_neg & INT_MIN) | ((~mask_pos & ~mask_neg) & sum);
掩码全1就是有溢出,全0就是未溢出。
符号位可以通过左移31位得到。(x_sign, y_sign, sum_sign)
溢出标志(正溢出为例):(x_sign == 0) && (y_sign == 0) && (sum_sign == -1)
进一步变形就是:pos_overflow = (x_sign | y_sign == 0) & (sum_sign == -1);
得到的结果要么是1,要么是0。取负就可以得到全1或者全0。至此,就可以做出来了
#include <stdio.h>
#include <limits.h>
int saturating_add(int x, int y) {
int sum = x + y;
int w = sizeof(int) << 3;//求int的位数
int x_sign = x >> (w - 1);// 得到x的符号位
int y_sign = y >> (w - 1);// 得到y的符号位
int sum_sign = sum >> (w - 1);// sum的符号位
//判断是正溢出还是负溢出
int pos_overflow = (x_sign | y_sign == 0) & (sum_sign == -1);
int neg_overflow = (x_sign & y_sign == -1) & (sum_sign == 0);
//得到掩码
int mask_pos = -pos_overflow;
int mask_neg = -neg_overflow;
return (mask_pos & INT_MAX) | (mask_neg & INT_MIN) | (~(mask_pos & mask_neg) & sum);
}
int main() {
int x, y;
scanf("%d %d", &x, &y);
printf("%d\n", saturating_add(x, y));
return 0;
}