# 2026西邮linux兴趣小组一面试题题解

## 0. 树懒闪电的二进制车牌

注意到按钮数字是斐波那契数列

写出二进制对照表

| 十进制 | 二进制      |
| -----: | ----------- |
|      1 | 1           |
|      2 | 10          |
|      3 | 11          |
|      5 | 101         |
|      8 | 1000        |
|     13 | 1101        |
|     21 | 10101       |
|     34 | 100010      |
|     55 | 110111      |
|     89 | 1011001     |
|    144 | 10010000    |
|    233 | 11101001    |
|    377 | 101111001   |
|    610 | 1001100010  |
|    987 | 1111011011  |
|   1597 | 11000111101 |
|    ... | ...         |

离屏幕显示的`11010100101`最接近的数是`11000111101(1597)`,相减后得到`1101000`

离`1101000`最近的是`1011001(89)`,相减后得到`1111`

继续这样计算有

`11010100101 = 11000111101(1597) + 1011001(89) + 1101(13) + 10(2)`

有题目条件:不能按相邻的两个按钮可以得到这是唯一解

应该按`2`,`13`,`89`,`1597`,四个按钮,结果的十进制是`1701`

## 1. 一句印不完的欢迎词

```c
int main()
{
    while (1) 
    {
        printf("Hi! ");
        if (!printf("The 202%d, Welcome to Xiyou Linux Group!\n",
        printf("guys! ")))
        {
            break;
        }
    }
}
```

光看`while(1)`肯定是死循环,但是内部循环里有一个`break`,我们看看`break`的执行条件`!printf("The 202%d, Welcome to Xiyou Linux Group!\n",printf("guys! "))`,如果`printf`的返回值为`0`(即一个字符都没输出)就会执行`break`

那我们就看看里面的函数,首先在外层`printf`里有一个`%d`,它的参数是`printf("guys! ")`的值,而`printf`的返回值就是输出的字符数,所以`%d`自然就是`6`

此外内层一定先输出,因为外层`printf`必须先把所有实参求值完,才能开始打印,所以先输出`guys! `,再输出`The 2026, Welcome to Xiyou Linux Group!`

外层`printf`打印`The 2026, Welcome to Xiyou Linux Group!\n`共`40`个字符,返回`40`,而`printf`只有在出错时才返回负数,取`!`两种情况都是`0`,所以`break`永远不会被执行,导致死循环

实际运行效果就是无限重复同一行:

```
Hi! guys! The 2026, Welcome to Xiyou Linux Group!
```

## 2. 失忆的交换生

```c
void swap_val(int x, int y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
void swap_ptr(int *x, int *y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}
int main()
{
    int a = 10, b = 20;

    swap_val(a, b);
    printf("swap_val: a=%d, b=%d\n", a, b);

    swap_ptr(&a, &b);
    printf("swap_ptr: a=%d, b=%d\n", a, b);

    return 0;
}
```

题目问为什么一个能交换一个不能交换

我们先看`swap_val`(按值传递)的那一个,在函数调用时,实参`a`,`b`将值传递给了形参`x`,`y`,在函数内对`x`,`y`进行操作,实际上对`a`,`b`没有任何影响,所以反映在输出上就是`a`,`b`没有像预期一样交换

再看`swap_ptr`(传地址),实参传递过去的是`a`,`b`的地址,于是形参`x`,`y`就能通过地址更改`a`,`b`的值,所以可以正常交换`a`,`b`

C语言只有按值传递,函数拿不到调用者变量本身,只能拿到副本,如果我们想改变外部变量,应该传递他的地址,用指针进行更改

输出:

```
swap_val: a=10, b=20
swap_ptr: a=20, b=10
```

## 3. sizeof 的视力表

```c
void inspect(char text[], int (*matrix)[4])
{
    printf("%zu %zu %zu %zu\n",
           sizeof(text), sizeof(matrix),
           sizeof(*matrix), strlen(text));
}
int main()
{
    char a[] = "Linux\0Group";
    char *p = a;
    int b[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
    printf("%zu %zu %zu\n",
           sizeof(a), sizeof(p), sizeof(a + 0));
    printf("%d %d\n", a[5] == '\0', strcmp(a, "Linux"));
    inspect(a, b);
}
```

