本机环境

$ uname -ap
Darwin Apple0.local 20.3.0 Darwin Kernel Version 20.3.0: Thu Jan 21 00:07:06 PST 2021; root:xnu-7195.81.3~1/RELEASE_X86_64 x86_64 i386

另外,配置原因,题目要求的在不同的机器上运行都只在一台机器上运行的。

知识点:

IEEE 浮点标准是 $V=(-1)^s\times M\times2^E$ 来表示一个数。

假设有 k 位阶码位,n 位尾数位。

Bias 对于规格化和非规格化的值都一样,都等于 $Bias=2^{(k-1)}-1$。

规格化的值:

E=e-Bias。

E 表示 2 的 E 次幂(可能为负数)。

e 为阶码字段 $e_{k-1}…e_1e_0$。

M=1+f。

f 为尾数字段 $\frac{f_{n-1}…f_1f_0} {2^n}$。

非规格化的值:

E=1-Bias。

M=f。

f 同规格化的 f。


2.55

/*这一段代码的大部分来自http://csapp.cs.cmu.edu/3e/students.html*/
/* $begin show-bytes */
#include <stdio.h>
/* $end show-bytes */
#include <stdlib.h>
#include <string.h>
/* $begin show-bytes */

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]); //line:data:show_bytes_printf
printf("\n");
}

void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int)); //line:data:show_bytes_amp1
}

void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float)); //line:data:show_bytes_amp2
}

void show_pointer(void *x) {
show_bytes((byte_pointer) &x, sizeof(void *)); //line:data:show_bytes_amp3
}
/* $end show-bytes */


/* $begin test-show-bytes */
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);
}
/* $end test-show-bytes */

void simple_show_a() {
/* $begin simple-show-a */
int val = 0x87654321;
byte_pointer valp = (byte_pointer) &val;
show_bytes(valp, 1); /* A. */
show_bytes(valp, 2); /* B. */
show_bytes(valp, 3); /* C. */
/* $end simple-show-a */
}

void simple_show_b() {
/* $begin simple-show-b */
int val = 0x12345678;
byte_pointer valp = (byte_pointer) &val;
show_bytes(valp, 1); /* A. */
show_bytes(valp, 2); /* B. */
show_bytes(valp, 3); /* C. */
/* $end simple-show-b */
}

void float_eg() {
int x = 3490593;
float f = (float) x;
printf("For x = %d\n", x);
show_int(x);
show_float(f);

x = 3510593;
f = (float) x;
printf("For x = %d\n", x);
show_int(x);
show_float(f);

}

void string_ueg() {
/* $begin show-ustring */
const char *s = "ABCDEF";
show_bytes((byte_pointer) s, strlen(s));
/* $end show-ustring */
}

void string_leg() {
/* $begin show-lstring */
const char *s = "abcdef";
show_bytes((byte_pointer) s, strlen(s));
/* $end show-lstring */
}

void show_twocomp()
{
/* $begin show-twocomp */
short x = 12345;
short mx = -x;

show_bytes((byte_pointer) &x, sizeof(short));
show_bytes((byte_pointer) &mx, sizeof(short));
/* $end show-twocomp */
}

int main(int argc, char *argv[])
{
int val = 12345;

if (argc > 1) {
if (argc > 1) {
val = strtol(argv[1], NULL, 0);
}
printf("calling test_show_bytes\n");
test_show_bytes(val);
} else {
printf("calling show_twocomp\n");
show_twocomp();
printf("Calling simple_show_a\n");
simple_show_a();
printf("Calling simple_show_b\n");
simple_show_b();
printf("Calling float_eg\n");
float_eg();
printf("Calling string_ueg\n");
string_ueg();
printf("Calling string_leg\n");
string_leg();
}
return 0;
}
打印输出为:
calling show_twocomp
39 30
c7 cf
Calling simple_show_a
21
21 43
21 43 65
Calling simple_show_b
78
78 56
78 56 34
Calling float_eg
For x = 3490593
21 43 35 00
84 0c 55 4a
For x = 3510593
41 91 35 00
04 45 56 4a
Calling string_ueg
41 42 43 44 45 46
Calling string_leg
61 62 63 64 65 66

2.56

更改数值为:192833445

编译运行输出为:

calling test_show_bytes
a5 67 7e 0b
7a e6 37 4d
38 f4 bf ef fe 7f 00 00

解释:

十进制:192833445 对应二进制表示为:1011011111100110011110100101,十六进制表示为:0xb7e67a5。

根据 IEEE 754 标准,单精度小数(float)的二进制位格式为:1 位符号位,8 位阶码位,23 位尾数位。

