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

喜欢这篇文章就点个赞吧

突然发现自己已经好几天没更新了

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

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

写出二进制对照表

十进制二进制
11
210
311
5101
81000
131101
2110101
34100010
55110111
891011001
14410010000
23311101001
377101111001
6101001100010
9871111011011
159711000111101
……

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

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

继续这样计算有

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

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

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

1. 一句印不完的欢迎词

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. 失忆的交换生

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 的视力表

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 密钥:藏在字节里的悄悄话

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. 宏召唤术:括号去哪儿了

#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 的步幅之争

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前进的字节数
pint *p = a;int *4
qint (*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

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 大神的奶酪

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. 当函数变成了数字

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. 字节战队排排站:大端、小端与内存对齐

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. 结构体总大小必须是其中最严格成员对齐数的整数倍,不够就在末尾补齐

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

成员类型大小对齐偏移
numint440
ununion3244
tagshort2236
填充238
magiclong long8840
bufint[4]16448

所以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~3arr[0] = 0x0000000000 00 00 00被num占掉,不算
4~7arr[1] = 0x6F79695858 69 79 6FX,i,y,o
8~11arr[2] = 0x694C207575 20 4C 69u,空格,L,i
12~15arr[3] = 0x2078756E6E 75 78 20n,u,x,空格
16~19arr[4] = 0x756F724747 72 6F 75G,r,o,u
20~23arr[5] = 0x0000007070 00 00 00p,\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

附件下载

ruosha 一个热爱计算机的普通人。这里记录算法竞赛题解与学习笔记,顺带折腾服务器。

《2026西邮linux兴趣小组一面试题题解》有2条评论

发表评论

评论需要经过审核后才会公开显示,请耐心等待。

冀ICP备2026040623号