CSAPP,深入理解计算机系统原书第三版的第二章的课后习题[小白版]

前言

本文是CSAPP,深入理解计算机系统原书第三版的第二章的课后习题,有加自己的一些理解,

资源主要来自于:

CSAPP-3E-Solution深入理解计算机系统(第三版)作业题答案(第二章)练习题:CSAPP Chapter2 Homework(已完结),非常感谢。

2.55 在不同机器运行show_bytes

#include<stdio.h>
typedef unsigned char * byte_pointer;
void show_bytes(byte_pointer start,size_t len){
	size_t i;
	for(i=0;i<len;i++)
		printf(" %2.x",start[i]);
	printf("\n");
}
void show_int(int x){
	show_bytes((byte_pointer)&x,sizeof(int));
}
void show_float(float x){
	show_bytes((byte_pointer)&x,sizeof(float));
}
void show_pointer(void *x){
	show_bytes((byte_pointer)&x,sizeof(void *));
}
void test_show_bytes(int val){
	int ival=val;
	float fval=(float) ival;
	int *pval=&ival;
	show_int(ival);
	show_float(fval);
	show_pointer(pval);
}
int main(){
	test_show_bytes(12345);
    return 0;
}

10#=>16#得到12345=>0x00003039(整型表示),浮点型0x4640E400

window:

 39 30 00 00
 00 e4 40 46
 dc f5 7f b7 6b 00 00 00

Linux:

 39 30 00 00
 00 e4 40 46
 1c 88 89 e8 fe 7f 00 00

window和linux都是小端法机器

2.56 用不同示例值运行show_bytes

更改test_show_bytes();传入值

2.57 编写show_short,show_long,show_double

#include<stdio.h>
typedef unsigned char * byte_pointer;
void show_bytes(byte_pointer start,size_t len){
	size_t i;
	for(i=0;i<len;i++)
		printf(" %.2x",start[i]);
	printf("\n");
}
void show_short(short x){
	show_bytes((byte_pointer)&x,sizeof(short));
}
void show_long(long x){
	show_bytes((byte_pointer)&x,sizeof(long));
}
void show_double(double x){
	show_bytes((byte_pointer)&x,sizeof(double));
}
void test_show_bytes(int val){
	short sval=(short)val;
	long lval=(long) val;
	double dval=(double) val;
	show_short(sval);
	show_long(lval);
	show_double(dval);
}
int main(){
	int num=12345;
	test_show_bytes(num);
	return 0;
}

windows

 39 30
 39 30 00 00
 00 00 00 00 80 1c c8 40

linux同上

2.58 编写过程is_little_endian,在小端法机器运行返回1,大端法返回0,无论机器字长

定义一个int变量,值为0xff,剩下两个16#由0补上,0x00ff。

#include<stdio.h>
#include <assert.h>
typedef unsigned char * byte_pointer;
int is_little_endian(){
	int num=0xff;
	byte_pointer byte_start=(byte_pointer)&num;
	if(byte_start[0]==0xff)//相等说明先返回ff而不是0
		return 1;//小端返回1
	else
		return 0;//大端返回0
}
int main(){
	assert(is_little_endian());
	return 0;
}

2.59 表达式,生成一个字,由x的最低有效位字节和y中剩下的字节组成,对于运算数x=0x89ABCDEF和y=0x76543210,得到0x76543EF

表达式:x&0xFF | y&~0xFF

解释:

x&0xFF,得到x最低有效位,y&~0xFF得到y最低有效位之外的位,两者进行或运算,得到组合结果

#include<stdio.h>
#include<assert.h>

int main(){
	size_t mask=0xff;
	size_t x=0x89ABCDEF;
	size_t y=0x76543210;
	size_t res=(x&mask)|(y&~mask);
	assert(res==0x765432EF);
	return 0;
}

2.60 函数,返回无符号值,从0(最低位)到w/8-1(最高位)编号,参数x的字节i被替换成字节b

函数头:

unsigned replace_byte(unsigned x,int i,unsigned char b)

示例:

replace_bytes(0x12345678,2,0xAB)-->0x12AB5678,第2位为34(78为0,56为1)

replace_bytes(0x12345678,0,0xAB)-->0x12AB56AB

#include<stdio.h>
#include<assert.h>

/*获取x的最高有效位*/
int get_msb(int x){
	int shift_val=(sizeof(int)-1)<<3;//1.当int有4位,(4-1)左移3位相当于*8,得到24,2.当int为8位,(8-1)*8=56
	int xright=x>>shift_val;//算数右移,1. 32-8=24,右移24,只剩前面8字节,即最高有效位的两个16#数字,2. 8*8-56=8
	return xright&0xFF;//除最高有效位外全赋值为0
}
unsigned replace_byte(unsigned x,int i,unsigned char b){
	// 1 byte has 8 bits, << 3 means * 8
	unsigned mask = ((unsigned) 0xFF) << (i << 3);
	/* i<<3即i*8,将位置i*8(4bit表示一个16#,8bit表示两个,即一位),i*8得到在bit上对应i的位置,
	  ((unsigned) 0xFF) << (i << 3)通过 (i << 3),将i对应的位置赋值为全1
	 */
	unsigned pos_byte = ((unsigned) b) << (i << 3);
	//将b移动到对应的位置
	return (x & ~mask) | pos_byte;//x&~mask将位置i上的值赋值为全1,与pos_byte与运算将位置i赋值为b
}
int main(){
	int num=0x12345678;
	printf("%.2x\n",get_msb(num));
	unsigned rep_0 = replace_byte(0x12345678, 0, 0xAB);
	unsigned rep_3 = replace_byte(0x12345678, 3, 0xAB);
	
	assert(rep_0 == 0x123456AB);
	assert(rep_3 == 0xAB345678);
	return 0;
}

2.61 表达式,在下列描述条件下产生1,其他情况得到0,假设x是int类型。

A.x的任何位都等于1

B.x的任何位都等于0

C.x的最低有效位字节中的位都等于1

D.x的最高有效位字节中的位都等于0

要求:不允许使用==!=

tips:

  • ! :代表值取反,对于整形变量,只要不为0,使用 ! 取反都是0,0取反就是1。就像 bool 只有真假一样
  • ~ :位的取反,对每一个二进制位进行取反,0变1,1变0

表达式:

A: !~x

x位取反,若x全1,x为全0,!x得到1;若x存在不等于1的位,x位取反后此位得到1,x不全为0,!x得到0

B: !x

x值取反,若x全0,值取反之后得到1

C: !~(x | ~0xff)

~0xff=ffffff00

x|(~0xff)=ffffff x的最低有效位

~=000000 x的最低有效位按位取反

错误写法!~(x&0xFF),x全1,x&0xff=ff,~(x&0xff)=ffffff00

D:!((x >> ((sizeof(int)-1) << 3)) & 0xff)

shift_val=(sizeof(int)-1) << 3):1.当int有4位,(4-1)左移3位相当于8,得到24,2.当int为8位,(8-1)8=56

xright=x>>shift_val:算数右移,1. 32-8=24,右移24,只剩前面8字节,即最高有效位的两个16#数字,2. 8*8-56=8
xright&0xFF:除最高有效位外全赋值为0

代码

#include <stdio.h>
#include <assert.h>

int A(int x) {
	return !~x;
}

int B(int x) {
	return !x;
}

int C(int x) {
	return A(x | ~0xff);
}

int D(int x) {
	return B((x >> ((sizeof(int)-1) << 3)) & 0xff);
}

int main(int argc, char* argv[]) {
	int all_bit_one = ~0;
	int all_bit_zero = 0;
	
	assert(A(all_bit_one));
	assert(!B(all_bit_one));
	assert(C(all_bit_one));
	assert(!D(all_bit_one));
	
	assert(!A(all_bit_zero));
	assert(B(all_bit_zero));
	assert(!C(all_bit_zero));
	assert(D(all_bit_zero));
	
	// test magic number 0x1234ff
	assert(!A(0x1234ff));
	assert(!B(0x1234ff));
	assert(C(0x1234ff));
	assert(D(0x1234ff));
	
	// test magic number 0x1234
	assert(!A(0x1234));
	assert(!B(0x1234));
	assert(!C(0x1234));
	assert(D(0x1234));
	
	return 0;
}

2.62 函数,对int类型的数使用算数位移的机器上运行此函数生成1,其他情况0,在任意字长都可以运行。

函数头int_shifts_are_arithmetic()

变量x设置为全1,num = ~0或者-1,通过表达式 !(num ^ (num >> 1))得到结果。

num位为全1,>>1 ,算数位移后仍然是全1,通过抑或运算,如果为算数位移,抑或后得到全0,否则不全0,再进行值取反。

//#include <stdio.h>
//#include <assert.h>
//
//   
//int int_shifts_are_arithmetic(){
//	int x=~0;//全1
//	int shift_val=(sizeof(int)-1)<<3;//1.当int有4位,(4-1)左移3位相当于*8,得到24,2.当int为8位,(8-1)*8=56
//	int x_shifted=x>>shift_val;
//	if(x_shifted==x)
//		return 1;
//	else
//		return 0;
//}
//
//int main(int argc, char* argv[]) {
//	
//	printf("%d\n",int_shifts_are_arithmetic());
//	return 0;
//}


#include <stdio.h>
#include <assert.h>

int int_shifts_are_arithemetic() {
	int num = ~0;//-1;
	return !(num ^ (num >> 1));
}

int main(int argc, char* argv[]) {
	assert(int_shifts_are_arithemetic());
	return 0;
}

2.63 将代码补充完整,srl使用算数右移(由值xsra给出)来完成逻辑右移,sra(由值xsrl给出)使用逻辑右移完成算数右移

其他操作不包括右移或者除法,可以通过计算8*sizeof(int)来确定数据类型int中的位数w,位移量k的取值范围为0~w-1。

unsigned srl(unsigned x,int k){
    unsigned xsra=(int)x>>k;
    ...
}
unsigned sra(unsigned x,int k){
    unsigned xsrl=(unsigned)x>>k;
    ...
}

补充后:

#include <stdio.h>
#include <assert.h>