第一个`printf`:

1. `sizeof(a)`得到的是整个`char`数组的大小:`a`的字面量是`Linux\0Group`,显式写了 11 个字符(`Linux` 5 个 + `\0` + `Group` 5 个),再加编译器自动补上的结尾`'\0'`,所以是`12`字节
2. `p`是一个指向`a`的指针,64 位平台上指针是`8`字节,所以`sizeof(p)` = 8
3. `a + 0`是表达式不是数组:`a`在这里会退化成`char *`,所以`a + 0`的类型就是`char *`;而`sizeof`只看类型、不求值,于是得到`8`

第二个`printf`:

1. `a[5]`正好是`Linux`后面那个`'\0'`,所以`a[5] == '\0'`得到`1`
2. `strcmp(a, "Linux")`逐字符比较,前 5 个字符相同,第 6 个又都是`'\0'`,两个字符串完全相等,所以返回`0`

最后的`inspect`:

1. 形参`char text[]`等价于`char *text`,所以`sizeof(text)` = 8
2. 形参`int (*matrix)[4]`是"指向`int[4]`的指针",所以`sizeof(matrix)` = 8
3. `*matrix`的类型是`int[4]`,所以`sizeof(*matrix)` = `4 * 4` = 16
4. `strlen(text)`从`text`开始数到第一个`'\0'`为止,也就是`Linux`的长度,得到 5

输出:

```
12 8 8
1 0
8 8 16 5
```

## 4. XOR 密钥：藏在字节里的悄悄话

```c
int main()
{
    int nums[] = {167, 150, 134, 144, 138, 179, 150, 145,138, 135, 184, 141, 144, 138, 143};
    int size = sizeof(nums) / sizeof(nums[0]);
    for (int i = 0; i < size; i++)
    {
        int mask = 0;
        int bitval = 1;
        int bits = 8;
        while (bits-- > 0)
        {
            mask |= bitval;
            bitval <<= 1;
        }
        printf("%c", nums[i] ^ mask);
        if (i == size - 1)
        {
            printf("\n");
        }
    }
}
```

我们以一次循环为例

`i = 0`, `mask = 0`, `bitval = 1`, `bits = 8`

进入`while`循环后

1. `mask = 1(00000001)`, `bitval = 2(00000010)`
2. `mask = 3(00000011)`, `bitval = 4(00000100)`
3. `mask = 7(00000111)`, `bitval = 8(00001000)`
4. ...

根据规律可见循环结束后mask = 255(11111111)

|       nums[i] | nums[i] ^ mask | 对应字符 |
| ------------: | -------------- | -------- |
| 167(10100111) | 88(01011000)   | `X`    |
| 150(10010110) | 105(01101001)  | `i`    |
| 134(10000110) | 121(01111001)  | `y`    |
| 144(10010000) | 111(01101111)  | `o`    |
| 138(10001010) | 117(01110101)  | `u`    |
| 179(10110011) | 76(01001100)   | `L`    |
| 150(10010110) | 105(01101001)  | `i`    |
| 145(10010001) | 110(01101110)  | `n`    |
| 138(10001010) | 117(01110101)  | `u`    |
| 135(10000111) | 120(01111000)  | `x`    |
| 184(10111000) | 71(01000111)   | `G`    |
| 141(10001101) | 114(01110010)  | `r`    |
| 144(10010000) | 111(01101111)  | `o`    |
| 138(10001010) | 117(01110101)  | `u`    |
| 143(10001111) | 112(01110000)  | `p`    |

任何数和全`1`异或等于按位取反,所以数组里存的本来就是每个字符`ASCII`码的反码

把这些二进制按 ASCII 换算成字符,依次是`X` `i` `y` `o` `u` `L` `i` `n` `u` `x` `G` `r` `o` `u` `p`,拼起来正是`XiyouLinuxGroup`