将二进制小数点向左移动 27 位(因为规格化表示要求所有的浮点数都表示为 1.XXX,而且前面的 1 不会实际存储,这样有多争取了 1 位来表示尾数),结果为 1.01101111110011001111010*0101*。因为尾数只有 23 位,因此将末尾的 0101 舍去(默认为向偶数舍去)结果为 1.01101111110011001111010,所以尾数的位表示为 01101111110011001111010

EXP=00011011(27 的二进制表示),Bias=127,因此 E=EXP+Bias=01001101,符号为的位表示为 00011011

综上:0,01001101,01101111110011001111010,十六进制表示位 0x4d37e67a

另外一个例子:

十进制:9978 二进制表示为:10011011111010,十六进制表示为:0x26fa

编译运行输出为:

calling test_show_bytes
fa 26 00 00
00 e8 1b 46
38 f4 bf ef fe 7f 00 00

将二进制位向左移动 13 位,结果为 1.0011011111010,又因为 IEEE 754 规定尾号位 23 位,所以在后边补 10 个 0,尾数的位表示为 00110111110100000000000

EXP=00001101(13 的二进制表示),Bias=127,因此 E=EXP+Bias=140,二进制位表示为 10001100,也就是符号位的二进制表示。

所以综上:0,10001100,00110111110100000000000。十六进制为 0x461be800


2.57

#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 n = 1234;
printf("short n = %hd\n", n);
show_bytes((byte_pointer)&n, sizeof(n));
}

void show_long() {
long n = 123456789;
printf("long n = %ld\n", n);
show_bytes((byte_pointer)&n, sizeof(n));
}

void show_double() {
double n = 123456789.0;
printf("long n = %lf\n", n);
show_bytes((byte_pointer)&n, sizeof(n));
}

int main(int argc, char const *argv[])
{
show_short();
show_long();
show_double();
return 0;
}

编译运行输出为:

short n = 1234
d2 04
long n = 123456789
15 cd 5b 07 00 00 00 00
long n = 123456789.000000
00 00 00 54 34 6f 9d 41

2.58

#include <stdio.h>

int is_little_endian() {
short n = 0x00ff;
unsigned char *p = (unsigned char *)&n;
if (*p) {
return 1;
}
return 0;
}

int main(int argc, char const *argv[])
{
printf("%d", is_little_endian());
return 0;
}

因为只有小端机器所以大端的没办法测试。

原理是:

十六进制的 short 0x00ff,二进制形式为:0000 0000 1111 1111。采用小端存储为 1111 1111 0000 0000,强制类型转换为 unsigned char *,因此 p 指向第一位,也就是第一个 1,因此 *p 是一个非零值。


2.59

#include <stdio.h>

void fun_2_59() {
int x = 0x89ABCDEF;
int y = 0x76543210;
printf("%x\n", (x & 0xff) | (y & 0xffffff00)); // 第二的括号还可以写成 y & ~0xff,~0xff = 0xffffff00
}

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

2.60

#include <stdio.h>

unsigned replace_byte(unsigned x, int i, unsigned char b) {
int move = i * 8;
return (x & ~(0xFF << move)) | b << move;
}

int main(int argc, char const *argv[])
{
printf("%#.8x\n", replace_byte(0x12345678, 0, 0xAB));
printf("%#.8x\n", replace_byte(0x12345678, 2, 0xAB));
return 0;
}

编译运行结果为:

0x123456ab
0x12ab5678

2.61

#include <stdio.h>

void func(int x) {
size_t sizeOfInt = sizeof(int); // l = 4
printf("变量 int x 为:%#.8x\n", x);

/* Condition A */
printf("X 的所有位都等于 1 么?%d\n", !(~x));

/* Condition B */
printf("X 的所有位都等于 0 么?%d\n", !(1 && x));

/* Condition C */
int shift_val = (shift_val - 1) << 3;
printf("X 的最低有效字节中的位都等于 1 么?%d\n", !((~(x & 0xFF)) << shift_val));

/* Condition D */
shift_val = (sizeOfInt - 1) << 3;
printf("X 的最高有效字节中的位都等于 0 么?%d\n", !(x >> shift_val));
printf("###############################\n");
}

int main(int argc, char const *argv[])
{
func(0x00FFFFFF);
func(0);
func(~0);
func(0x010FFFFF0);
return 0;
}

编译运行结果为:

变量 int x 为:0x00ffffff
X 的所有位都等于 1 么?0
X 的所有位都等于 0 么?0
X 的最低有效字节中的位都等于 1 么?1
X 的最高有效字节中的位都等于 0 么?1
###############################
变量 int x 为:00000000
X 的所有位都等于 1 么?0
X 的所有位都等于 0 么?1
X 的最低有效字节中的位都等于 1 么?0
X 的最高有效字节中的位都等于 0 么?1
###############################
变量 int x 为:0xffffffff
X 的所有位都等于 1 么?1
X 的所有位都等于 0 么?0
X 的最低有效字节中的位都等于 1 么?1
X 的最高有效字节中的位都等于 0 么?0
###############################
变量 int x 为:0x10fffff0
X 的所有位都等于 1 么?0
X 的所有位都等于 0 么?0
X 的最低有效字节中的位都等于 1 么?0
X 的最高有效字节中的位都等于 0 么?0
###############################

2.62

int int_shifts_are_arithmetic() {
int x = -1;
return (x >> 1) == -1;
}

int main(int argc, char const *argv[])
{
int x = int_shifts_are_arithmetic();
printf("%d\n", x);
return 0;
}

2.63

unsigned srl(unsigned x, int k) {
unsigned xsra = (int)x >> k;
size_t w = 8 * sizeof(int);
unsigned int a = ~0;
a = a << (w - k);
return xsra & ~a;
}

int sra(int x, int k) {
int xsrl = (unsigned) x >> k;
size_t w = 8 * sizeof(int);
unsigned int a = ~0;
a = a << (w - k);
return xsrl | a;
}

int main(int argc, char const *argv[])
{
unsigned x = 0b10000000000000000000000000000000;
printf("%u\n", srl(x, 8));
printf("%u\n", 0b00000000100000000000000000000000);
printf("%d\n", sra(x, 8));
printf("%d\n", 0b11111111100000000000000000000000);
return 0;
}

输出:

8388608
8388608
-8388608
-8388608

解释:

为了简便用 w=8 位来举例子。

算数右移完成逻辑右移

假设二进制 10010110k = 3。则想要的结果是 000 10010。

然而进行算数右移得到的是 n = *111*10010。为了得到 00010010,我们可以想:如果 n00011111 进行 & 运算,则刚好是我们想要的结果 00010010。

那我们怎样可以产生我们想要的 00011111 呢?(注意题目不让用右移和除法)

如果将 11111111 左移 w-k 位是不是就有了 11100000 了么,再取反是不是就有了 00011111。

所以答案就出来了。

逻辑右移完成算数右移

逻辑右移完成算数右移只不过是将 & 运算改为 | 运算,而且 11111111 左移 w-k 位后也不用取反。


2.64

int any_odd_one(unsigned x) {
unsigned n = 0x55555555;
return (x & n) && 1;
}

int main(int argc, char const *argv[])
{
unsigned x = 0b00010000000000000000000000000000;
printf("%d\n", any_odd_one(x));
x = 0b00100000000000000000000000000000;
printf("%d\n", any_odd_one(x));
return 0;
}

不要搞错奇偶位,是从右往左数,不是从左往右数。

打印输出为:

1
0

提示:0xAAAAAAAA 的二进制为 10101010101010101010101010101010,特点是:齐位都为 1。


2.65


2.66


2.67

A.

(C11, 6.5.7p3) “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 is undefined”

B.

int int_size_is_32() {
int set_msb = 1 << 31;
int beyond_msb = set_msb;
beyond_msb <<= 1;
return set_msb && !beyond_msb;
}

int main(int argc, char const *argv[])
{
printf("%d\n", int_size_is_32());
return 0;
}

打印输出为:

1

见招拆招,32 位不让移动,我就分两步走,先走 31 位, 再走 1 位。

C.

int int_size_is_32() {
int set_msb = 1 << 15;
set_msb <<= 15;
set_msb <<= 1;
int beyond_msb = set_msb;
beyond_msb <<= 1;
return set_msb && !beyond_msb;
}

int main(int argc, char const *argv[])
{
printf("%d\n", int_size_is_32());
return 0;
}

打印输出为:

1

在 16 位的机器上每步最多可以移动 15 位。将 31 和 32 拆成多步,并且每步最多走 15 位。


2.68

int lower_one_mask(int n) {
unsigned a = ~0;
size_t w = sizeof(int) << 3;
return a >> (w - n);

// return (2<<(n-1)) - 1; // 这个方法更牛逼
}

int main(int argc, char const *argv[])
{
printf("%#.8x\n", lower_one_mask(17));
printf("%#.8x\n", lower_one_mask(32));
return 0;
}

打印输出为:

0x0001ffff
0xffffffff

2.69

unsigned rotate_left(unsigned x, int n) {
size_t w = sizeof(unsigned int) << 3;
unsigned a = x << n;
unsigned b = x >> (w - n - 1);
b = b >> 1;
return a + b;
}

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

printf("n = 0: %#.8x\n", rotate_left(0x12345678, 0));
printf("n = 4: %#.8x\n", rotate_left(0x12345678, 4));
printf("n = 20: %#.8x\n", rotate_left(0x12345678, 20));
printf("n = 31: %#.8x\n", rotate_left(0x12345678, 31));
return 0;
}

打印输出为:

n = 0: 0x12345678
n = 4: 0x23456781
n = 20: 0x67812345
n = 31: 0x091a2b3c

注意:当 n=0 的时候,按照我的方法 b 要右移 32 位,但是这会产生警告:"Shift count >= width of type",详见 2.67。所以我将右移拆成了两步,先右移 (w - n - 1) 位,再右移 1 位。


2.70

int fits_bits(int x, int n) {
x >>= (n - 1);
return !x || !(~x);
}

int main(int argc, char const *argv[])
{
printf("%d\n", fits_bits(INT_MAX, 32));
printf("%d\n", fits_bits(INT_MIN, 32));
printf("%d\n", fits_bits(INT_MIN, 31));
printf("%d\n", fits_bits(INT_MAX, 31));
return 0;
}

打印输出为:

1
1
0
0

解释:

一个数的补码的符号位只有两种情况,要么 0,要么 1。如果一个数字 x 可以被表示为 n 位的补码,那么 x 的补码的第 n-1 位,也就是符号位要么是 0,要么是 1。如果第 n-1 位是 1,那么前面的 w-n 位也是 1,如果第 n-1 位是 0,那么前面的 w-n 位也是 0

因此,通过上述分析,判断一个数字 x 能否被表示为 n 位的补码数,就转变成了,看 x 的高 w-n+1 位是否全 0,或者全 1


2.71

说实话这题的题目我读了半小时才读懂。其实题目挺简单的,就是让你把无符号的 32 位的数 word,给拆成 4 个有符号数(每个大小为 1 byte)。

typedef unsigned packed_t;

int xbyte(packed_t word, int bytenum) {
int left_move = (3 - bytenum) << 3;
int right_move = 3 << 3;
return (int)word << left_move >> right_move ;
}

int main(int argc, char const *argv[])
{
packed_t word = 0x8060FF00;
// 10000000 01100000 11111111 00000000
printf("%d\n", xbyte(word, 0));
printf("%d\n", xbyte(word, 1));
printf("%d\n", xbyte(word, 2));
printf("%d\n", xbyte(word, 3));
return 0;
}

打印输出为:

0
-1
96
-128

2.72

A.

因为 sizeof 的返回值是 size_t 类型的,是无符号的。所以 if 条件 maxbytes-sizeof(val) >= 0 总是会成立。

B.

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

2.73

连同 2.74 一块做个溢出情况总结。

补码加法:

假设 t=a+b

如果 a、b 同号,

a>0,b>0,如果 t<=0 则正溢出。

a<0,b<0,如果 t>=0 则负溢出。

如果 a、b 异号,肯定不会溢出。

补码减法:

假设 t=a-b

如果 a、b 同号,则肯定不会溢出。

如果 a、b 异号,

a>=0,b<0,如果 t<0 则溢出。

a<0,b>=0,如果 t>=0 则溢出。

因此,a、b 异号,t、b 同号,则判定为补码减法溢出。


2.74

int tsub_ok(int x, int y) {
size_t w = sizeof(int) << 3;
int l, m;
int t = x - y;
l = (x >> (w - 1)) - (y >> (w - 1)); // x、y 同号 l 为 0
m = (t >> (w - 1)) - (y >> (w - 1)); // t、y 同号 l 为 0
return !l || !(l && (!m)); // || 两边两个条件分别为:同号不会溢出;a、b 异号,t、b 同号则溢出。具体参见 2.73 的溢出情况整理。
}

int main(int argc, char const *argv[])
{
printf("%d\n", tsub_ok(123456, 54321));
printf("%d\n", tsub_ok(2147483647, -1));
printf("%d\n", tsub_ok(-2147483648, 1));
return 0;
}

打印输出为:

1
0
0

后来突然发现,该题目并没有限制 ==!= 的使用,所以重新写一下该方法。

