突然发现自己已经好几天没更新了
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. 一句印不完的欢迎词
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:
sizeof(a)得到的是整个char数组的大小:a的字面量是Linux\0Group,显式写了 11 个字符(Linux5 个 +\0+Group5 个),再加编译器自动补上的结尾'\0',所以是12字节p是一个指向a的指针,64 位平台上指针是8字节,所以sizeof(p)= 8a + 0是表达式不是数组:a在这里会退化成char *,所以a + 0的类型就是char *;而sizeof只看类型、不求值,于是得到8
第二个printf:
a[5]正好是Linux后面那个'\0',所以a[5] == '\0'得到1strcmp(a, "Linux")逐字符比较,前 5 个字符相同,第 6 个又都是'\0',两个字符串完全相等,所以返回0
最后的inspect:
- 形参
char text[]等价于char *text,所以sizeof(text)= 8 - 形参
int (*matrix)[4]是”指向int[4]的指针”,所以sizeof(matrix)= 8 *matrix的类型是int[4],所以sizeof(*matrix)=4 * 4= 16strlen(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循环后
mask = 1(00000001),bitval = 2(00000010)mask = 3(00000011),bitval = 4(00000100)mask = 7(00000111),bitval = 8(00001000)- …
根据规律可见循环结束后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前进的字节数 |
|---|---|---|---|
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字节,所以相减得到的就是字节数
(char *)(p + 1) - (char *)p,p前进了 1 个int,得到4(char *)(q + 1) - (char *)q,q前进了 1 个int[4],得到16
第二个printf里两种写法最终都指向同一个元素
p + 2指向a[2],解引用得到a[2] = 3*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)
- 进入函数时
n = 3,x还是0 n != 0,先执行x += n,再printf输出before 3 3- 接着执行
int result = visit(--n);,这里的--n先把本层的n改成2,再把2传进去
进入visit(2)这一层
x += 2,x变成5,输出before 2 5--n把本层n改成1,传1给下一层
进入visit(1)这一层
x += 1,x变成6,输出before 1 6--n把本层n改成0,传0给下一层
进入visit(0)这一层
n == 0,直接return x,也就是返回6
之后开始一层层”归”,这里有个容易忽略的点:
after里输出的n已经被上面那句--n改小了,所以比before里的n小 1
- 回到
visit(1),此时它自己的n是0,result是6,输出after 0 6 6,返回x + result = 6 + 6 = 12 - 回到
visit(2),此时它自己的n是1,result是12,输出after 1 6 12,返回6 + 12 = 18 - 回到
visit(3),此时它自己的n是2,result是18,输出after 2 6 18,返回6 + 18 = 24
这个24作为printf的参数,最后输出first = 24
第一次调用结束时x仍然是6,所以第二次调用visit(2)是在x = 6的基础上继续累加
x += 2,x变成8,输出before 2 8- 进入
visit(1),x += 1,x变成9,输出before 1 9 - 进入
visit(0),直接return 9 - 回到
visit(1),输出after 0 9 9,返回9 + 9 = 18 - 回到
visit(2),输出after 1 9 18,返回9 + 18 = 27 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和*的相对位置决定了到底哪个不能动
const在*的右边:int *const p,这时p是”指针常量”,指针本身的值(存的地址)不能改,但它指向的内容可以改const在*的左边:const int *p(等价于int const *p),这时p是”常量指针”,它指向的内容不能改,但指针本身可以重新指向别处- 两边都有:
const int *const p,指针本身和所指内容都不能改
btw,
struct P里的y是const int,不管用哪个指针来操作,给y赋值都是非法的
这段代码中:
ptr1的类型struct P *const是”指针常量”,可以通过它改变p1的值,但是不能动ptr1本身ptr2的类型const struct P *是”常量指针”,与1.相反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]);
}
写这道题首先得理解内存对齐
内存对齐的规则是:
- 每个成员的偏移必须是自身对齐数(通常是它的自然大小)的整数倍,不够就在前面补填充
- 结构体总大小必须是其中最严格成员对齐数的整数倍,不够就在末尾补齐
可以得到结构体的内存布局
| 成员 | 类型 | 大小 | 对齐 | 偏移 |
|---|---|---|---|---|
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
附件下载
- 2026西邮linux兴趣小组一面试题题解.md(20 KB · 600 行)
tql,ddwawa
ddw