2.57

编写程序 show_shortshow_longshow_double,它们分别打印类型为 shortlongdouble 的字节表示。

char 是字符型,但也是属于整形的,因为 char 最终存储的是 ASCLL 码值到内存的。一个 char 有 8 个位,所以能存储的范围为 $-128\sim 127$ ,而 unsigned char 存储的范围为 $0\sim 255$ 。因为它占用一个字节,所以 unsigned char 可以精确地表示一个字节的所有可能值,所以它通常用于存储和处理字节数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>

typedef unsigned char *byte_pointer;

// 大端
void show_byte(byte_pointer start, size_t sz) {
// 展示十六进制
for(int i = 0; i < sz; i ++) {
printf("%.2x", start[i]);
}
printf("\n");
// 展示二进制(大端)
for(int i = 0; i < sz; i ++) {
for(int j = 7; j >= 0; j --) {
printf("%d", (start[i] >> j) & 1);
if(j % 4 == 0) printf(" ");
}
}
printf("\n");
}

void show_short(short num) {
printf("short: \n");
show_byte((byte_pointer)&num, sizeof(short));
}

void show_long(long num) {
printf("long: \n");
show_byte((byte_pointer)&num, sizeof(long));
}

void show_float(float num) {
printf("float: \n");
show_byte((byte_pointer)&num, sizeof(float));
}

void show_double(double num) {
printf("double: \n");
show_byte((byte_pointer)&num, sizeof(double));
}

int main() {
int num = 365;
show_short((short)num);
show_long((long)num);
show_float((float)num);
show_double((double)num);
return 0;
}

2.58

编写过程 is_little_endian ,当在小端法机器上编译和运行时返回1,在大端法机器上编译运行时则返回0。这个程序应该可以运行在任何机器上,无论机器的字长是多少。

计算机存储数据在地址上都是由低到高,大小端的区别为小端法存储数据由低位到高位,而大端法则是由高位到低位,例如数据 0x12345678 小端法存储就是 78 56 34 12,而大端法存储则是 12 34 56 78
可以使用 union 来实现,在 union 中存放两个数据,一个 int 一个 charchar 占的地址为 int 数据的第一个字节,修改 int 值为1,这样就只需要判断 char 数据是否为 1 即可。

1
2
3
4
5
6
7
8
int is_little_endian() {
union {
int i;
char c;
} test;
test.i = 1;
return (int)test.c;
}

2. 59

编写一个 C 表达式,它生成一个字,由 $x$ 的最低有效字节和 $y$ 中剩下的字节组成。对于运算数 x = 0x89ABCDEFy = 0x76543210 得到 0x765432EF

  • 位(bit)是计算机内部数据存储的最小单位,只能取0或者取1;
  • 字节(byte)是计算机数据处理的最小单位,每个字节有8个二进制位,其中最右边的一位为最低位,最左边的一位为最高位;
  • 字是计算机进行数据处理和运算的单位,即cpu一次处理二进制代码的位数,字的位数叫做字长,字长的大小与计算架构有关,通常说的32位机就是一个字有4个字节,32个位。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef unsigned char *byte_pointer;
...
size_t x = 0x89ABCDEF;
size_t y = 0x76543210;
size_t mask = 0xff;
size_t res = ((x & mask) | (y & ~mask));
byte_pointer start = (byte_pointer)& res;
for(int i = 0; i < sizeof(size_t); i ++) {
printf("%.2x", start[i]);
}
/*
输出: ef32547600000000 (小端法机器)
*/

2.60

假设我们将一个 $w$ 位的字中的字节从 0(最低位)到 $\frac{w}{8} - 1$(最高位)编号。写出下面C函数代码,它会返回一个无符号值,其中参数 $x$ 的字节 $i$ 被替换成字节 $b$ :
unsigned replace_byte (unsigned x, inti, unsigned char b);
以下示例说明其如何工作:
replace_byte(Ox12345678, 2, OxAB) --> Ox12AB5678
replace_byte(Ox12345678, 0, OxAB) --> Ox123456AB

按要求模拟就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

typedef unsigned char *byte_pointer;

/*
* @brief 参数 x 的字节 i 被替换成字节 b
*/
unsigned replace_byte (unsigned x, int i, unsigned char b) {
byte_pointer start = (byte_pointer)& x;
start[i] = b;
return *(unsigned*)start;
}