## 5. 宏召唤术：括号去哪儿了

```c
#define SQUARE(x) x * x
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{
    int i = 3;
    printf("%d\n", SQUARE(i + 1));
    printf("%d\n", MAX(i, 5));
    printf("%d\n", MAX(i++, 5));
    printf("%d\n", i);
}
```

我们按顺序一个个来

首先第一个`printf`里调用了`SQUARE`宏,将`i + 1`替换成了`i + 1 * i + 1`(因为`x`没加括号所以不是`(i + 1)^2`),带入`i`后得到`7`

第二个把宏替换后得到`i > 5 ? i : 5`,作用就是求较大值,这里`i = 3 < 5`,所以结果是`5`

第三个和第二个差不多,`i++`是先进行判断再自增,所以值还是`3`,结果依旧是更大者`5`

第四个输出`i`,此时`i`自增过,所以结果是`4`

输出结果`7 5 5 4`

> 值得注意的是,如果对SQUARE传入i++会自增两次,而且是一个未定义行为
> 执行MAX(i++,5)时如果i已经大于5自增同样会执行两次

## 6. p 与 q 的步幅之争

```c
int main()
{
    int a[4] = {1, 2, 3, 4};
    int *p = a;
    int (*q)[4] = &a;
    printf("%td %td\n",
           (char *)(p + 1) - (char *)p,
           (char *)(q + 1) - (char *)q);
    printf("%d %d\n", *(p + 2), *(*q + 2));
}
```

这段代码里`p`是指向`a`中第一个元素的指针,`q`是指向整个数组`a`的指针,两者存的是同一个地址,但类型不同,所以加同样的数字走出来的距离也不同

| 变量  | 声明                  | 类型           | `+1`前进的字节数 |
| ----- | --------------------- | -------------- | -----------------: |
| `p` | `int *p = a;`       | `int *`      |              `4` |
| `q` | `int (*q)[4] = &a;` | `int (*)[4]` |             `16` |

`a`在表达式里会退化成`int *`,所以`p`是"指向`int`的指针";而`&a`的类型是`int (*)[4]`,所以`q`是"指向`int[4]`的指针",也就是指向整个数组

指针的加减法是以它所指类型的大小为单位的,所以`p + 1`跳过`1`个`int`(4 字节),`q + 1`跳过`1`个`int[4]`(16 字节)

第一个`printf`里把两个指针都强转成`char *`再相减,`char`正好是`1`字节,所以相减得到的就是字节数

1. `(char *)(p + 1) - (char *)p`,`p`前进了 1 个`int`,得到`4`
2. `(char *)(q + 1) - (char *)q`,`q`前进了 1 个`int[4]`,得到`16`

第二个`printf`里两种写法最终都指向同一个元素

1. `p + 2`指向`a[2]`,解引用得到`a[2] = 3`
2. `*q`的类型是`int[4]`,在表达式里同样会退化成`int *`,所以`*q + 2`也指向`a[2]`,解引用得到`3`

输出:

```
4 16
3 3
```

## 7. 记性特别好的 x

```c
int visit(int n)
{
    static int x = 0;
    if (n == 0)
        return x;
    x += n;
    printf("before %d %d\n", n, x);
    int result = visit(--n);
    printf("after %d %d %d\n", n, x, result);
    return x + result;
}
int main()
{
    printf("first = %d\n", visit(3));
    printf("second = %d\n", visit(2));
}
```

这道题的关键就在于`static`这个关键词

`static int x = 0`只在程序第一次执行到这一行时初始化一次,之后所有递归到`main`里的第二次调用,用的都是同一个`x`,它的值会一直保留下来

先看第一次调用`visit(3)`

1. 进入函数时`n = 3`,`x`还是`0`
2. `n != 0`,先执行`x += n`,再`printf`输出`before 3 3`
3. 接着执行`int result = visit(--n);`,这里的`--n`先把本层的`n`改成`2`,再把`2`传进去