int tsub_ok(int x, int y) {
size_t w = sizeof(int) << 3;
int t = x - y;
// 取符号位
x >>= (w - 1);
y >>= (w - 1);
t >>= (w - 1);

return (x == y) || !((x != y) && (y == t));
}

2.75


2.76


2.77

A.

(x<<4)+x。

B.

x-(x<<3)。

C.

(x<<6)-(x<<2)。

D.

(x<<4)-(x<<7)。


2.78

什么叫「正确的舍入方式」?

int divide_power2(int x, int k) {
size_t w = sizeof(int) << 3;
int bias = (x >> (w - 1)) & (1 << k) - 1;
return (x + bias) >> k;
}

int main(int argc, char const *argv[]) {
printf("%d\n", divide_power2(1024, 2));
printf("%d\n", divide_power2(5, 2));
printf("%d\n", divide_power2(-1024, 2));
printf("%d\n", divide_power2(-5, 2));
return 0;
}

打印输出:

256
1
-256
-1

解释:

首先我们要知道,补码除以 2 的幂,是要分补码是正数、负数两种情况的。

对于正数,我们可以直接用右移操作来得出结果。

对于负数,我们就要先加上偏置(bias)再进行右移。

所以综上,除以 2 的 k 次幂可以表示为 (x<0 ? x+(1<<k)-1 : x) >> k,其中 $bias=2^k-1=(1<<k)-1$。

但是对于该题,我们不可以用 :? 来解答。那么我们该怎么根据 x 的正、负来计算 bias 呢?(可以参考下练习题2.42)。

我们也可以认为不论正、负,其实都加了 bias。只不过,正数,bias = 0;负数,bias = (1<<k)-1

我们首先将 x 算数右移 w-1 位,如果 x 是负数,则会得到 0xFFFFFFFF,如果 x 是正数,则会得到 0x00000000

然后,我们就可以通过掩码来屏蔽掉适当的位来求得 bias。那么掩码该怎么确定呢?其实掩码就是 bias 本身。因为对于正数,0x00000000 & bias 还是等于 0,对于负数 0xFFFFFFFF & bias 就会得到 (1<<k)-1。


2.79


2.80


2.81


2.82

A.

false。当 x=TMin,y=0 时。

B.

true。

C.

true。

左边等于:~x+~y+1 = (~x+1)+(~y+1) -1 = -x + -y -1。

右边等于:~(x+y) = (~(x+y)+1)-1 = -(x+y)-1 = -x + -y + 1。

PS:求补码的相反数:取反再加一。

D.

E.

true。右移 2 位,再左移 2 位,这只会改变 32 位中最低的 2 位,结果就是把低 2 位变为 00。对于正数和负数来说,这会小于原始值,对于 0 来说,这不会改变值。


2.83


2.84

假设 k<=n,如果。

$ Bias=2^{k-1}-1 $

A. 数 7.0

数 7.0 转为规格化表示为 $0.7 \times $

位表示:0, 0...1, 0...111

E=e-Bias=1-BIas。

$f=\frac {7}{2^n}$。

M=1+f。

$V=(-1)^s \times M \times 2^E$

B. 能够被准确描述的最大基数

位表示:0, 11...10, 11...11

$E=e-Bias=2^{k}-2-Bias$。

$f=\frac {2^{n-1}}{2^n}$。

M=1+f。

$V=(-1)^s \times M \times 2^E$

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

位表示:1,

$E=e-Bias$。

f=

M=1+f

$V=(-1)^s \times M \times 2^E$。


2.85

A.

7 的位表示为 111,那么位数 M 的表示就应该是 1.11(IEEE 标准中默认 M 前面隐藏了 1),所以小数位就是 0.25()

B.

C.


2.86

1 个符号位,15 个阶码位,63 个小数位。$ Bias=2^{14}-1 $。

最小的正非规格化数: 0,00...00,00...01

$ E=1-Bias=1-(2^{14}-1)=2-2^{14} $。$ M=f= \frac {1}{2^{63}}=2^{-63} $。

值:$ M \times 2^E=2^{-63} \times 2^{2-2^{14}} $。

最小的正规格化数: 0,00...01,00...00

$ E=e-Bias=1-(2^{14}-1)=2-2^{14} $。$ M=1+f=1+ \frac {0} {2^{63}}=1$。

值:$ M \times 2^E = 2^{2-2^{14}}$。

最大的规格化数: 0,11...10,11...11

$ E=e-Bias=2^{15}-2-(2^{14}-1)=2^{14}-1 $。$ M=1+f=1+2-2^{-63}$。

值:$ M \times 2^{E}=(1+2-2^{-63}) \times 2^{2^{14}-1} $。