//通过算数位移完成逻辑位移
unsigned srl(unsigned x,int k){
	unsigned xsra=(int)x>>k;//int右移为算数位移
	int w=sizeof(int)<<3;//计算int位数
	int mask=(int)-1<<(w-k);//-1即全1位,左移将右边的w-k位设置为0,左边k位仍然是全1
	return xsra&~mask;//~mask得到左k位为0,右边全1
}
//通过逻辑位移完成算数位移
unsigned sra(unsigned x,int k){
	unsigned xsrl=(unsigned)x>>k;//转为unsigned,进行逻辑位移
	int w=sizeof(int)<<3;//计算int位数
	int mask=(int)-1<<(w-k);
	int m = 1 << (w - 1);//第一位是1,其余0
	//当x的第一位为1时,让掩码保持不变,否则为0。
	mask&= ! (x & m) - 1;//x&m=1则x第一位是1,否则0;值取反=1(第一位0),0(第一位1);再-1=-1(第一位1),0(第一位0);mask&-1不变,&0为0
	return xsrl | mask;
}
int main() {
	unsigned test_unsigned = 0x12345678;
	int test_int = 0x12345678;
	
	assert(srl(test_unsigned, 4) == test_unsigned >> 4);
	assert(sra(test_int, 4) == test_int >> 4);
	
	test_unsigned = 0x87654321;
	test_int = 0x87654321;
	
	assert (srl (test_unsigned, 4) == test_unsigned >> 4);
	assert (sra (test_int, 4) == test_int >> 4);
	
	return 0;
}

2.64 实现any_odd_one函数

当x有任意奇数位等于1时返回1,否则返回0,假设w=32

表达式:!!(0xAAAAAAAA & x)

因为A=1010,0xA...A &x即和x中奇数位进行与运算;接下来两次值取反:

  • 第一次值取反,若奇数位存在1,非全0值取反会得到0,第二次值取反得到1
  • 第一次值取反,若奇数位不存在1,全0值取反得到1,第二次值取反得到0;
#include <stdio.h>
#include <assert.h>

int any_odd_one(unsigned x) {
	//A=1010,0xA...A &x即和x中奇数位进行与运算,第一次值取反,若奇数位存在1,非全0值取反会得到0,第二次值取反得到1;
	//第一次值取反,若奇数位不存在1,全0值取反得到1,第二次值取反得到0;
	return !!(0xAAAAAAAA & x);
}

int main(int argc, char* argv[]) {
	assert(any_odd_one(0x2));
	assert(!any_odd_one(0x4));
	return 0;
}

2.65 实现odd_ones函数

当x包含奇数个1时返回1;否则返回0,假设w=32,代码最多包含12个算术运算、位运算和逻辑运算。

思路来自这里,首先需要知道,执行 x ^= x >> L 时,结果的最后L位将具有与x的原始值的最后2*L位的奇偶性相同。
请先思考为什么...


因为1 ^ 1 = 0,那么当我在x ^= x >> L中使用XOR时,它只是删除了2*i位数的1。这就是为什么最后的L位与x的奇偶性相同。

以8位为例介绍

思路来自这里

八位记为D7~D0:D7D6D5D4D3D2D1D0。

x=D7...D0,则x>>4得到0000D7D6D5D4

  1. x=x>>4即`D7D6D5D4D3D2D1D0`0000D7D6D5D4

    设D4=D0的结果为E0,若E0=1,说明D4和D0共有奇数个1,E0=0说明D4和D0共偶数个1,同样设D5=D1的结果为E0,若E1=1,说明D5和D1共有奇数个1,E1=0说明D5和D1共偶数个1。同理得到D5D2=E2,D7D3=E3。

    抑或运算结束后,x=XXXXE3E2E1E0(X后面会被去掉)

  2. x=x>>2即`XXXXE3E2E1E0`00XXXXE3E2

    E0E2=F0,若F0=0,说明E0和E2共有奇数个1,而E0和E2共有奇数个1等价于D4D0D5D2共奇数个1,若F0=0,等价于D4D0D5D2共偶数个1。同理得到E1E3=F1。

    抑或运算结束后,x=XXXXXXF1F0

  3. x=x>>1即F1F0

    G1=F1^F0,若G1=1,说明F1F0共奇数个1,等价于E1E3E0E2共奇数个1,等价于D7D6D5D4D3D2D1D0共奇数个1。若G1=0,等价于D7D6D5D4D3D2D1D0共偶数个1。

    抑或运算结束后,x=XXXXXXXG1

  4. x&=0x1,即G1&0x1

    若G1=1,&1后得到1,而G1=1等价于D7D6D5D4D3D2D1D0共奇数个1,前面的7个X因为&0得到0;若G1=0,&1后得到0,而G1=0等价于D7D6D5D4D3D2D1D0共偶数个1,前面的7个X同样因为&0得到0。

思路与代码

通过连续折半进行抑或运算,保留x的奇偶性,直到用最低位的1位来表示x的奇偶性,再和0x1进行与运算,将前面多余的高位去掉。

/*当x包含奇数个1时返回1;否则返回0,假设w=32,代码最多包含12个算术运算、位运算和逻辑运算。*/
#include <stdio.h>
#include <assert.h>


int odd_ones(unsigned x) {
	x ^= x >> 16;/*通过连续XOR值的一半来XOR所有的比特,直到只剩下一个比特。
直到只剩下一个单一的位。*/
	x ^= x >> 8;
	x ^= x >> 4;
	x ^= x >> 2;
	x ^= x >> 1;
	x &= 0x1;/* 仅保留最低位的掩码。 */
	return x;
}

int main(int argc, char* argv[]) {
	assert(odd_ones(0x10101011));
	assert(!odd_ones(0x01010101));
	return 0;
}

2.66 实现leftmost_one

生成表示 x 中最左边 1 的掩码,假设w=32,例如,0xFF00->0x8000,0x6600->0x4000。

如果x=0,返回0。最多包含15个算术运算、位运算和逻辑运算。

提示:先将x转换成形如[0...011...1]的位向量。

思路

来自这里,和这里

  1. 首先,生成一个掩码,使最左边的一位之后的所有位都是1。

    例如,0xFF00 -> 0xFFFF,以及0x6000 -> 0x7FFF,如果x=0,就得到0

    用二进制举例:010...0>011...1,01010...0>01111..1

    如何利用最左边的1将此1右边的所有位都填满1呢,通过 mask |= mask >> 2^n 的操作,每次填满 2 的幂次个 1,由 n = 0到 n = 4 即可填滿 32 位元。

    例如:x=0100 0000 0000 0000 ...0

    n=0=>x1=0110 0000 0000 0000 ...0,//最左边的1会往右移动2的0次方个位,

    n=1=>x2=0111 1000 0000 0000 ...0,//左边的两个1会往右移动2的1次方个位,

    n=2=>x3=0111 11111 1000 0000 ...0,//左边的4个1会往右移动2的2次方个位,

    n=3,n=4,左边的1会依次填满 2 的幂次个 1。直到右边全部为1。

  2. 右移一位(先加 1 有可能 overflow) 再+1,(mask >> 1) + 1,即0011 1111=>0001 1111再+1得到0010 0000。

  3. 考虑x可能为0,所以与自己做与运算, x & ((mask >> 1) + 1)

代码

/**
 * 生成表示 x 中最左边 1 的掩码,假设w=32,例如,0xFF00->0x8000,0x6600->0x4000。
  如果x=0,返回0。最多包含15个算术运算、位运算和逻辑运算。
  提示:先将x转换成形如[0...011...1]的位向量。
 */
#include <stdio.h>
#include <assert.h>


int leftmost_one(unsigned x) {
	unsigned mask = x;
	mask |= mask >> 1;
	mask |= mask >> 2;
	mask |= mask >> 4;
	mask |= mask >> 8;
	mask |= mask >> 16;
	return x & ((mask >> 1) + 1);
}

int main() {
	printf("Left most bit of 0xff00 is 0x%x\n", leftmost_one(0xff00));
	printf("Left most bit of 0x6600 is 0x%x\n", leftmost_one(0x6600));
	printf("Left most bit of 0x0001 is 0x%x\n", leftmost_one(0x0001));
	printf("Left most bit of 0x0000 is 0x%x\n", leftmost_one(0x0000));
}

2.67 int_size_is_32()

bad_int_size_is_32

当int 32位时为返回1,否则0,在32位字长机器运行下面程序,不仅返回0,还会得到警告warning: left shift count>=width of type

int bad_int_size_is_32(){
    //获取最高有效位msb(32位)
    int set_msb=1<<31;
    //左移找到msb左边的位
    int beyond_msb=1<<32;
    //size>=32 ==> msb非0,size<=32,beyond_msb ==> 0
    return set_msb&&!beyond_msb;
}

A.代码在哪里没有遵守规范?

B.修改代码,在32位机器上可以运行

C.修改代码,在16位机器上运行

解答

A.In section 6.5.7 Bitwise shift operators of c11 standard, it said

在c11标准的第6.5.7节中,比特移位运算符,它说

If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior it undefined.如果右操作数的值是负的,或者大于或等于被提升的左操作数的宽度,则行为未定义。

其实就是1<<32左移32位(32即字长),此操作行为是undefined的

B.不要一次性移动32位,而是利用左移31位得到的set_msb,beyond_msb=set_msb<<1

C.在16位上运行,同样也是将32拆开成每次移动不大于16位的操作,set_msb = 1 << 15 << 15 << 1

#include <stdio.h>
#include <assert.h>

/* The following function does not run properly on some machine */
/*
int bad_int_size_is_32() {
  int set_msb = 1 << 31;
  int beyond_msb = 1 << 32;

  return set_msb && !beyond_msb;
}
*/

int int_size_is_32() {
  int set_msb = 1 << 31;
  int beyond_msb = set_msb << 1;

  return set_msb && !beyond_msb;
}

int int_size_is_32_for_16bit() {
  int set_msb = 1 << 15 << 15 << 1;
  int beyond_msb = set_msb << 1;

  return set_msb && !beyond_msb;
}

