CSAPP : Data Lab record
record
整数篇
bitXor
这个比较简单了,没什么好说的
int bitXor(int x, int y)
{
return (~(x & y) & ~(~x & ~y));
}
tmin
补码的数值计算式:\(-2^{31}·b_{31} + \sum_{i = 0}^{30} 2^i · b_i\)
所以当补码的最高位是1,后面的全都是0,可以得到补码表示法能表示的最小值 \(-2^{31}\)
其补码就是 (1 << 31)
int tmin(void)
{
//Tmin = 1000...0000
return 1 << 31;
}
isTmax
判断传入的数是不是最大数: 01111....111
具体的写到代码里面了
int isTmax(int x)
{
//Tmax 的性质-> ~x = x + 1
//注意到 -1 同样也有这个性质
//所以判断 !(x + 1) 来排除 -1 的情况
int y = x + 1;
return !((~x ^ y) | (!y));
}
allOddBits
判断当前数字其奇数上面是否为全部为1,特别注意最低位是第0位
注意到 \(A_{(16)} = 10_{(10)} = 1010_{(2)},AA = 1010101_{(2)}\),一个十六进制字符占据着四个二进制字节,以此我们扩展到32位,就是 \(AAAAAAAA_{(16)}\) 就是我们要找的一个标准数字\(\text{mask}\),然后我们用传入这个数与mask做 & 运算,看看是否与mask 相等
int allOddBits(int x)
{
int mask = 0xAA | (0xAA << 8);
mask = mask | (mask << 16);
return !(mask ^ (mask & x));
}
negate
最经典的求解负数,经典公式
int negate(int x)
{
return ~x + 1;
}
isAsciiDigit
判断这个字符是不是满足 \(0x30\leq x \leq 0x39\)
其实就是判断 x - 0x30和0x39 - x 是不是负数罢了,x - 0x30 = x + (-0x30) = x + (~0x30 + 1)
int isAsciiDigit(int x)
{
int tmin = 1 << 31;
int inf = x + (~0x30 + 1); //x - 0x30
int sup = 0x39 + (~x + 1); //0x39 - x
//检查符号位是否为0
return !((inf & tmin) | (sup & tmin));
}
conditional
返回条件语句 x?y:z;
首先将 x 变为最好分析的 0000000... 和 1....,使用 x = !x,则 x = 0 时将 x 转化为 1 ,反之将 x 转化为0,然后 x = -x (~x + 1),就能将 0 转化成0000000...,1转化成111111...,(特别注意这里的0/1和最开始是相反的!!!所以后面似 y & ~x 而不是 y & x)然后 y & ~x | z & x来判断该返回哪个
int conditional(int x, int y, int z)
{
x = !x; //x为0时变为1,x非0时变为0
x = ~x + 1; //x为0时变为0x00000000,x非0时变为0xFFFFFFFF
return (y & ~x) | (z & x);
}
isLessOrEqual
实现一个\(\leq\) 符号
令 \(diff = y - x = y + (~x + 1)\),我们只需要判断 x,y,diff 三者的符号
首先如果 tagx是1,tagy是0,那么x为负y为正,直接返回1
如果二者符号相同,并且diff的符号位是0,则diff非负,意味着$ 0\leq diff = y - x$,也返回1
int isLessOrEqual(int x, int y)
{
int diff = y + (~x + 1); //y - x
int tagx = (x >> 31) & 1; //x的符号位
int tagy = (y >> 31) & 1; //y的符号位
int tagdiff = (diff >> 31) & 1; //y - x 的符号位
//x和y符号位相同,则看y - x 的符号位
//x和y符号位不同,则看x的符号位
return (tagx & !tagy) | (!(tagx ^ tagy) & !tagdiff);
}
logicalNeg
这里是不用 ! 实现 ! 的操作,其实就是判断一个数是不是0
0的重要性质: 0 = -0 ,也就是说我们直接对 x 和 -x 判断其符号位是否相同就好了
这里涉及到了算术右移这个东西,突然发现自己对于 >> 这个符号的理解这么多年没有搞明白,记录一下:
对于有符号整数,C++执行的是算数右移:对于有符号整数 x ,每执行一次 >> 1,相当于在 x 左边加入 x 的符号位(正数补 0,负数补 1),最右边那个位直接舍去。用8位二进制数举个例子:
所以在这里这一步是\((x|(~x + 1)) >> 31\),
对于无符号整数,必须使用逻辑右移,也就是只会补0,没有补1的情况
这时候 如果 x 是 0 我们得到 0,x 非 0得到的是 -1
,最后加上1就好了 (!0 = 1,别忘了)
int logicalNeg(int x)
{
return ((x|(~x + 1)) >> 31) + 1;
}
howManyBits
在 Data Lab 题目,比如:
计算“某个数的最高有效位的位置”;
去掉“前导 0”或“前导符号位 1”;
比较“有符号/无符号”的大小。
这些地方说的“有效位”,就是指:
无符号数:去掉左边所有的 0,剩下的那段;
有符号数:
正数:同上,去掉左边的 0;
负数:去掉左边多出来、只用于符号扩展的那一串 1,只保留真正能区分不同负数的那几位。
用人话说就是一些只是为了补足32位或者8位二进制的那些前导0和前导1是不算进去的
在这里还遇到了一个神秘的远古问题
dlc 这个检查工具非常古老,它强制使用 C89 标准。 在 C89 标准中,所有的变量声明必须写在函数的最最最前面,不能夹杂在代码中间
非负数找有效位数比较简单,找到最高位的1就好了,负数因为前导1的存在不能这么简单粗暴找
所以先把负数取反,这样取反后最高位的1(也就是取反前最高位的0),由于不能使用条件语句,我们统一把负数取反,按照正数的处理方式去完成,所以使用 x ^ (x >> 31) 就能完成这个取反了,(x >> 31) 这个东西刚刚在上一题才用了,所以不再细讲
接下来就是模拟一个类似二分/倍增法的过程,判断最高的前 16 8 4 2 1 位是否存在1,然后累加
int howManyBits(int x)
{
//首先我们要统一处理正数,所以先把负数转化成正数,其实就是取反,这样方便消去符号位
int tag = x >> 31;
int bit_0,bit_1,bit_2,bit_4,bit_8,bit_16;
//tag = 0000...0 = 0 ,当 x 非负, tag = 111...1111 = -1,x 是负数
x = x ^ tag; // tag = 0,x^tag = x; tag = -1, x^tag = ~x
//然后就是伟大的倍增法
bit_16 = !(!(x >> 16)) << 4; //是否高16位有1
x = x >> bit_16; //有1,则右移16位,否则不变,接下来以此类推
bit_8 = !(!(x >> 8)) << 3;
x = x >> bit_8;
bit_4 = !(!(x >> 4)) << 2;
x = x >> bit_4;
bit_2 = !(!(x >> 2)) << 1;
x = x >> bit_2;
bit_1 = !(!(x >> 1));
x = x >> bit_1;
bit_0 = x;
return bit_0 + bit_1 + bit_2 + bit_4 + bit_8 + bit_16 + 1;
//最后加1是因为符号位
}
浮点数篇
因为有点懒得把浮点数的那一大堆东西重新写一遍了,这部分有关浮点数的知识补足就让AI写了一下
浮点数的部分讲解
这里的讨论都是基于32位单精度浮点数 (float)
这部分内容的思维转换非常大。做整数题时,你是按照补码(Two's Complement)思考的;而做浮点数题时,你需要彻底切换到 IEEE 754 标准。
在 Data Lab 里,浮点数题目通常给你的参数是 unsigned uf,它代表内存里的 32 个 0/1。你需要把这 32 位拆解开来理解。
1. 宏观蓝图:32 位是如何划分的
内存里的 32 个 bit 被切成了三段:
| 31 (1位) | 30 ... 23 (8位) | 22 ... 0 (23位) |
|---|---|---|
| S (符号位) | exp (阶码) | frac (尾数) |
- S (Sign): 决定正负。0 是正,1 是负。
- exp (Exponent): 决定数量级(2 的多少次方)。
- frac (Fraction): 决定精度(具体的数值是多少)。
它们共同构成的数值公式通常长这样:
其中 \(M\) 是由 frac 算出来的有效数字(尾数),\(E\) 是由 exp 算出来的真实指数。
2. 这里的“坑”:三种状态
IEEE 754 并不是一套规则通吃,它根据 exp (中间那 8 位) 的值不同,分成了三种情况。这是你做题时必须判断的第一步。
情况一:规格化数值 (Normalized) —— 最普通的情况
特征:exp 的位模式既不是全 0,也不是全 1(即 exp 在 1 到 254 之间)。
这是绝大多数浮点数的样子。
-
真实的指数 E:
这里用了一个“偏置值(Bias)”的概念。\[E = exp - 127 \]- 例如
exp是 127 (二进制01111111),那么真实指数 \(E = 127 - 127 = 0\)。 - 例如
exp是 130,真实指数 \(E = 3\)。
- 例如
-
有效数字 M:
这里有个隐含的 1。
frac部分只存储了小数点后面的数字。计算时,要在前面自动补一个1.。\[M = 1.frac \]- 例如
frac是100...0(二进制),意思是 \(0.1\)(二进制小数),即 \(0.5\)(十进制)。 - 但实际的 \(M\) 是 \(1.1\)(二进制),即 \(1.5\)(十进制)。
- 例如
总结公式:\(V = (-1)^S \times (1.frac) \times 2^{exp - 127}\)
情况二:非规格化数值 (Denormalized) —— 极小的数
特征:exp 全是 0。
当数字非常接近 0 的时候,规格化表示不了那么小的数,就切换到这种模式。
-
真实的指数 E:
注意!这里不再是 \(0 - 127\),而是固定为:\[E = 1 - 127 = -126 \](为什么要这样?为了让它能和规格化数值平滑连接)。
-
有效数字 M:
不再隐含 1 了,而是隐含 0。\[M = 0.frac \] -
零 (Zero):
- 如果
exp=0且frac=0,这就是数字 0。 - 注意有
+0.0(S=0) 和-0.0(S=1)。
- 如果
总结公式:\(V = (-1)^S \times (0.frac) \times 2^{-126}\)
情况三:特殊值 (Special) —— 无穷与 NaN
特征:exp 全是 1 (即 255, 0xFF)。
这时候不看公式了,只看 frac:
-
无穷大 (Infinity):
- 如果
exp全 1,且frac全 0。 - 表示 \(\infty\)(根据 S 决定是 \(+\infty\) 还是 \(-\infty\))。
- 这是溢出时的结果。
- 如果
-
NaN (Not a Number):
- 如果
exp全 1,且frac不为 0。 - 表示非数(比如 \(\sqrt{-1}\) 或 \(\infty - \infty\))。
- 在 Data Lab 里,有时候题目会叫你直接返回参数本身(如果是 NaN 的话)。
- 如果
3. Data Lab 做题核心思路
既然你要完成浮点数题目,你的工作流程通常是:
-
提取:
利用位运算(&,>>)把输入的 32 位整数拆解成s,exp,frac三个整型变量。s取最高 1 位。exp取中间 8 位。frac取低 23 位。
-
分类讨论:
用if判断exp属于上面三种情况的哪一种(是用== 0,还是== 0xFF,还是其他)。 -
数学操作:
题目通常让你实现float_abs(绝对值),float_neg(取反),float_twice(乘2), 或者float_i2f(int转float)。
你需要在各个分量上做手脚:- 乘 2:通常意味着让指数
exp + 1,或者让frac左移一位。要小心exp加完会不会溢出变成无穷大。 - 绝对值:最简单,只要把符号位
S强行变成 0,其他不动。 - int 转 float:这是最难的。你需要手动计算这个 int 的最高位 1 在哪里(找指数),然后把剩下的位移进
frac里,还要处理舍入(Rounding)。
- 乘 2:通常意味着让指数
4. 快速自测表
给你几个关键的位模式,看看你是否理解了:
| 位模式 (Hex) | exp (8位) | frac (23位) | 类别 | 真实含义 |
|---|---|---|---|---|
0x00000000 |
0 | 0 | 非规格化 | 0.0 |
0x80000000 |
0 | 0 | 非规格化 | -0.0 |
0x3F800000 |
127 | 0 | 规格化 | \(1.0 \times 2^{127-127} = 1.0\) |
0x7F800000 |
255 | 0 | 特殊 | +Inf |
0x7FFFFFFF |
255 | 非0 | 特殊 | NaN |
floatScale2
给你一个无符号序列uf,其代表一个浮点数 x, 返回 2.0 * x
首先基本操作先取出 sign exp frac 这三部分,然后先通过exp判断这个数是否为NaN或者无穷大
然后判断其是否为非规格数(exp = 0)。如果是,检测乘2之后 frac部分会不会 > 2 ,这样exp 会修改为1,*2之后变成了规格数
接下来处理规格数,直接 exp += 1 就好了,要注意判断*2之后会不会变成无穷大就好了
unsigned floatScale2(unsigned uf)
{
int sign = uf >> 31; //符号位
int exp = (uf >> 23) & ((1 << 8) - 1); //指数位
int frac = uf & ((1 << 23) - 1); //尾数位
//判断 是否为NaN或无穷大,NaN前面说了返回自身,无穷大的两倍还是无穷大,也返回自身
if(exp == 0xFF) return uf;
//判断是否为非规格化数
if(exp == 0)
{
frac = frac << 1; //尾数左移一位
if(frac & (1 << 23)) //判断是否变为规格化数
{
exp = 1; //变为规格化数,指数变为1
frac = frac & ((1 << 23) - 1); //去掉隐含的1
}
return (sign << 31) | (exp << 23) | frac;
}
//规格化数,指数加1
exp = exp + 1;
//判断是否变为无穷大
if(exp == 0xFF) return (sign << 31) | (exp << 23); //尾数为0
return (sign << 31) | (exp << 23) | frac;
}
floatFloat2Int
给你一个无符号序列uf,其代表一个浮点数 x, 返回 (int)x
首先还是取出 sign exp frac ,顺便计算实际指数 E = exp - 127
首先判断 exp = 0xFF ,这时候是 NaN or 无穷大,统一返回题目要求的无穷大.然后如果实际指数 E < 0,说明这个数是零点几,返回0就好;要是 E >= 31,超出整数的表示范围了,于是就返回无穷大
注意到 frac 这部分的表示是 1.frac 这样,将这一整个(包括隐藏 1 )转化为整数,执行 frac | (1 << 23),如果E >= 23,说明还需要左移 E - 23;E < 23 ,说明左移多了,需要右移 (23 - E)
最后用 sign 的符号判断正负
int floatFloat2Int(unsigned uf)
{
int sign = uf >> 31; //符号位
int exp = (uf >> 23) & ((1 << 8) - 1); //指数位
int frac = uf & ((1 << 23) - 1); //尾数位
int E = exp - 127; //实际指数
int ans;
if(exp == 0xFF) return 0x80000000u; //NaN或无穷大
if(E < 0) return 0; //小于1的数,转化为整数为0
if(E >= 31) return 0x80000000u; //超出 int 范围
ans = frac | (1 << 23); //加上隐含的1,把 frac 按照正数数值来算
if(E >= 23) ans = ans << (E - 23); //乘的> 2^23 ,相当于还要左移
else ans = ans >> (23 - E); //乘的< 2^23 ,相当于右移
return sign ? -ans : ans; //根据符号位返回正负值
}
floatPower2
给定整数 x,返回 \(2.0^x\)
其实就是计算exp = x + 127,然后注意一下表示范围,判断无穷。还有比\(2^{-149}\)小的数字无法被表示,要返回0
在规格化数范围内,这里的sign 和 frac 全都是0,只和 exp有关
unsigned floatPower2(int x)
{
int INF = 0xFF << 23; //表示无穷大的浮点数
int exp = x + 127; //计算指数位
if(x < -149) return 0; //太小,返回0
if(exp <= 0) return 1 << (exp + 22); //非规格化数
if(exp >= 255) return INF; //太大
return exp << 23; //返回浮点数表示
}

浙公网安备 33010602011771号