进入`visit(2)`这一层

1. `x += 2`,`x`变成`5`,输出`before 2 5`
2. `--n`把本层`n`改成`1`,传`1`给下一层

进入`visit(1)`这一层

1. `x += 1`,`x`变成`6`,输出`before 1 6`
2. `--n`把本层`n`改成`0`,传`0`给下一层

进入`visit(0)`这一层

1. `n == 0`,直接`return x`,也就是返回`6`

之后开始一层层"归",这里有个容易忽略的点:

`after`里输出的`n`已经被上面那句`--n`改小了,所以比`before`里的`n`小 1

1. 回到`visit(1)`,此时它自己的`n`是`0`,`result`是`6`,输出`after 0 6 6`,返回`x + result = 6 + 6 = 12`
2. 回到`visit(2)`,此时它自己的`n`是`1`,`result`是`12`,输出`after 1 6 12`,返回`6 + 12 = 18`
3. 回到`visit(3)`,此时它自己的`n`是`2`,`result`是`18`,输出`after 2 6 18`,返回`6 + 18 = 24`

这个`24`作为`printf`的参数,最后输出`first = 24`

第一次调用结束时`x`仍然是`6`,所以第二次调用`visit(2)`是在`x = 6`的基础上继续累加

1. `x += 2`,`x`变成`8`,输出`before 2 8`
2. 进入`visit(1)`,`x += 1`,`x`变成`9`,输出`before 1 9`
3. 进入`visit(0)`,直接`return 9`
4. 回到`visit(1)`,输出`after 0 9 9`,返回`9 + 9 = 18`
5. 回到`visit(2)`,输出`after 1 9 18`,返回`9 + 18 = 27`
6. `27`作为`printf`的参数,输出`second = 27`

输出:

```
before 3 3
before 2 5
before 1 6
after 0 6 6
after 1 6 12
after 2 6 18
first = 24
before 2 8
before 1 9
after 0 9 9
after 1 9 18
second = 27
```

## 8. 谁能动 const 大神的奶酪

```c
struct P
{
    int x;
    const int y;
};
int main()
{
    struct P p1 = {11, 22}, p2 = {33, 44};
    const struct P p3 = {55, 66};
    struct P *const ptr1 = &p1;
    const struct P *ptr2 = &p2;
    const struct P *const ptr3 = &p3;
}
```

首先要理解一个知识点:`const`和`*`的相对位置决定了到底哪个不能动

1. `const`在`*`的右边:`int *const p`,这时`p`是"指针常量",指针本身的值(存的地址)不能改,但它指向的内容可以改
2. `const`在`*`的左边:`const int *p`(等价于`int const *p`),这时`p`是"常量指针",它指向的内容不能改,但指针本身可以重新指向别处
3. 两边都有:`const int *const p`,指针本身和所指内容都不能改

> btw,`struct P`里的`y`是`const int`,不管用哪个指针来操作,给`y`赋值都是非法的

这段代码中:

1. `ptr1`的类型`struct P *const`是"指针常量",可以通过它改变`p1`的值,但是不能动`ptr1`本身
2. `ptr2`的类型`const struct P *`是"常量指针",与`1.`相反
3. `ptr3`的类型`const struct P *const`,啥也改不了

接下来逐条判断

| 语句               | 合法么 | why                                    |
| ------------------ | :----: | -------------------------------------- |
| `ptr1->x = 111;` |  合法  |                                        |
| `ptr2->x = 333;` |  非法  | `ptr2`是常量指针,不能改`x`         |
| `ptr3->x = 555;` |  非法  | 无法通过`ptr3`改东西                 |
| `ptr1->y = 222;` |  非法  | `y`是`const`成员,改不了            |
| `ptr1 = &p2;`    |  非法  | `ptr1`是`const`指针,不能被重新赋值 |
| `ptr2->y = 444;` |  非法  | 略                                     |
| `ptr2 = &p1;`    |  合法  |                                        |
| `ptr3->y = 666;` |  非法  | 略                                     |
| `ptr3 = &p1;`    |  非法  | 略                                     |