int main(int argc, char *argv[]) {
  assert(int_size_is_32());
  assert(int_size_is_32_for_16bit());
  return 0;

2.68 编写lower_one_mask

注意n=w的情况。int lower_one_mask(int n)

最低有效 n 位设置为 1 的掩码,例如,n=6 --> 0x3F,11 1111,n=17,17个1即0x1FFFF.

假设1<=n<=w。

#include <stdio.h>
#include <assert.h>


int lower_one_mask(int n) {
	int w=sizeof(int)<<3;
	return (unsigned) -1>>(w-n);//全1往右逻辑移动w-n
}


int main(int argc, char* argv[]) {
	assert(lower_one_mask(6) == 0x3F);
	assert(lower_one_mask(17) == 0x1FFFF);
	assert(lower_one_mask(32) == 0xFFFFFFFF);
	return 0;
}

2.69 rotate_left

在第n位,旋转左移,假设0<=n<=w,例如,x=0x12345678,w=32,n=4 -> 0x23456781,n=20 -> 0x67812345。

x << n | x >> (w - n - 1) >> 1,其中x << n 是左移后的结果,与右移后的结果 x >> (w - n - 1) >> 1做左或运算,得到完整结果。

#include <stdio.h>
#include <assert.h>


unsigned rotate_left(unsigned x, int n) {
	int w = sizeof(unsigned) << 3;
	/* pay attention when n == 0 */
	return x << n | x >> (w - n - 1) >> 1;
}

int main(int argc, char* argv[]) {
	assert(rotate_left(0x12345678, 4) == 0x23456781);
	assert(rotate_left(0x12345678, 20) == 0x67812345);
	return 0;
}

2.70 fits_bits

int fits_bits(int x, int n)

当x可以被表示为一个n位(n可能比W小)的二进制补码时,返回1 ,否则0,假设1<=n<=w。

例如,当w = 8, n = 3时,

  • 当x > 0

    0b00000010是可以的;0b00001010不可以, 因为4>n=3;0b00000110不可以,因为首位是1,补码首位1表示负数

  • x < 0

    0b11111100 是可以的; 0b10111100不可以,因为n=3容纳不下;0b11111000也不行

所以问题的重点即,当x进行w-n位的移动后,是否可以保持不变,如果要返回1 ,则x << (w-n) >> (w-n)必须等于x本身。

#include <stdio.h>
#include <assert.h>

int fits_bits(int x, int n) {
	int w = sizeof(int) << 3;
	int offset = w - n;
	return (x << offset >> offset) == x;
}

int main(int argc, char* argv[]) {
	assert(!fits_bits(0xFF, 8));
	assert(!fits_bits(~0xFF, 8));
	
	assert(fits_bits(0b0010, 3));
	assert(!fits_bits(0b1010, 3));
	assert(!fits_bits(0b0110, 3));
	
	assert(fits_bits(~0b11, 3));
	assert(!fits_bits(~0b01000011, 3));
	assert(!fits_bits(~0b111, 3));
	return 0;
}

2.71 xbyte

他们要实现一组工程来操作一个数据结构,要求将4个有符号字节封装成一个32位unsigned。一个字中的字节从0(最低有效位)编号到3(最高有效位)。分配给你的任务是:为一个使用补码运算和算数右移的机器编写一个具有如下原型的函数:

typedef unsigned packed_t;
int xbyte(packed_t, word,int bytenum);

也就是说,函数会抽取出指定的字节,再把它符号扩展为一个32位int,如0x00112233, 抽取第一位1位,第一位即从右往左第2位(0开始)的22,0010 0010,扩展位有符号号32位,得到 0x22。而0xAABBCCDD, 1,第一位CC,即1010 1010,右移扩展通过算数右移,1会填充,得到0xFFFFFFCC

你的前任编写了下面的代码:

int xbyte(packed_t word,int bytenum){
    return (word>>(bytenum<<3))&0xFF;
}

A。这段代码错在哪里?

B。给出代码的正确实现,只能使用左右位移和一个减法。

该函数的作用是取出一个字中的某个字节,然后把该字节扩展为有符号整数.

A。因为unsigned右移是逻辑右移,所以该函数不能从 packet_t word 中提取负数。

B。通过算数右移,先将packet_t转换为int,再将要抽取的字节移动到最高位,然后再利用算数右移回去

#include <stdio.h>
#include <assert.h>

typedef unsigned packet_t;

// 该函数的作用是取出一个字中的某个字节,然后把该字节扩展为有符号整数
// 难点在于如何利用算数右移填充前边的位
// 核心思想就是先把目前字节左移到最高位,然后再利用算数右移
int xbyte(packet_t word, int bytenum) {
    int size = sizeof(unsigned);
    int shift_left_val = (size - 1 - bytenum) << 3;
    int shift_right_val = (size - 1) << 3;
    return (int)word << shift_left_val >> shift_right_val;
}

int main(void) {
    assert(xbyte(0x00112233, 2) == 0x11);
    assert(xbyte(0x00112233, 0) == 0x33);
    assert(xbyte(0x00112233, 1) == 0x22);
    assert(xbyte(0x00112233, 3) == 0x00);

    assert(xbyte(0xAABBCCDD, 0) == 0xFFFFFFDD);
    assert(xbyte(0xAABBCCDD, 1) == 0xFFFFFFCC);
    assert(xbyte(0xAABBCCDD, 2) == 0xFFFFFFBB);
    assert(xbyte(0xAABBCCDD, 3) == 0xFFFFFFAA);
    return 0;
}

2.72 copy_int

给你一个任务,写一个函数将整数val复制到缓冲区buf中,但是只有缓冲区中有足够可用的空间时,才执行复制。你写的代码如下:

void copy_int(int val,void *buf,int maxbytes){
    if(maxbytes-sizeof(val)>=0)
        memcpy(buf,(void*)&val,sizeof(val));
}

这段代码使用了库函数memcpy,虽然在这里使用这个函数有点刻意,因为我们只是想复制一个int,但是它说明了一种复制较大数据结构的常见方法。你仔细地测试了这段代码后发现,哪怕maxbytes很小的时候,它也能把值复制到缓冲区中。

A.解释为什么代码中的条件总是测试成功,哪怕Maxbytes很小的时候,它也能把值赋值到缓冲区中。

B.你该如何重写这个函数。

A.sizeof(val)返回size_t类型,它通常是一种无符号类型。maxbytes - sizeof(val)返回size_t类型的值,并且它总是>=0。

B.将sizeof返回的类型强转为int,

#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>

void copy_int(int val, void* buf, int maxbytes) {
	/* compare two signed number, avoid someone set maxbytes a negetive value */
	if (maxbytes >= (int) sizeof(val)) {
		memcpy(buf, (void*)&val, sizeof(val));
	}
}

int main(int argc, char* argv[]) {
	int maxbytes = sizeof(int) * 10;
	void* buf = malloc(maxbytes);
	int val;
	
	val = 0x12345678;
	copy_int(val, buf, maxbytes);
	assert(*(int*)buf == val);
	
	val = 0xAABBCCDD;
	copy_int(val, buf, 0);
	assert(*(int*)buf != val);
	
	free(buf);
	return 0;
}

2.73 saturating_add

写出具有如下原型的函数代码int saturating_add(int x,int y)。编写饱和加法,同正常的补码加法溢出的方式不同,当正溢出时,饱和加法返回TMAX,负溢出时,返回TMIN,饱和运算常常用于在执行数字信号处理的程序中。

int saturating_add(int x, int y)
{
    //正溢出时返回Tmax 负溢出时返回Tmin
    //x_sign_mask、y_sign_mask为符号位的掩码,结果为0xffffffff(表示x为负数) or 0x0(表示x为正数)
    int x_sign_mask = x>>((sizeof(int)<<3)-1);
    int y_sign_mask = y>>((sizeof(int)<<3)-1);

    int sum = x + y;
    //若sum_sign_mask为0xffffffff 表示sum为负数
    int sum_sign_mask = sum >> ((sizeof(int)<<3)-1);

    //写出正溢出的标志
    //正溢出时~x_sign_mask为0xffffffff、~y_sign_mask为0xffffffff、 sum_sign_mask为oxffffffff
    //否则无论何种情况 下面式子都为0x0
    int postive_flag = ~x_sign_mask & ~y_sign_mask &sum_sign_mask;

    //负溢出时x_sign_mask为0xffffffff、y_sign_mask为0xffffffff、 ~sum_sign_mask为0xffffffff
    //否则无论何种情况 下面式子都为0x0
    int negative_flag = x_sign_mask & y_sign_mask & ~sum_sign_mask;

    //若正溢出A为INT_MAX B为0 C为0 D为0 (D为~(negative_flag|postive_flag))
    //若负溢出A为0 B为INT_MIN C为0 D为0 (D为~(negative_flag|postive_flag))
    //若正负都没有溢出A B都为0 C为sum D为0xffffffff(D为~(negative_flag|postive_flag))
    //
    //              A                     B                               C
    return sum = (INT_MAX&postive_flag)|(INT_MIN&negative_flag)|(sum&(~(negative_flag|postive_flag)));
}

2.74 tsub_ok

判断是否可以无溢出做减法。int tsub_ok(int x,int y);如果x-y不溢出,函数返回1.

-y可以看作+(-y)。

#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
/**
 * @brief 如果x-y不溢出,函数返回1
 */

/* Determine whether arguments can be substracted without overflow */
int tsub_ok(int x, int y)
{
	if (y == INT_MIN) {// 当y为最小整数的时候,就产生了溢出,因为任何数减最小数都会溢出
		return 0;
	}
	
	int neg_y = -y;
	int sum = x + neg_y;
	int pos_over = x > 0 && neg_y > 0 && sum < 0;//正溢出,
	int neg_over = x < 0 && neg_y < 0 && sum >= 0;
	
	return !(pos_over || neg_over);
}

int main(int argc, char* argv[]) {
	assert(!tsub_ok(0x00, INT_MIN));
	assert(tsub_ok(0x00, 0x00));
	return 0;
}

2.75 unsigned_high_prod

假设我们想要计算x*y的完整的2w位表示,x、y都是无符号数,并且运行在数据类型unsigined是w位的机器上,乘积的低w位能够用表达式x*y计算,所以需要一个具有下列原型的函数:

unsigned unsiged_high_prod(unsigned x,unsigned y);

计算无符号变量x乘以y的高w位,使用一个具有下面原型的库函数;int signed_high_pord(int x,int y),计算x和y采用补码形式的情况下,x乘以y的高w位,编写代码调用这个过程,以实现无符号数为参数的函数,验证你的解答。

提示:看看等式2.18的推导中,有符号乘积x乘以y和无符号乘积x'乘以y'之间的关系。

过程

等式2.18:

\[(x'\cdot y')\mod2^w=(x\cdot y)\mod2^w \]

这个问题需要一步一步的进行推导 T2Uw(x)我们把这种写法称为补码转无符号数,那么很容易得出:

if x < 0  => x + 2^w   
if x > 0  => x  // 2^w表示2的w次方,为什么当x<0时是这个结果呢,其实,补码的负数就是把原来w-1之后的位的结果减去了最高一位的值,最高位的值就是2^w,如T2Uw(1100)将原本的-8+4=-4转换为8+4=12即-4+2^w=-4+16=12

​ 上边的公式很简单,但在使用的时候还要做判断,显然很不科学,我们可以认为T2Uw(x)是一个函数 接下来就想办法推导出一个表达式来 ,这里省略了一系列的推导过程,得出了这样一个结果T2Uw(X)= X + X(w-1)2^w,大家看看这个式子跟上边的那个作用一样,x的w-1位就是他的最高位,如果该位的值是1,那么就相当于x<0的情况,否则就是另一种情况 .

我们假设x‘表示x的无符号值 X’ = X + X(w-1)2^w , 我们假设y‘表示x的无符号值 ,则Y’ = Y + Y(w-1)2^w,那么X’ * Y’ = (X + X(w-1)2^w) * (Y + Y(w-1)2^w) ,如果要把这个计算式展开会很麻烦,我们可以进一步抽象:

设a = X(w-1)2^w, b= Y(w-1)2^w 则: X’ * Y’ = XY + Xb + Ya + ab,我们假定有这样一个函数,他的功能是取出无符号数的最高位uh(),因此上边的式子变形为:

uh(X’ * Y’ ) = uh(XY + Xb + Ya + ab)= uh(XY) + uh(Xb) + uh(Ya) + uh(ab) ,

那么X * b 也就是Xb= XY(w-1)2^w ,他的最高位的值就是XY(w-1)2^w / 2^w => XY(w-1) ;

那么Y * a 也就是Ya= YX(w-1)2^w 他的最高位的值就是YX(w-1)2^w / 2^w => YX(w-1) ;

综合起来a * b 也就是ab= X(w-1)2^w * Y(w-1)2^w ,它 / 2^w => 0 ===> uh(X’ * Y’ ) = uh(XY) + XY(w-1) + YX(w-1)

上边推理的核心思想就是 :无符号X’ 的补码表示:X + X(w-1)2^w,求高位的/ 2^w 操作。

代码

#include <stdio.h>
#include <assert.h>
#include <inttypes.h>

int signed_high_prod(int x, int y) {//即函数uh,取出无符号数的最高位
	int64_t mul = (int64_t) x * y;
	return mul >> 32;
}

unsigned unsigned_high_prod(unsigned x, unsigned y) {
	/* TODO calculations */
	int sig_x = x >> 31;//获取x的符号,0或者1
	int sig_y = y >> 31;//获取y的符号
	int signed_prod = signed_high_prod(x, y);
	return signed_prod + x * sig_y + y * sig_x;
	/**
	 * 推理知道a*b= X(w-1)2^w * Y(w-1)2^w, 它 / 2^w => 0    
     ===> uh(X’  * Y’ ) = uh(X*Y) + X*Y(w-1) + Y*X(w-1) */ 
}

/* 通过64位计算来验证a theorically correct version to test unsigned_high_prod func */
unsigned another_unsigned_high_prod(unsigned x, unsigned y) {
	uint64_t mul = (uint64_t) x * y;//通过64得到2w位的乘积结果
	return mul >> 32;
}

int main(int argc, char* argv[]) {
	unsigned x = 0x12345678;
	unsigned y = 0xFFFFFFFF;
	//比较结果
	assert(another_unsigned_high_prod(x, y) == unsigned_high_prod(x, y));
	return 0;
}

2.76 calloc

库函数calloc声明:void *calloc(size_t nmemb,size_t size);根据文档,其为一个数组分配内存,该数组有nmemb个元素,每个元素size字节,内存设置为0,如果nmeb或者size为0,则calloc返回NULL。

编写实现,通过调用malloc执行分配,调用memset将内存设置为0,你的代码应该没有溢出引起的漏洞,且无论数据类型size_t用多少位表示,代码都正常工作,作为参考,malloc和memset声明如下:

void *malloc(size)t size);
void *memset(void *s,int c,size_t n);

