一 前言
看了csapp2e第二章,感觉讲的很透彻,理解了一些以前学组成原理没有学懂的东西。这章最让我感觉深刻的还是计算机是怎么实现c语言中的基本数据类型的表示和操作的,这对程序员理解程序无疑是帮助巨大的。也正如这本书的题目--以编程人的视角来理解计算机系统(自己这么翻译的,呵呵!)。第二章的homework很多,很多题目都是和位打交道,并且有些题目还只能使用位操作符,不允许使用循环,比较运算符,这让平时很少使用位操作的我来说确实很捉急啊!不过这也正是锻炼自己位操作的大好机会嘛!下面选了几道我认为还是挺不错的题目拿来分享下,由于homework是没有答案的,所以下面解答只是个人的见解。
二 homework
2.65
这道题目要求写下面这样一个函数:
int even_one(unsigned x);
当x展开的比特位中包含偶数个1时返回1,否则返回0。假设x的位数是32位。只能使用位操作,逻辑操作,+、-操作,并且使用这些操作的总次数不能超过12次。
最直观的的方法当然是循环32次统计x位中1的个数,不过题目要求不能使用循环,所以显然是不可以的。既然不能使用循环,想想什么位操作能统计1的个数呢?很显然是没有的,不过题目也没让统计1的个数,换个角度,它只要统计1的个数为偶数还是奇数个就行了。很直观会想到用异或操作,具体如下:
int even_ones(unsigned x) { x ^= (x >> 16); x ^= (x >> 8); x ^= (x >> 4); x ^= (x >> 2); x ^= (x >> 1); return !(x & 1); }
2.66
这道题目要求写下面这样一个函数:
int leftmost_one(unsigned x);
实现x展开的比特位中只保留最高位1,并将结果作为函数值返回,如果x为0,返回0。例如x=0xff00,函数返回0x8000。函数中使用的操作要求和题目2.65一样。
这道题目其实是确定最高位的1在x中是第几位,不过不能使用循环来确定。那就只有把最高位的1右移来填充比这个最高位低的所有位,这样其实就已经确定了最高位1的位置了,具体如下:
int leftmost_one(unsigned x) { x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; x &= ~(x >> 1); return x; }
2.73
这道题目要求写下面这样一个函数:
int saturating_add(int x, int y);
如果x + y向上溢出,则函数返回TMax,如果x + y向下溢出,则函数返回TMin,其它都返回x + y。函数中使用的操作要求和题目2.65一样。
先判断x + y是否会溢出,如果会溢出,则根据x的符号位判断是上溢出还是下溢出就可以了。
int saturating_add(int x, int y) { int sum = x + y; int w = (sizeof(int) << 3) - 1; int xs = x >> w; int ys = y >> w; int sums = sum >> w; (xs == ys) && (xs != sums) && (!xs && sum = TMax || xs && sum = TMin); return sum; }
2.81
对于给定的随机数x和y,它们的类型都为int型,判断c表达式(x > y) == (-x < -y)是否总是产生1或0?
直观上这个等式应该总是1,但由于c中的int类型在计算机中使用补码表示的。而对于补码,TMin是个很特别的数,它的反依旧是TMin,即-TMin == TMin。所以当x = TMin,y = 0时,上面的等式为0。
2.88
题目先定义了六个变量,并假设这些变量都是在IA32的机器上定义的,分别如下:
int x = random();
int y = random();
int z = random();
double dx = (double)x;
double dy = (double)y;
double dz = (double)z;
判断下面的c表达式是否总是为1或0:
A. (double)(float)x == dx
B. dx + dy == (double)(x + y)
C. dx + dy + dz == dz + dy + dx
D. dx / dx == dy / dy
A并非总是1,由于单精度的小数位数只有23位,加隐藏的1位,最高也就24位,所以很显然对于32位的int,有些数它是无法精确表示的,如2 ^ 24 + 1,它是单精度最小的无法精确表示的int数。
B也是并非总是1,由于x + y可能会溢出。
C总是为1的,由于dx,dy,dz是双精度,小数位数有52位,对于只有32的int行来说足够了。当如果dx, dy, dz是float(不考虑dx,dy,dz是由int型转化过来的),就不是总为1了,正如A中所讲的,单精度的小数部位只有24位,如果dx相对dy很小的话,dx会被舍弃掉。如dx = 3.14, dy = 1e10, dz = -1e10, dx + dy + dz == 0,而dz + dy + dx = 3.14。
D并非总是1,如果dx或者dy为0的话,会出现除0情况,这是不允许的。
2.93
这道题目要求写下面这样一个函数:
float_bits float_half(float_bits f);
其中float_bits是32位的无符号整数类型。参数f是其浮点数对应的比特位,函数要求计算0.5 * f,并返回,如果f是NaN(不是一个浮点数),则返回f。并且在电脑上穷举f的2^32种情况来比较电脑中的浮点操作结果和你自己计算的浮点操作的结果。如果需要舍入的话,用round-to-even。
这道题目要求自己实现浮点数的操作,这其实是很不习惯的(反正对于我来是这样,毕竟都没这么搞过啊!)。这道题目需要特殊处理的是浮点数的指数部分(加了偏移量的)为0或1的情况,而其它情况只要对指数减1。如果为0,则必须要将小数向右移动1位,不过这就涉及到舍入的问题了;如果为1,那么需要在小数点中加上隐藏位1,再向右移动一位,并且指数减1,这样就转化为了指数为0的情况。具体实现如下:
#include <stdio.h> typedef unsigned float_bits; float_bits float_half(float_bits f) { unsigned sign = f >> 31; unsigned exp = f >> 23 & 0xff; unsigned frac = f & 0x7fffff; if (exp == 0xff) return f; if (exp > 0) { exp--; if (exp == 0) frac += 1 << 23; } if (exp == 0) { if ((frac & 0x3) == 0x3) frac = (frac >> 1) + 1; else frac >>= 1; } return ((sign << 31) | (exp << 23) | frac); } int main(void) { unsigned i, j; float f1, f2; for (i = 0x3f800000U; i <= 0xffffffffU; i++) { f1 = *((float *)&i); f1 *= (float)0.5; j = float_half(i); f2 = *((float *)&j); printf("%#x : %f %f\n", i, f1, f2); } return 0; }
2.94
这道题目要求写下面这样一个函数:
float_bits float_twice(float_bits f);
题目的要求和2.93一样,只是要求2 * f。
需要特殊处理的是指数部分(加了偏移量的)为0的情况。当指数为0时,左移小数部分1位,如果小数部分的最高位1为第23位(从0开始数),则指数加1,并且小数部分去掉这个最高位的1,否则只要左移小数部分1位就可以了。其它情况只需要将指数加1。具体实现如下:
#include <stdio.h> typedef unsigned float_bits; float_bits float_twice(float_bits f) { unsigned sign = f >> 31; unsigned exp = (f >> 23) & 0xff; unsigned frac = f & 0x7fffff; if (exp == 0xff) return f; if (exp == 0) { frac <<= 1; if (frac & 0x800000) { frac &= 0x7fffff; exp++; } } else exp++; return ((sign << 31) | (exp << 23) | frac); } int main(void) { unsigned i, j; float f1, f2; for (i = 0x37800000U; i <= 0xffffffffU; i++) { f1 = *((float *)&i); f1 *= (float)2.0; j = float_twice(i); f2 = *((float *)&j); printf("%#x : %f %f\n", i, f1, f2); } return 0; }
2.95
这道题目要求写下面这样一个函数:
float_bits float_i2f(int i);
函数实现对int类型到float类型的转化。在c中我们可能用简简单单的强制类型转化就搞定了,不过如果是自己来实现,还是有一定的难度的。
这道题主要注意三个方面。一是i为特殊数字的时候,如i=0, i=TMin;二是i为负数的时候,需要求出i的原码来,由于int类型在机器中都是用补码表示的;三是慎重考虑舍入的情况。具体实现如下:
#include <stdio.h> typedef unsigned float_bits; #define TMin 0x80000000 float_bits float_i2f(int i) { unsigned sign, exp, frac, last_bit; sign = i >> 31; if (i == TMin) //处理i为最小负数的时候
{ exp = 158; frac = 0; } else if (i == 0) //处理i为0
{ exp = 0; frac = 0; } else { if (sign) //如果i为负数,需要求其原码
frac = (~i + 1) << 1; else frac = i << 1; exp = 0; while (!(frac & 0x80000000)) { exp++; frac <<= 1; } exp = 157 - exp; last_bit = ((frac >> 8) & 1); if (!last_bit) //处理舍入,对于最后1位为0
{ if ((frac & 0xff) == 0x80 || !((frac & 0xff) & 0x80)) //舍弃的位离0更近
frac = ((frac << 1) >> 9); else frac = ((frac << 1) >> 9) + 1; } else //处理舍入,对于最后1位为1
{ if ((frac & 0xff) == 0x80 || ((frac & 0xff) & 0x80)) //舍弃的位离1更近
{ frac = ((frac << 1) >> 9) + 1; if (frac & 0x800000) { exp++; frac = 0; } } else frac = ((frac << 1) >> 9); } } return ((sign << 31) | (exp << 23) | frac); } int main(void) { unsigned i; int j, k; float f1, f2; for (i = 0x80000000U; i <= 0xffffffffU; i++) { j = (int)i; f1 = (float)j; k = float_i2f(j); f2 = *((float *)&k); printf("%#x : %f %f\n", i, f1, f2); } return 0; }
2.96
这道题目要求写下面这样一个函数:
int float_f2i(float_bits f);
函数实现对float类型到int类型的转化。如果转化中溢出或者f是NaN,则返回0x80000000。
这道题和2.95差不多,主要是考虑溢出的情况。
#include <stdio.h> typedef unsigned float_bits; #define TMin 0x80000000
int float_f2i(float_bits f) { unsigned sign, exp, frac, last_bit; int i; sign = f >> 31; exp = (f >> 23) & 0xff; frac = f & 0x7fffff; if (exp == 158 && frac == 0 && sign == 1) //处理f = TMin的情况
i = TMin; else if (exp > 157 || exp == 0xff) //处理f上溢出或f是NaN的情况
i = TMin; else if (exp < 126) //处理f下溢出
i = 0; else { exp -= 127; frac |= 0x800000; if (exp > 23) { exp -= 23; frac <<= exp; } else if (exp < 23) { exp = 23 - exp; frac >>= exp; } if (sign == 1) i = (~frac) + 1; else i = frac; } return i; } int main(void) { unsigned i; int j, k; float f; for (i = 0x3fbfff70U; i <= 0xffffffffU; i++) { f = *(float *)&i; j = (int)f; k = float_f2i(i); printf("%f : %d %d\n", f, j, k); } return 0; }