void show_byte(byte_pointer start, size_t sz) {
for(int i = 0; i < sz; i ++) {
printf("%.2x", start[i]);
}
printf("\n");
}

int main() {
unsigned res1 = replace_byte(0x12345678, 2, 0xAB);
unsigned res2 = replace_byte(0x12345678, 0, 0xAB);
show_byte((byte_pointer)&res1, sizeof(unsigned));
show_byte((byte_pointer)&res2, sizeof(unsigned));
return 0;
}
/*
output:
7856ab12
ab563412
*/

这里提一下最后为什么返回 *(unsigned*)start ,这里先要将 unsigned char * 类型的 start 转变为 unsigned* ,即先转换指针类型,再去指针指向地址的值,而如果使用 (unsigned)(*start) 的话,*start 由于是 unsigned char 类型的指针,所以只会取出第一个地址的值,而不是所有的内存单元。

在接下来的作业中,我们特意限制了你能使用的编程结构,来帮你更好地理解 C 语言的位级、逻辑
和算术运算。在回答这些问题时,你的代码必须遵守以下规则:

  • 假设:
    • 整数用补码形式表示;
    • 有符号数的右移是算术右移;
    • 数据类型 int 是 $w$ 位长的。对于某些题目,会给定 $w$ 的值,但是在其他情况下,只要 $2$ 是 8 的整数倍,你的代码就应该能工作。你可以用表达式 sizeof(int)<<3 来计算 $w$。
  • 禁止使用
    • 条件语句、循环、分支语句、函数调用和宏调用;
    • 除法、模运算和乘法;
    • 相对比较运算($>$ 、$<$ 、$\le$ 和 $\ge$)。
  • 允许的运算
    • 所有的位级和逻辑运算;
    • 左移和右移,但是位移量只能在 $0$ 到 $w-1$ 之间;
    • 加法和减法;
    • 相等(==)和不等(!=)测试。(有些题目里也不允许这些运算);
    • 整型常数 INT_MAXINT_MIN;
    • intunsigned 进行强制类型转换,无论是显式的还是隐式的。

2.61

写一个C表达式,在下列描述的条件下产生1, 而在其他情况下得到 0。假设 $x$ 是 int 类型。
A. $x$ 的任何位都等于 1
B. $x$ 的任何位都等于 0
C. $x$ 的最低有效字节中的位都等于1
D. $x$ 的最高有效字节中的位都等于0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <assert.h>

typedef unsigned char *byte_pointer;

void show_byte(int x) {
unsigned mask = 1u << 31;
for(int cnt = 1; mask; mask >>= 1, cnt ++) {
printf("%d", x & mask ? 1: 0);
if(cnt % 4 == 0) printf(" ");
}
printf("\n");
}

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

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

int C(int x) {
return !~(x | ~0xFF);
}

int D(int x) {
return !(x >> ((sizeof(int) - 1) << 3));
}

int main() {
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));

assert(!A(0x1234FF));
assert(!B(0x1234FF));
assert(C(0x1234FF));
assert(D(0x1234FF));

assert(!A(0x1234));
assert(!B(0x1234));
assert(!C(0x1234));
assert(D(0x1234));
return 0;
}

解释:

  1. ~ 是对每一位取反
  2. ! 是对值取反

而 C 部分,首先是和 ~0xFF (亦即 0xFFFFFF00) 做或运算,前 3 个字节都为 1,最后一个字节为 $x$ 本来的最低字节,要判断是否为全 1 只需要全部取反再判断是否为0即可

D 部分,(x >> ((sizeof(int) - 1) << 3)) 会得到最高位字节右移后的结果,例如0011 1011 1001 1010 1100 1010 0000 0000 会变为 0000 0000 0000 0000 0000 0000 0011 1011 ,最后判断即可。

2. 62

编写一个函数 int_shifts_are_arithmetic() 在对 int 类型的数使用算术右移的机器上运行时这个函数生成 1, 而其他情况下生成 0 。你的代码应该可以运行在任何字长的机器上。在几种机器上测试你的代码。

1
2
3
4
int int_shifts_are_arithmetic() {
int x = -1;
return !(x ^ (x >> 1));
}

后面的有时间慢慢做…