计算所需内存后通过除以size进行验证,没有溢出再进行分配。

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>

void *another_calloc(size_t nmemb, size_t size) {
	if(nmemb == 0 || size == 0)//如果nmeb或者size为0,则calloc返回NULL。
		return NULL;
	size_t buff_size=nmemb*size;//计算需要的区域
	if(nmemb==buff_size/size){//验证是否溢出
		void *ptr=malloc(buff_size);//申请区域
		memset(ptr,0,buff_size);//分配
		return ptr;
	}
	return NULL;
}

int main() {
	void *p;
	p = another_calloc(0x1234, 1);
	assert(p != NULL);
	free(p);
	
	p = another_calloc(SIZE_MAX, 2);
	assert(p == NULL);
	free(p);
	
	return 0;
}

2.77 将整数变量x乘以不同的常数因子K

假设我们有一个任务:生成一段代码,将整数变量x乘以不同的常数因子K。为了提高效率,我们想只使用+、-和<<运算。对于下列K的值,写出执行乘法运算的C表达式,每个表达式中最多使用3个运算。
A.K=17

B.K=-7

C.K=60
D.K=-112

利用左移完成乘法,通过加减拼凑K值。

A:17=16+1,x<<4+x

B:-7=-8+1,-(x<<3)+x

C:60=64-4,x<<6-(x<<2)

D:-112=-128+16,-(x<<7)+x<<4

#include <stdio.h>
#include <assert.h>

// K = 17
int A(int x) {
	return (x << 4) + x;
}

// K = -7
int B(int x) {
	return x - (x << 3);
}

// K = 60
int C(int x) {
	return (x << 6) - (x << 2);
}

// K = -112
int D(int x) {
	return (x << 4) - (x << 7);
}

int main() {
	int x = 0x12345678;
	
	assert(A(x) == x * 17);
	assert(B(x) == x * -7);
	assert(C(x) == x * 60);
	assert(D(x) == x * -112);
	
	printf("Passed.\n");
	return 0;
}

2.78 divide_power2

以正确的舍入方式计算x/2^k,假定0<=k<w-1。

int divide_power2(int x,int k);

通过右移实现除法运算,但是当x是负数的时候,直接右移会导致向下舍入,而不是整数除法采用的向0舍入,如k=4,x二进制为1100111111001100,十进制为-12340,右移4位得到1111110011111100(加粗的4个1是因为符号位为1,右移后得到的)。转换为十进制得到-772,而-12340/2^4=-771.25。向下舍入了,而不是向0舍入。

所以在移位之前,需要加入偏置,修正不合适的舍入。

偏置技术利用如下属性:对于整数x和y(y>0),upper(x/y)=floor((x+y-1)/y)。

当y=2k,c表达式x+(1<<k)-1得到数值x+2k-1,将这个值算术右移k位即产生upper(x/2^k)。

对于使用算术右移的补码机器,c表达式(x<0?x+(1<<k)-1:x)>>k

#include <stdio.h>
#include <assert.h>
#include <limits.h>


int divide_power2(int x, int k) {
	int is_neg = x & INT_MIN;//is_neg =1即负数,0即非负数
	(is_neg && (x = x + (1 << k) - 1));// 替代x<0? (x+((1<<k)-1))/(1<<k) ,如果is_neg=0,非负数,就不会给x增加偏执
	return x >> k;
}

int main() {
	int x = 0x80000007;
	assert(divide_power2(x, 1) == x / 2);
	assert(divide_power2(x, 2) == x / 4);
	return 0;
}

2.79 mul3div4

对于参数x,计算3*x/4,遵循位级整数编码规则,你的代码计算3*x可能会产生溢出。

先通过左移和加法实现乘以3,再利用利用上题的除法代码divide_power2实现除以4.

#include <stdio.h>
#include <assert.h>
#include <limits.h>

int divide_power2(int x, int k) {
	int is_neg = x & INT_MIN;//is_neg =1即负数,0即非负数
	(is_neg && (x = x + (1 << k) - 1));// 替代x<0? (x+((1<<k)-1))/(1<<k) ,如果is_neg=0,非负数,就不会给x增加偏执
	return x >> k;
}

int mul3div4(int x){
	int res=(x<<1)+x;//x*3
	res=divide_power2(res,2);//除以4
	return res;
}

int main() {
	int x = 0x87654321;
	assert(mul3div4(x) == x * 3 / 4);
	return 0;
}

2.80 threefourths

对于整数参数x,计算3/4x的值,向0舍入,它不会溢出,函数遵循位级整数编码规则。

过程

计算3/4倍,没有溢出,向0舍入。

没有溢出意味着先除以4,然后再乘以3,与这里的2.79不同。

向0舍入是有点复杂的。
每个int x, 等于f(前30位数字)加l(最后2位数字)

f = x & ~0x3
l = x & 0x3
x = f + l
threeforths(x) = f/4*3 + l*3/4

f根本不关心舍入,我们只关心从l*3/4开始向0舍入的问题

lmul3 = (l << 1) + l

当x>0时,向0舍入很容易

lm3d4 = lm3 >> 2

当x<0时,四舍五入就像2.78中的divide_power2一样。

题目2.78讨论了除法通过增加偏置实现向0舍入的问题。

bias = 0x3 // (1 << 2) - 1
lm3d4 = (lm3 + bias) >> 2

代码

#include <stdio.h>
#include <assert.h>
#include <limits.h>


int threeforths(int x) {
	int is_neg = x & INT_MIN;
	int f = x & ~0x3;//除低2位之外的高位
	int l = x & 0x3;//低2位
	
	int fd4 = f >> 2;
	int fd4m3 = (fd4 << 1) + fd4;
	
	int lm3 = (l << 1) + l;
	int bias = (1 << 2) - 1;
	(is_neg && (lm3 += bias));//如果是负数加上偏置
	int lm3d4 = lm3 >> 2;
	
	return fd4m3 + lm3d4;
}
int main() {
	assert(threeforths(8) == 6);
	assert(threeforths(9) == 6);
	assert(threeforths(10) == 7);
	assert(threeforths(11) == 8);
	assert(threeforths(12) == 9);
	
	assert(threeforths(-8) == -6);
	assert(threeforths(-9) == -6);
	assert(threeforths(-10) == -7);
	assert(threeforths(-11) == -8);
	assert(threeforths(-12) == -9);
	return 0;
}

2.81 1(w-k)0k 0(w-l-j)1k0j

编写c表达式产生如下位模式,其中a^k表示符号a重复k次,假设一个w位的数据类型,代码可以包含对参数j和k的引用,分别表示j和k的值,但是不能使用表示w的参数。