## 9. 当函数变成了数字

```c
typedef int (*BinOp)(int, int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
BinOp ops[3] = {add, sub, mul};
int (*get_op(char c))(int, int)
{
    switch (c)
    {
    case '+':
        return add;
    case '-':
        return sub;
    default:
        return mul;
    }
}
int main()
{
    printf("%d\n", get_op('*')(7, -2));
    printf("%d\n", ops[1](INT_MIN, 20));
    printf("%d\n", get_op('+')(0xFFFFFFFF, 2027));
}
```

第一个调用函数`get_op('*')`,字符`'*'`没有对应的`case`,走到`default`返回`mul`,所以是`7 * (-2) = -14`

第二个调用的是`ops[1]`,也就是`sub`,计算`INT_MIN - 20`

`INT_MIN`是`-2147483648`,二进制是`10000000 00000000 00000000 00000000`,也就是`0x80000000`

`INT_MIN - 20`看起来应该是`-2147483668`,但这个数已经超出`int`能表示的范围了,我们从二进制看看到底发生了什么

减法`a - b`在机器上等价于`a`加上`b`的补码,`20`是`0x00000014`,取反加一得到`-20`的补码`0xFFFFFFEC`

```
  0x80000000
+ 0xFFFFFFEC
------------
 0x17FFFFFEC   最高位超出了 32 位,被丢掉
```

丢掉第`33`位后得到`0x7FFFFFEC`,最高位是`0`,说明它会被当成正数,换成十进制就是`2147483628`

再看第三个`printf`,`get_op('+')`返回`add`,所以实际调用的是`add(0xFFFFFFFF, 2027)`

`0xFFFFFFFF`这个字面量的类型是`unsigned int`(即`4294967295`),而`add`的参数类型是`int`,所以传参时会先把它转成`int`

32 位的`0xFFFFFFFF`按补码解释就是`-1`,所以真正算的是`-1 + 2027 = 2026`

> `BinOp ops[3]`是一个函数指针数组,里面存的是三个函数的地址
> `int (*get_op(char c))(int, int)`中`get_op`是一个函数,它的参数是`char`,返回值是`int (*)(int, int)`(也就是一个函数指针)

综合看来,这段代码不是很标准,不同环境下的结果很可能不一样

首先是第二个`printf`,有符号整数溢出在C标准里属于未定义行为。标准并没有规定它一定要回绕成`2147483628`,只是补码机器会这么算

第三个`printf`属于实现定义行为,`0xFFFFFFFF`的类型是`unsigned int`,把它传给`int`类型的参数要经过一次"无符号转有符号"的转换,而标准把这个转换的具体结果交给具体的实现去定(补码机器上按位重新解释,得到`-1`)

相比之下第一个`printf`是完全正常的:`get_op('*')`返回`mul`,`7 * (-2) = -14`没有溢出,任何平台上都是`-14`

输出:

```
-14
2147483628
2026
```

## 10. 字节战队排排站：大端、小端与内存对齐

```c
struct data_box
{
    int num;
    union
    {
        unsigned int u32_val;
        unsigned char bytes[4];
        char string[32];
    } un;
    short tag;
    long long magic;
    int buf[4];
};
int main()
{
    int arr[] = {0x00000000, 0x6F796958, 0x694C2075,
                 0x2078756E, 0x756F7247, 0x00000070,
                 0x44556677, 0x8899aabb};
    printf("%s\n", ((struct data_box *)arr)->un.string);
    printf("byte0 = 0x%02X\n", ((struct data_box *)arr)->un.bytes[0]);
    printf("byte1 = 0x%02X\n", ((struct data_box *)arr)->un.bytes[1]);
    printf("byte2 = 0x%02X\n", ((struct data_box *)arr)->un.bytes[2]);
    printf("byte3 = 0x%02X\n", ((struct data_box *)arr)->un.bytes[3]);
}
```

写这道题首先得理解内存对齐

内存对齐的规则是:

1. 每个成员的偏移必须是自身对齐数(通常是它的自然大小)的整数倍,不够就在前面补填充
2. 结构体总大小必须是其中最严格成员对齐数的整数倍,不够就在末尾补齐

可以得到结构体的内存布局

| 成员      | 类型          | 大小 | 对齐 | 偏移 |
| --------- | ------------- | ---: | ---: | ---: |
| `num`   | `int`       |    4 |    4 |    0 |
| `un`    | `union`     |   32 |    4 |    4 |
| `tag`   | `short`     |    2 |    2 |   36 |
| 填充      |               |    2 |      |   38 |
| `magic` | `long long` |    8 |    8 |   40 |
| `buf`   | `int[4]`    |   16 |    4 |   48 |

所以`sizeof(struct data_box)` = 64

`union`的大小由最大的成员决定,`string[32]`最大,所以这个`union`是`32`字节,对齐是`4`,它紧跟在`num`后面,从偏移`4`开始

`un.string`的起点是`arr`的第`4`个字节,也就是`arr[1]`的第一个字节,而`arr[0]`那 4 个`0x00`正好被`num`占掉

接下来看小端

`x86-64`是小端机器,多字节整数在内存里是低字节放在低地址,所以一个`int`写进内存后再从低地址往高地址读那`4`个字节,得到的顺序正好是反的

以`0x6F796958`为例,从低到高的`4`个字节是`58 69 79 6F`,换成字符就是`X`,`i`,`y`,`o`

于是整块内存长这样

|  偏移 | 来源                    | 内存里的字节    | 对应字符                |
| ----: | ----------------------- | --------------- | ----------------------- |
|   0~3 | `arr[0] = 0x00000000` | `00 00 00 00` | 被`num`占掉,不算      |
|   4~7 | `arr[1] = 0x6F796958` | `58 69 79 6F` | `X`,`i`,`y`,`o` |
|  8~11 | `arr[2] = 0x694C2075` | `75 20 4C 69` | `u`,空格,`L`,`i`  |
| 12~15 | `arr[3] = 0x2078756E` | `6E 75 78 20` | `n`,`u`,`x`,空格  |
| 16~19 | `arr[4] = 0x756F7247` | `47 72 6F 75` | `G`,`r`,`o`,`u` |
| 20~23 | `arr[5] = 0x00000070` | `70 00 00 00` | `p`,`\0`            |

`%s`从`un.string`开始,一路打印到第一个`\0`为止,拼出来就是

`Xiyo` + `u Li` + `nux ` + `Grou` + `p` = `Xiyou Linux Group`

后面的`arr[6]`,`arr[7]`根本没被读到

接着的四个`printf`要的就是开头`4`个字符的`ASCII`码,`un.bytes[0]`和`un.string[0]`其实是同一个地址,所以`byte0`到`byte3`正好是`X`,`i`,`y`,`o`的编码

那如果是大端呢?

大端机器是多字节整数的高字节放在低地址,同一个`0x6F796958`在内存里会存成`6F 79 69 58`,读出来就是`o`,`y`,`i`,`X`,每一组`4`个字符的顺序都会反过来,`bytes[0]`也会从`0x58`变成`0x6F`

拼出来的字符串变成了

`oyiX` + `iL u` + ` xun` + `uorG` = `oyiXiL u xunuorG`

而且会在`arr[5]`开头的`0x00`处提前结束,`p`这个字符根本不会出现

所以这段代码的输出依赖于机器的字节序,换一个平台结果就变了

此外`sizeof(struct data_box)`是`64`字节,而`arr`只有`32`字节,这个强制转换等于把一块小内存当成了更大的结构体来用

不过因为只读取了`un.string`(偏移`4~35`),而`%s`在第`21`个字节就碰上`\0`停下了,所以没有越界

但如果去访问`magic`或`buf`就会导致未知的结果

输出:

```
Xiyou Linux Group
byte0 = 0x58
byte1 = 0x69
byte2 = 0x79
byte3 = 0x6F
```