A.

\[1^{w-k} 0^k \]

B.

\[0^{w-k-j}1^k0^j \]

A.前w-k位全为1,后面低k位全0。-1的补码是全1,-1左移k位即可。-1<<k

B.前w-k-j位是0,中间k位为1,低j位全0。可以先将-1左移k位,得到前w-k位为1,低k位为0,按位取反,前w-k位为0,低k位为1,再次左移,将低j位设置为0.~(-1<<k)<<j

#include <stdio.h>
#include <assert.h>

/* Assume 0 <= k < w */
int A(int k) {
	return -1 << k;
}

/* Assume 0 <= j,k < w */
int B(int k, int j) {
	return ~A(k) << j;
}

int main() {
	assert(A(8) == 0xFFFFFF00);
	assert(B(16, 8) == 0x00FFFF00);
	return 0;
}

2.82 convert to unsigned

我们在一个int类型值为32位的机器上运行程序。这些值以补码形式表示,而且它们都是算术右移的。unsigned类型的值也是32位的。
我们产生随机数x和y,并且把它们转换成无符号数,显示如下:

/*Create some arbitrary values*/
int x=random();
int y=random();
/*Convert to unsigned*/
unsigned ux=(unsigned)x;
unsigned uy=(unsigned)y;

对于下列每个C表达式,你要指出表达式是否总是为1.如果它总是为1,那么请描述其中的数学原理。否则,列举出一个使它为0的参数示例。

A.(x<y)==(-x>-y)
B.((x+y)<<4)+y-x==17*y+15*x 
C. ~x+~y+1==~(x+y)
D.(ux-uy)==-(unsigned)(y-x)
E.((x>>2)<<2)<=x

A.x=INT_MAX,转换为负数溢出

B.正确,无论是否溢出,

	((x+y)<<4)+y-x
    ==>x<<4-x+y<<4+y
    ==>17*y+15*x 

C.正确。

P66的旁注,对于任意整数x,计算表达式-x和~x+1得到的结果完全一样。

x=y=-1,0+0+1!=-1

~x+~y+1==~(x+y)
	~x+~y+1
==>~x+1+~y+1-1
==>-x+-y-1
==>-(x+y)-1
==>~(x+y)

D.正确。

 (ux - uy) == -(unsigned) (y - x)
=>
  -(ux - uy) == (unsigned) (y - x)
=>(ux - uy) == (unsigned) (x - y)

E.正确。

((x>>2)<<2)<=x
   x >> 2 << 2
==>x & ~0x3
==>x - num(00/01/10/11)
==>((x >> 2) << 2) <= x

2.83 给出一个由Y和k组成的公式表示这个无穷串的值

一些数字的二进制表示是由形如0.yyyyyy…的无穷串组成的,其中y是一个k位的序列。例如,1/3的二进制表示是0.01010101…(y=01),而1/5的二进制表示是0.001100110011…(y=0011)。
A.设Y=B2Uk(y),也就是说,这个数具有二进制表示y。给出一个由Y和k组成的公式表示这个无穷串的值。
提示:请考虑将二进制小数点右移k位的结果。
B.对于下列的y值,串的数值是多少?
(a)101
(b)0110
(c)010011

A:
  这个问题的关键是找到y,k 和数字n的关系
  我们假设这个数字是n
  那么n = 0.yyyyyyyy... 这个时候是无法得出结果的,并没有用到k
  那么要想用到k,我们把n左移k为 n << k = y.yyyyyyyy...(反正y是不断重复的)
  因为y是k位序列,y.yyyyyyyyy... = y+0.yyyyy...=Y + n
  因此得出 n << k = Y + n === > n << k - n = Y == > n = Y/(2^k - 1)
B:
(a).
y = 101, Y = 5, k = 3
n = 5/7

(b).
y = 0110, Y = 6, k = 4
n = 2/5

(c).
y = 010011, Y = 19, k = 6
n = 19/63

2.84 float_le

填写下列程序的返回值,这个程序测试它的第一个参数是否小于或者等于第二个参数。假定函数f2u返回一个无符号32位数字,其位表示与它的浮点参数相同。你可以假设两个参数都不是NaN。两种0,+0和一0被认为是相等的。

int float_1e(float x,float y){
unsigned ux=f2u(x);
unsigned uy=f2u(y);
/*Get the sign bits*/
unsigned sx=ux >>31;
unsigned sy=uy >>31;
/*Give an expression using only ux,uy,sx,and sy*/
return;
}

如果使用判断就比较简单,下面是不使用判断.

第一个参数是否小于等于第二个参数,将可能满足的情况(即会返回1的情况)进行分类处理。
情况一:两个参数相等且为0, 根据IEEE规则,0用Denormalized表示,且有+0(0x0)和-0(0x80000000)两种表示 所以通过左移一位来比较
==>ux << 1 == 0 && uy << 1 == 0
情况二:第一个参数为负(此时ux>>31为sx=0x1得到!sx=0) ,第二个参数为0或者正数(此时uy>>31为sy=0x0 !sy=1)
==>(sx && !sy)

反之,若第一个参数为正或者0, 第二个参数为负,这种情况肯定返回0,就不需要特殊处理。

情况三:两个参数都为正

==> (!sx && !sy && ux <= uy)

情况四:两个参数都为负,此时根据IEEE的定义 正数可以用无符号整数的升序进行排列(正数越大 无符号数越大) 负数可以用无符号整数的升序进行排列(负数越小,无符号数越大)

==> (sx && sy && ux >= uy)

四种情况或运算,满足一种就返回1。

#include <stdio.h>
#include <assert.h>
//不能使用强制类型转换
unsigned f2u(float x)
{
	return *((unsigned *)&x);
	
}

int float_le(float x, float y)
{
	unsigned ux = f2u(x);
	unsigned uy = f2u(y);
	unsigned sx = ux>>31;
	unsigned sy = uy>>31;
    return (ux << 1 == 0 && uy << 1 == 0) || /* both zeros */
            (sx && !sy) ||                         /* x < 0, y >= 0 or x <= 0, y > 0 */
            (!sx && !sy && ux <= uy) ||            /* x > 0, y >= 0 or x >= 0, y > 0 */
            (sx && sy && ux >= uy);                /* x < 0, y <= 0 or x <= 0, y < 0 */
}
int main(int argc, char* argv[]) {
	assert(float_le(-0, +0));
	assert(float_le(+0, -0));
	assert(float_le(0, 3));
	assert(float_le(-4, -0));
	assert(float_le(-4, 4));
	return 0;
}

2.85 阶码、尾数、小数、值公式

给定一个浮点格式,有k位指数和n位小数,对于下列数,写出阶码E、尾数M、小数f和值V的公式。另外,请描述其位表示。
A.数7.0。
B.能够被准确描述的最大奇整数。
C.最小的规格化数的倒数。

A.

7.0不接近0,所以采用规格化浮点数,bias=2(k-1)-1,E=e-bias,V=M*2E,M=1+f.

7.0=111.000=1.11000x2^2,M=1.11,E=2,因为M=1+f==>f=M-1=0.11,V=7.0

E=2=e-bias

==>e=E+bias=1+2^(k-1),即阶码第k-1位和第0位为1(k-1 k-2 ... 0)1000...001

==>0 1000...001 1100...(即 符号位s 实际阶码E编码后的e 尾数f)

B.

最大奇整数,M必须是1.111...,则f=0.111...(n位小数,每个n都为1),那么M=1+f,得到了n+1个1,即V=2^(n+1)-1,得到阶码E=n,则e=E+bias=n+bias

==>0 n+bias 1111....

C.最小的规格化数的倒数,

最小的规格数,M必须是1.0000...的样式,E=1-bias,V=2^(1-bias), 取倒数得到V=2^(bias -1)==>E=bias-1,

得到e=bias+E==>2*bias-1=2*[2^(k-1)-1]-1=2^k-2-1=2^k-3==> 0 11...101 000...

2.86 扩展精度

与lntel兼容的处理器也支持“扩展精度”浮点形式,这种格式具有80位字长,被分成1个符号位、k=15个阶码位、1个单独的整数位和n=63个小数位。整数位是IEEE浮点表示中隐含位的显式副本。也就是说,对于规格化的值它等于1,对于非规格化的值它等于0。填写下表,给出用这种格式表示的一些“有趣的”数字的近似值。

描述 扩展精度-值 扩展精度-十进制
最小的正非规格化数
最小的正规格化数
最大的规格化数

将数据类型声明为long double,就可以把这种格式用于为与Intel兼容的机器编译C程序。
但是,它会强制编译器以传统的8087浮点指令为基础生成代码。由此产生的程序很可能会比数据类型为float或double的情况慢上许多。

过程

第一行答案:
最小的正非规格化数,要满足下面几个条件

  1. 符号位为1
  2. 阶码位全部为0
  3. 单独的整数位为0
  4. 小数位最后一位为1,其他都为0
    得出的结论是: 0 000..00(15位) 0 000..01(63位)
偏量bias = 2^(k-1) - 1 = 2^(15-1) - 1 = 2^14 - 1
E = 1 - bias = 1 - 2^14 + 1 = 2 - 2^14
V = M * 2^E = 2^(-63) * 2^(2 - 2^14) = 2^(-63 + 2 - 2^14) = 2^(-61-2^14)

第二行答案:
最小的正规格数,满足下边几个条件

  1. 符号位为0
  2. 阶码位为1
  3. 按照该题目要求,单独的整数位为1
  4. 小数位全是0
    得出的结论是: 0 000..01(15位) 1 000..00(63位)
偏量bias = 2^(k-1) - 1 = 2^(15-1) - 1 = 2^14 - 1
E = e - bias = 1 - 2^14 + 1 = 2 - 2^14
V = M * 2^E = 1 * 2^(2 - 2^14)

第三行答案:
最大的规格数,满足下边几个条件

  1. 符号位为0
  2. 阶码位除最低位以外全为1(因为规格化阶码不能全1或者全0)
  3. 按照该题目要求,单独的整数位为1
  4. 小数位全是1
    得出的结论是: 0 111..10(15位) 1 111..11(63位)
偏量bias = 2^(k-1) - 1
E = e - bias = 2^15 - 2 - bias
V = M * 2^E = M * 2^(2^15 - 2 - bias) = M * 2^(2^14 * 2 - 2 - bias)
= M * 2^(2bias - bias) = M * 2^bias
此时M = 1 + (1 - 2^-63) = 2 - 2^-63

得出最终的结果是:2^bias * (2 - 2^-63)

结果

描述 扩展精度-值 扩展精度-十进制
最小的正非规格化数 0 0000…(15) 0 000…(62)1 2^(1-bias-63)
最小的正规格化数 0 000…(14)1 1 000….(63) 2^(1-bias)
最大的规格化数 0 111…(14)0 1 111…(63) 2^bias * (2-2^-63)

2.87 16位的“半精度”浮点格式

2008版IEEE浮点标准,即IEEE754-2008,包含了一种16位的“半精度”浮点格式。它最初是由计算机图形公司设计的,其存储的数据所需的动态范围要高于16位整数可获得的范围。这种格式具有1个符号位、5个阶码位(k=5)和10个小数位(n=10)。阶码偏置量是2^(5-1)-1=15。
对于每个给定的数,填写下表,其中,每一列具有如下指示说明:
Hex:描述编码形式的4个十六进制数字。
M:尾数的值。这应该是一个形如x或x/y的数,其中x是一个整数,而y是2的整数幂。例如:0、67/64和1/256.
E:阶码的整数值。
V:所表示的数字值。使用x或者x×2^E表示,其中x和z都是整数。
D:(可能近似的)数值,用printf的格式规范f打印。
举一个例子,为了表示数7/8,我们有s=0,M=7/4和E=-1.因此这个数的阶码字段为01110(十进制值15-1=14),尾数字段为1100000000,得到一个十六进制的表示3B00。其数值为0.875。
标记为“一”的条目不用填写。

描述 Hex M E V D
-0 -0
最小的>2的值
512 512
最大的非规格化数
负无穷 - - 负无穷 负无穷
十六进制表示为38B0的数 3BB0

过程

-0:
首先尾数M必须为0
阶码可以设置成00000 因此E = 1 - bias = 1 - 15 = -14
得到的位模式为:1 00000 0000000000 ==> 0x8000


最小的>2的值:
由M * 2^E ==> E = 1,M = 1.0000000001
E = e - bias ==> e = E + bias = 16 ==> e = 100000
M的值为2^-10 + 1 = 1025/1024
得到的位模式为:0 10000 0000000001 ==> 0x4001
V = 1025/1024 * 2 = 1025/512


512:
M = 1 E = 9 = e - bias ==> e = 9 + 15 = 24 ==> 11000
得到的位模式为:0 11000 0000000000 ==> 0x6000


最大的非规格化数:
非规格化数表示阶码位都是0 E= 1 - bias = -14
因为最大所以尾数全1
M 1023/1024
得到的位模式为:0 00000 1111111111 ==> 0x03FF


-oo:
阶码全1,尾数全0
1 11111 0000000000 ==> 0xFC00


十六进制表示为3BB0:
先把这个数展开:0011 1011 1011 0000 ==> 0 01110 1110110000
e = 14 
E = e - bias = 14 - 15 = -1
M = 2^-1 + 2^-2 + 2^-3 + 2^-5 + 2^-6 = 59/64
V = M * 2^E = 59/64 * 2^-1 = 59/128

结果

描述 Hex M E V D
-0 0x8000 0 -14 -0-0 -0.0
最小的>2的值 0x4001 1025/1024 1 1025/512 2.00195312
512 0x6000 1 9 512 512.0
最大的非规格化数 0x03FF 1023/1024 -14 1023/(2^24) 6.09755516e-5
-∞ 0xFC00 - - -∞ -∞
十六进制表示为38B0的数 0x3BB0 123/64 -1 123/128 0.9609375

2.88 格式A转换为格式B

考虑下面两个基于IEEE浮点格式的9位浮点表示。
1.格式A

  • 有一个符号位。

  • 有k=5个阶码位。阶码偏置量是15。

  • 有n=3个小数位。

2.格式B

  • 有一个符号位。
  • 有k=4个阶码位。阶码偏置量是7。
  • 有n=4个小数位。

下面给出了一些格式A表示的位模式,你的任务是把它们转换成最接近的格式B表示的值。
如果需要舍入,你要向+∞舍入。另外,给出用格式A和格式B表示的位模式对应的值。要么是整数(例如17),要么是小数(例如17/64或17/2^6)。

格式A-位 格式A-值 格式B-位 格式B-值
1 01110 001 -9/16 1 0110 0010 -9/16
0 10110 101
1 00111 110
0 00000 101
1 11011 000
0 11000 100

过程

注意:如果是规格化的M = 1 + f 非规格化M = f

0 10110 101 :
A:
E = 22 - 15 = 7,V = (2^-1 + 2^-3 + 1) * 2^7 = 13 * 2^4
B:
通过观察,我们发现,先保持小数位不变,求阶码,如果不行,在改变小数位.
E=7+7=14==>1110,f直接加0
因此B的 0 1110 1010, V = 13 * 2^4

    
1 00111 110:
A:
E = 7 - 15 = -8 
V=(2^-1 + 2^-2 + 1) * 2^-8 = 7/4 * 2^-8 = -7/2^10
先强制把阶码最后的1给尾数,得到B:
1 0011 1110 ==> M = 1 + 2^-1 + 2^-2 + 2^-3 = 15/8
==> 2^E = (7/2^10) / (15/8)  = 7/15 / 2^7 约等于2^-1*2^-7 = 2^-8

我们看看2^E的范围 2^-6(1-7) ~ 2^7(14-7),因为e=1(即0001)~14(1110)
由于上边计算的2^-8不在这个范围中,因此需要调整阶码的值
先从最小的开始,设阶码为2^-6(即0000) ,因为V=M*2^E,那么M=V/2^E= 7/2^10 / 2^-6 = 7 / 16
==> (1/16 + 2/16 + 4/16) ==> (1/16 + 1/8 + 1/4)  ==> (2^-4 + 2^-3 + 2^-2)
因此B的 0 0000 0111


0 00000 101:
A:非规格化
E = 1 - 15 = -14 
V = (2^-1 + 2^-3) * 2^-14 = 5 * 2^-3 * 2^-14 = 5 * 2^-17 = 5/(2^17)
假设使用101作为尾数,那么M = (2^-1 + 2^-3 + 1) = 13 * 2^-3
2^E = V/M = 5/(2^17) / (13 * 2^-3) 
    = 5/13 * 2^-14 显然不在范围之内(2^E的范围 2^-6(1-7) ~ 2^7(14-7))
先从最小的开始,设阶码为2^-6 那么 M=V/2^E=5/(2^17) / 2^-6 = 5 * 2^-11 显然B无法表示这个小数值
取一个最近似的 0 0000 0001


1 11011 000:
A:
E = 27 - 15 = 12 
V = 2^12 取-  得-2^12
B:
由于这个值比较大,因此阶码取最大值1110
得到e = 14,所以 E = e - 7 = 14 - 7 = 7 这样才能计算M的最小值
M = V/2^E=2^12 / 2^7 = 2^5
3位尾数无法表示,所以尾数取全1,尽可能接近2^5
得到 1 1110 1111

0 11000 100:
A:
E=2^4+2^3-(2^4-1)=16+8-15=9
M=1+f=1+1/2=3/2
V=3/2*2^9=3*2^8
B:
因为值很大,所以根据题目要求“如果需要舍入,你要向+∞舍入”,向正无穷舍入

结果

格式A-位 格式A-值 格式B-位 格式B-值
1 01110 001 -9/16 1 0110 0010 -9/16
0 10110 101 13*2^4 0 1110 1010 13*2^4
1 00111 110 -7/2^10 1 0000 0111 -7/2^10
0 00000 101 5/2^-17 0 0000 0001 1/2^-10
1 11011 000 -2^12 1 1110 1111 -31*2^3
0 11000 100 3*2^8 0 1111 0000 +oo

2.89 整数转换成double类型的值

我们在一个int类型为32位补码表示的机器上运行程序。float类型的值使用32位IEEE格式,而double类型的值使用64位IEEE格式。
我们产生随机整数x、y和z,并且把它们转换成double类型的值:

/*Create some arbitrary values*/
int x=random();
int y=random();
int z=random();
/*Convert to double*/
double dx=(double)x;
double dy=(double)y;
double dz=(double)z;

对于下列的每个C表达式,你要指出表达式是否总是为1。如果它总是为1,描述其中的数学原理。否则,列举出使它为0的参数的例子。请注意,不能使用IA32机器运行GCC来测试你的答案,因为对于float和double,它使用的都是80位的扩展精度表示。
A.(float)x(float)dx
B. dx-dy
(double)(x-y)
C.(dx+dy)+dz= =dx+(dy+dz)
D.(dx*dy)*dzdx*(dy*dz)
E. dx/dx
dz/dz

#include <stdio.h>
#include <assert.h>
#include <limits.h>
#include "lib/random.h"

/*
 * most important thing is that all double number come from ints
 */

/* right  (float)x==(float)dx */
int A(int x, double dx) {
  return (float)x == (float)dx;
}

/* wrong when y is INT_MIN, dx-dy==(double)(x-y)*/
int B(int x, double dx, int y, double dy) {
  return dx-dy == (double)(x-y);
}

/* right */
int C(double dx, double dy, double dz) {
  return (dx+dy)+dz == dx+(dy+dz);
}

/*
 * wrong
 *
 * FIXME I don't know what conditions cause false
 */
int D(double dx, double dy, double dz) {
  return (dx*dy)*dz == dx*(dy*dz);
}

/* wrong when dx != 0 and dz == 0 */
int E(double dx, double dz) {
  return dx/dx == dz/dz;
}

int main(int argc, char* argv[]) {
  init_seed();

  int x = random_int();
  int y = random_int();
  int z = random_int();
  double dx = (double)x;
  double dy = (double)y;
  double dz = (double)z;

  printf("%x %x %x\n", x, y, z);

  assert(A(x, dx));
  assert(!B(0, (double)(int)0, INT_MIN, (double)(int)INT_MIN));
  assert(C(dx, dy, dz));
  /* magic number, brute force attack */
  assert(!D((double)(int)0x64e73387, (double)(int)0xd31cb264, (double)(int)0xd22f1fcd));
  assert(!E(dx, (double)(int)0));
  return 0;
}

2.90 2^x fpower2

分配给你一个任务,编写一个C函数来计算2^x的浮点表示。你意识到完成这个任务的最好方法是直接创建结果的IEEE单精度表示。当x太小时,你的程序将返回0.0。当x太大时,它会返回+oo。填写下列代码的空白部分,以计算出正确的结果。假设函数u2f返回的浮点值与它的无符号参数有相同的位表示。

float fpwr2(int x)
{
    /*Result exponent and fraction*/
    unsigned exp,frac;
    unsigned u;
    if(x<________)(
    	/*Too small.Return 0.0*/
        exp=_______;
        frac=______;
    }else if(x<_______)(
    	/*Denormalized result*/
    	exp=_______;
        frac=______;
    }else if(x<_______)(
   		 /*Normalized result.*/
   		 exp=;frac=;
	}else{
   		 /*Too big.Return +oo*/
   		exp=_______;
        frac=______;
    }
    /*Pack exp and frac into 32 bits*/
    u=exp<<23 | frac;
    /*Return as float*/
    return u2f(u);
}
#include <stdio.h>
#include <assert.h>
#include <math.h>
//返回的浮点值与它的无符号参数有相同的位表示。
float u2f(unsigned x) {
	return *(float*) &x;
}

/* 2^x */
float fpwr2(int x) {
	/* Result exponent and fraction  结果的指数和分数部分 */
	unsigned exp, frac;
	unsigned u;
	
	/* 因为2^x 是大于0的,因此我们 首先要确定浮点数能够表示的正非规格化数的最小值是
	  0 00000000 00000...001  ==> 2^-23 * 2^(1-bias) = 2^-23 * 2^(1-(2^7 - 1))
	  = 2^-23 * 2^(2-2^7)) = 2^(2 - 2^7 -23) = 2 - 128 - 23 = -149
	 */
	if (x < 2-pow(2,7)-23) {//阶码E=2 - 2^7 -23
		/* too small. return 0.0 */
		exp = 0;
		frac = 0;
	} else if (x < 2-pow(2,7)) {
		/* Denormalized result */
		/* 求出最小的规格化数
		  0 00000001 00000...000
		  E = 1 - 2^7 + 1 = 2 - 2^7 = -126 */
		
		exp = 0;
		/* 这段代码块求的值应该是非规格化数范围内的值
		  根据 V = M * 2^E 和V = 2^x  ==> 2^x = M * 2^E
		  frac = M = 2^x / 2^E
		  E = 1 - bias = 2-2^7
		  frac = 2^(x - (2 - 2^7)) 这个是frac的值,但是我们如何获得它的位模式呢?
		  我们知道0 00000000 00000...001 最后边这个1对应的值是2^-23 也就是说
		  小数位的值和他的位模式有一个对应关系,我们只要求出frac是最后这个1(2^-23)的多少
		  倍,然后1 << 这个倍数就可以了,这样就得到了frac的位模式
		 */
		frac = 1 << (unsigned)(x - (2-pow(2,7)-23));
	} else if (x < pow(2,7)-1+1) {
		/* Normalized result */
		/* 11111111 2^8 -1 - (2^7 - 1) ==> 2^8 - 2^7 -1 + 1 ==> 2^7
		  因此求exp 就等于求e e = E + bias = x + (2^7 - 1)
		 */
		exp = pow(2,7)-1+x;
		frac = 0;
	} else {
		/* Too big, return +oo */
		exp = 0xFF;
		frac = 0;
	}
	
	/* pack exp and frac into 32 bits */
	u = exp << 23 | frac;
	/* Result as float */
	return u2f(u);
}

int main(int argc, char* argv[]) {
	assert(fpwr2(0) == powf(2,0));
	assert(fpwr2(100) == powf(2,100));
	assert(fpwr2(-100) == powf(2,-100));
	assert(fpwr2(10000) == powf(2,10000));
	assert(fpwr2(-10000) == powf(2,-10000));
	return 0;
}

2.91 float_denorm_zero

大约公元前250年,希腊数学家阿基米德证明了223/71<Π<22/7等。如果当时有一台计算机和标准库
<math.h>,他就能够确定x的单精度浮点近似值的十六进制表示为0x40490FDB。当然,所有的这些都只是近似值,因为Π不是有理数。
A.这个浮点值表示的二进制小数是多少?
B.22/7的二进制小数表示是什么?提示:参见家庭作业2.83。
C.这两个Π的近似值从哪一位(相对于二进制小数点)开始不同的?
位级浮点编码规则
在接下来的题目中,你所写的代码要实现浮点函数在浮点数的位级表示上直接运算。你的代码应该完全遵循IEEE浮点运算的规则,包括当需要舍入时,要使用向偶数舍人的方式。
为此,我们把数据类型float-bits等价于unsigned:

/*Access bit-1evel representation floating-point nunber*/
typedef unsigned float_bits;

你的代码中不使用数据类型float,而要使用float_bits。你可以使用数据类型int和unsigned,包括无符号和整数常数和运算。你不可以使用任何联合、结构和数组。更重要的是,你不能使用任何浮点数据类型、运算或者常数。取而代之,你的代码应该执行实现这些指定的浮点运算的位操作。
下面的函数说明了对这些规则的使用。对于参数f,如果f是非规格化的,该函数返回±0(保持f的符号),否则,返回f。

/*If f is denorm,return 0.Othervise,return f*/
float_bits float_denorm_zero(float_bits f){
    /*Decompose bit representation into parts*/
    unsigned sign=f>>31;
    unsigned exp=f>>23&0XFF;
    unsigned frac=f&0x7FFFFF;
    if(exp==0){
        /*Denormalized.Set fraction to 0*/
        frac=0;
    }
    /*Reassemble bits */
    return(sign<<31)|(exp<<23)|frac;
}
A.这个浮点值表示的二进制小数是多少?
B.22/7的二进制小数部分表示是什么?提示:参见家庭作业2.83。
C.这两个Π的近似值从哪一位(相对于二进制小数点)开始不同的?
    
A:
0x40490FDB 展开后 0100 0000 0100 1001 0000 1111 1101 1011
换成float小数的位模式: 0 10000000 10010010000111111011011

由于2^E = 2^(127-127+1)=2, V = 2^E*M=2M  我们知道M =f+1= 1.10010010000111111011011 那么2m
就相当于 << 1 得到:11.0010010000111111011011

B:
在问题2.83中我们得出这么一个公式:n = Y/(2^k - 1)
在本题中,22/7的二进制小数部分是1/7,即 n = 1/7 也就是说Y = 1 k = 3 说明Y是3位且值为1 因此就是001
所以最终的答案是11.001001001...(001)

C:
十进制小数转二进制数:“乘以2取整,顺序排列”(乘2取整法)
223/71 = 3.140845070422535 小数部分:0.140845070422535
0.140845070422535 * 2 = 0.28169014084507  ----- 取整 ----- 0
0.28169014084507 * 2 = 0.563380281690141  ----- 取整 ----- 0
0.563380281690141 * 2 = 1.126760563380282  ----- 取整 ----- 1
0.126760563380282 * 2 = 0.253521126760563  ----- 取整 ----- 0
0.253521 * 2 = 0.507042  ----- 取整 ----- 0
0.507042 * 2 = 1.014084  ----- 取整 ----- 1
0.014084 * 2 = 0.028168  ----- 取整 ----- 0
0.028168 * 2 = 0.056336  ----- 取整 ----- 0
0.056336 * 2 = 0.112672  ----- 取整 ----- 0
0.112672 * 2 = 0.225344  ----- 取整 ----- 0
因此在第9位就不同了

2.92 float_negate

遵循位级浮点编码规则,实现具有如下原型的函数:

/*Compute-f.If f is Nal,then return f.*/
float_bits float_negate(float_bits f);

对于浮点数f,这个函数计算-f。如果f是NaN,你的函数应该简单地返回f。
测试你的函数,对参数f可以取的所有2^32个值求值,将结果与你使用机器的浮点运算得到的结果.

#include <stdio.h>
#include <assert.h>

typedef unsigned float_bits;
/* 对于浮点数f,这个函数计算-f。如果f是NaN,你的函数应该简单地返回f。*/
float_bits float_negate(float_bits f) {
	unsigned sig = f >> 31;
	unsigned exp = f >> 23 & 0xFF;
	unsigned frac = f & 0x7FFFFF;
	
	int is_nan = (exp == 0xFF && frac != 0);
	if (is_nan) {
		return f;
	}
	
	return ~sig << 31 | exp << 23 | frac;
}

int main() {
	printf("%u", float_negate(32.0));
	assert(float_negate(32.0) == -32.0);
	return 0;
}

为什么这个验证过不了。。。

2.93 float_absval

遵循位级浮点编码规则,实现具有如下原型的函数:

/*Compute Ifl.If f is NaN,then return f.*/
float_bits float_absval(float_bits f);

对于浮点数f,这个函数计算|f|。如果f是NaN,你的函数应该简单地返回f。
测试你的函数,对参数f可以取的所有2^32个值求值,将结果与你使用机器的浮点运算得到的结果相比较。

#include <stdio.h>
#include <assert.h>

typedef unsigned float_bits;
/* 对于浮点数f,这个函数计算|f|。如果f是NaN,你的函数应该简单地返回f。*/
float_bits float_absval(float_bits f) {
	unsigned exp = f >> 23 & 0xFF;
	unsigned frac = f & 0x7FFFFF;
	
	int is_NAN = (exp == 0xFF) && (frac != 0);
	if (is_NAN) {
		return f;
	}
	
	return 0 << 31 | exp << 23 | frac;
}

int main() {
	printf("%u\n", float_absval(32.0));
	return 0;
}

2.94 float_twice

遵循位级浮点编码规则,实现具有如下原型的函数:

/*Compute 2*f.If f is NaN,then return f.*/
float_bits float_twice(float_bits f);

对于浮点数f,这个函数计算2.0·f。如果f是NaN,你的函数应该简单地返回f。测试你的函数,对参数f可以取的所有2^32个值求值,将结果与你使用机器的浮点运算得到的结果相比较。

#include <stdio.h>
#include <assert.h>

typedef unsigned float_bits;
/* 对于浮点数f,这个函数计算2.0·f。如果f是NaN,你的函数应该简单地返回f。*/
/*
  浮点数*2,因为`V=2^E*M`,
  规格化把阶码E加一即可,
	  但是+1有个特殊情况,要是e的位模式为11111110 +1 就需要特殊处理,返回+∞;
  非规格化,E不能更改,f<<1实现乘以2.
 */
float_bits float_twice(float_bits f) {
	unsigned sig = f >> 31;
	unsigned exp = f >> 23 & 0xFF;
	unsigned frac = f & 0x7FFFFF;
	
	int is_nan_or_oo = (exp == 0xFF);
	if (is_nan_or_oo) {
		return f;
	}
	
	if (exp == 0) {//非规格化改变尾数
		frac <<= 1;
	} else if (exp == 0xFE) {//11111110 +1 就需要特殊处理,返回+∞
		exp = 0xFF;
		frac = 0;
	} else {//规格化改变阶码
		exp += 1;
	}
	
	return sig << 31 | exp << 23 | frac;
}

int main() {
	printf("%u\n", float_twice(32.22));
	return 0;
}

2.95 float_half

遵循位级浮点编码规则,实现具有如下原型的函数:

/*Compute 0.5*f.If f is NaN,then return f.*/
float_bits float_half(float_bits f);

对于浮点数f,这个函数计算0.5·f。如果f是NaN,你的函数应该简单地返回f。

测试你的函数,对参数f可以取的所有22个值求值,将结果与你使用机器的浮点运算得到的结果相比较。

通过分类讨论处理规格化和非规格化数,同时对向偶取整进行处理。

#include <stdio.h>
#include <assert.h>

typedef unsigned float_bits;
/* 对于浮点数f,这个函数计算0.5·f。如果f是NaN,你的函数应该简单地返回f。*/
/*
 * 这里就用到了向偶数取整的知识,在下边的注释中描述的很详细
 * 那么如何理解取整呢,我们假设这个被右移出去的位为a,那么a就有可能是1或者0,如果是0,那么我们
  就不需要取整,如果是1,我们可以这么想:1111.a 这个a如果是1,折算成小数就是0.5 因此是需要
  取整的,它前边的那一位如果是0,表示已经是偶数了,就舍弃a 如果是1,要向上取整,在未右移之前+1就可以了
 */


float_bits float_half(float_bits f) {
	unsigned sig = f >> 31;
	unsigned rest = f & 0x7FFFFFFF;//去掉符号位的剩余部分
	unsigned exp = f >> 23 & 0xFF;
	unsigned frac = f & 0x7FFFFF;
	
	int is_NAN_or_oo = (exp == 0xFF);
	if (is_NAN_or_oo) {
		return f;
	}
	/*
	 * round to even, we care about last 2 bits of frac
	 *
	 * 00 => 0 just >>1
	 * 01 => 0 (round to even) just >>1
	 * 10 => 1 just >>1
	 * 11 => 1 + 1 (round to even) just >>1 and plus 1
	 */
	int addition = (frac & 0x3) == 0x3;//frac最低两位,是否全1,如果全1需要特殊处理,移位之后加一
	
	if (exp == 0) {//非规格化
		/* Denormalized */
		frac >>= 1;
		frac += addition;//如果全1需要特殊处理,移位之后加一
	} else if (exp == 1) {//规格化情况1:f的exponent field为0000 0001,需要从规格化转为非规格化
		/* Normalized to denormalized */
		/*rest >>= 1任务:
		  1.frac右移1位实现/2
		  2.将expnent field的1移到fraction field最靠近0那位保证除2效果
		  因为规格化M=1+f,而非规格化M=f,需要在前面补个1*/
		rest >>= 1;
		rest += addition; //向偶取整
		exp = rest >> 23 & 0xFF;//重新划分exp和frac
		frac = rest & 0x7FFFFF;
	} else {//规格化情况2:去掉情况1的规格化数E-1即可
		/* Normalized */
		exp -= 1;
	}
	
	return sig << 31 | exp << 23 | frac;
}

2.96 float_f2i

遵循位级浮点编码规则,实现具有如下原型的函数:

/*Compute(int)f.
*If conversion causes overflow or f is NaN,return 0x80000000*/
int float_f2i(float_bits f);

对于浮点数f,这个函数计算(int)f。如果f是NaN,你的函数应该向零舍入。如果f不能用整数表示(例如,超出表示范围,或者它是一个NaN),那么函数应该返回0x80000000。
测试你的函数,对参数f可以取的所有2^32个值求值,将结果与你使用机器的浮点运算得到的结果相比较。

我们首先考虑作为浮点数f能表示的最大的合法的整数是多少?
V = M * 2^E,E = e - bias 由这两个公式可知E越大越好也就是e越大越好
而 e ==> 11111110 不能是11111111,
我们再考虑一个范围 0 <= f < 1 如果f在这个范围中,那么它的值就直接取0
我们要找出这个范围的浮点位模式,

V=0:0 00000000 00000000000000000000000
V=1:0 01111111 00000000000000000000000
在上边的这个空间的值直接取0就行

那么f能表示的最大的合法的规格数是 0 11111110 11111111111111111111111
超过这个数的就成为越界了,因为E=e-bias,e=E+bias,因为int 最大2^31,所以E最大31,得到e=31+bias。

如果在这个范围内:
E = exp - bias;

我们知道M的值的二进制小数是1.xxxxx... 但是下边M的值明显是做了<<23操作的(因为M = frac | 0x800000;//1000 00000,通过|0x800000来给f+1得到M),因此后边就要用E- 23,把之前左移的23位减去。
M = frac | 0x800000;//M=f+1
V = M * 2^E 根据这个公式,向0取整

if (E > 23) {
  num = M << (E - 23);//<<23
} else {
  num = M >> (23 - E);//如果不够,就右移回去不够的位,向0取整
}
#include <stdio.h>
#include <assert.h>

typedef unsigned float_bits;
/* Compute (float) f
 * If conversion cause overflow or f is NaN, return 0x80000000
 */
int float_f2i(float_bits f) {
	unsigned sig = f >> 31;
	unsigned exp = f >> 23 & 0xFF;
	unsigned frac = f & 0x7FFFFF;
	unsigned bias = 0x7F;
	
	int num;
	unsigned E;
	unsigned M;
	
	/*
	 * consider positive numbers
	 *
	 * 0 00000000 00000000000000000000000
	 *   ===>
	 * 0 01111111 00000000000000000000000
	 *   0 <= f < 1
	 * get integer 0
	 *
	 * 0 01111111 00000000000000000000000
	 *   ===>
	 * 0 (01111111+31) 00000000000000000000000
	 *   1 <= f < 2^31
	 * integer round to 0
	 *
	 * 0 (01111111+31) 00000000000000000000000
	 *   ===>
	 * greater
	 *   2^31 <= f < oo
	 * return 0x80000000
	 */
	if (exp >= 0 && exp < 0 + bias) {
		/* number less than 1 */
		num = 0;
	} else if (exp >= 31 + bias) {
		/* number overflow */
		/* or f < 0 and (int)f == INT_MIN */
		num = 0x80000000;
	} else {
		E = exp - bias;
		M = frac | 0x800000;//100 00000
		if (E > 23) {
			num = M << (E - 23);
		} else {
			/* whether sig is 1 or 0, round to zero */
			num = M >> (23 - E);
		}
	}
	
	return sig ? -num : num;
}

2.97

遵循位级浮点编码规则,实现具有如下原型的函数:

/*Compute(float)i*/
float_bits float_i2f(int i);

对于函数i,这个函数计算(float)i的位级表示。
测试你的函数,对参数f可以取的所有2^32个值求值,将结果与你使用机器的浮点运算得到的结果相比较。

typedef unsigned float_bits;

float u2f(unsigned f)
{
    return *((float *)&f);
}

unsigned f2u(float f)
{
    return *((unsigned *)&f);
}

//判断int类型正数长度
unsigned length(int i)
{
    int length = 0;
    while((unsigned)i>=(unsigned)(1<<length))//通过找左边第一个1
    {
        length++;
    }
    return length;
}

//给定一个t长度 求其t位都为1的掩码
unsigned mask(unsigned t)
{
    return (1<<t) -1;
}

float_bits float_i2f(int i)
{
    //提取fraction field
    unsigned frac;

    //提取exponent field
    unsigned exp;

    //提取sign
    unsigned sign;
    //bias为常量
    unsigned bias = 127;

    //如果i为0,进行特殊处理
    //如果i为INT_MIN,进行特殊处理
    if(i==0)
    {
        sign = 0;
        frac = 0x0;
        exp = 0;
        return sign<<31|exp<<23|frac;
    }
    //如果i为INT_MIN,进行特殊处理
    if(i==INT_MIN)
    {
        sign = 1;
        frac = 0x0;
        exp = 31+bias;
        return sign<<31|exp<<23|frac;
    }
    //记录i的正负
    unsigned i_sign = !!((unsigned )(i>>31));
    //否则若为 负数先将负数转化为正数再进行处理
    i = i<0 ? -i:i;

    //查看i的最高位1到第0位的长度
    unsigned i_length = length(i);
      //因为此时的float一定为Normalized Value 所以要隐藏最高位
    unsigned f_length = i_length-1;
    //若过此时i的length小于等于24 不需要rouding
    if(i_length<=24)
    {
        //将除最高位的其他位移动到frac的最左边
        frac = (((unsigned)i)&(mask(f_length)))<<(23-f_length);
        //printf("%x\n",mask(10));
        //printf("%x\n",frac);
        exp = f_length+bias;
        //printf("%x\n",exp);
        return i_sign<<31|exp<<23|frac;
    }
    //此时i_length大于24位 float无法精确表示 要进行舍入
    else
    {
        //将前23位(除去最高位和符号位以外的前23位)进行保存
        //
        frac = (((unsigned)i)>>7)&0x7fffff;
       //因为E的大小和f_length长度一样 f_lengt为M=1.fn-1fn-2fn-3fn-4fn-5.. 中f的个数(M=1+f)
        exp = f_length+bias;
      //将frac和exp字段合并 方便后续进行rounding补偿 具体原因见下图
        unsigned frac_exp = exp<<23|frac;
        //将i的后7位保存
        unsigned last_seven_bits = i&0x7f;
        //如果后7位为1000000 则要查看24位中的最后一位
        if(last_seven_bits==0x40)
        {
            //如果最后一位为1,则要整体进1
            if((frac&0x1)==1)
            {
                frac_exp+=1;
            }
            //否则保持不变
        }
        //如果该字段大于0x1000000 则一定要整体进1
        else if(last_seven_bits>0x40)
        {
            frac_exp+=1;
        }
        //如果小于0x40则不进位
        else
        {
                    //不做任何处理
        }
        return i_sign<<31|frac_exp;
    }
}
posted @ 2023-01-08 21:21  付玬熙  阅读(2438)  评论(1编辑  收藏  举报