西邮linux兴趣小组2024一面题题解
总结
本次一面题考点包含:
0.拼命的企鹅
答案
16根
详细分析
总香蕉:100根;最大携带:50根;路程50米;前进、后退每走1米消耗1根香蕉。
关键点:一共100根,单次最多拿50,前半段必须往返转运。
设中转点距离起点x米。
- 第一趟运50根:前进x米,放下
50‑2x根,消耗x根返回起点。 - 第二趟运50根:前进x米,消耗x根。
中转点汇总香蕉:
(50‑2x)+(50‑x)=100‑3x(50‑2x)+(50‑x)=100‑3x(50‑2x)+(50‑x)=100‑3x
当中转点香蕉数量 ≤50,就不再往返,可以一次性走完剩下路程。
100−3x≤50⇒x≥503≈16.67 100-3x \le 50 \Rightarrow x\ge \frac{50}{3}\approx16.67100−3x≤50⇒x≥350≈16.67
取整数x = 17米。
- 转运阶段 0‑17米
- 第一趟:拿50根走到17米,吃掉17根,放下16根,带17根返回起点。
- 第二趟:拿50根走到17米,吃掉17根,剩余33根。
- 17米处合计香蕉:
16 + 33 = 49根,不超过50,结束往返。
- 直走回家,剩余路程:
50‑17 = 33米
携带49根继续前进,每米吃掉一根,消耗33根。
到家剩余:49‑33 = 16根。
若x取16米,中转得到52根,大于携带上限,需要再次往返,香蕉损耗更大。
1. 西邮Linux欢迎你啊
原题代码
code
int main() { unsigned int a = 2024; for (; a >= 0; a--) printf("%d\n", printf("Hi guys! Join Linux - 2%d", printf(""))); return 0; }程序输出
Hi guys! Join Linux - 2024 Hi guys! Join Linux - 2024 ...... Hi guys! Join Linux - 2024运行分析
1.a>=0符合,进入循环,执行最内层printf,不打印,返回0。
2.执行第二个printf,打印Hi guys! Join Linux - 20,返回24。
3.打印24;
4,完整输出Hi guys! Join Linux - 2024;之后a–,继续循环,直至a<0。
2. 眼见不一定为实
原题代码
code
int main() { char p0[] = "I love Linux"; const char *p1 = "I love Linux\0Group"; char p2[] = "I love Linux\0"; printf("%d\n%d\n", strcmp(p0, p1), strcmp(p0, p2)); printf("%d\n%d\n", sizeof(p0) == sizeof(p1), strlen(p0) == strlen(p1)); return 0; }程序输出
0 0 0 1运行分析
strcmp(p0,p1):两个字符串在第一个 \0 前内容完全一致 →输出 0
strcmp(p0,p2):第一个 \0 前完全一致 →输出 0
sizeof(p0)=12; sizeof(p1) =8(指针); 12==8 为假,C语言假输出 0
strlen(p0)=11,strlen(p1)=11;相等,真输出 1
知识点
strlen():函数,统计有效字符,遇到 \0 终止,不包含 \0 。
sizeof:
- 作用于数组:计算数组整块内存大小,包含末尾 \0 。
- 作用于指针:只会计算指针本身所占字节,64位环境为8字节,和字符串长度无关。
3. 1.1 - 1.0 != 0.1
原题代码
code
int main() { float a = 1.0, b = 1.1, ex = 0.1; printf("b - a == ex is %s\n", (b - a == ex) ? "true" : "false"); int A = a * 10, B = b * 10, EX = ex * 10; printf("B - A == EX is %s\n", (B - A == EX) ? "true" : "false"); }程序输出
b - a == ex is false B - A == EX is false运行分析
- float遵循IEEE‑754二进制浮点数存储。 0.1 、 1.1 在十进制是有限小数,转换成二进制是无限循环小数,计算机只能保存截断后的近似值。
- b‑a 的计算结果,与直接赋值 ex=0.1 的近似值不完全一样,直接用 == 比较得到 false 。
- 即便乘10转int依然输出false: 1.1 本身就是近似存储, 1.1 * 10 并不会精确等于11,截断转为int时出错,放大法在这里失效。
避免方法
epsilon误差比较
不要直接相等,判断两个数差值的绝对值小于一个极小值。
4. 听说爱用位运算的人技术都不太差
原题代码
code
int average(int nums[], int start, int end) { if (start == end) return nums[start]; int mid = (start + end) / 2; int leftAvg = average(nums, start, mid); int rightAvg = average(nums, mid + 1, end); return (leftAvg & rightAvg) + ((leftAvg ^ rightAvg) >> 1); }函数原理
整个函数分为两部分:递归+ 位运算求两数平均公式
1、位运算部分:(x & y) + ((x ^ y) >> 1)
作用:计算两个整数的平均值,避免 x+y 发生int溢出。
- x & y :两个数都为1的二进制位才得1。这一位相加: 1+1 = 2 ,除以2之后结果还是 1 ,直接保留。
- x ^ y :两个数不一样的二进制位(一个0,一个1)。相加结果是 1 ,除以2等价右移 >>1 。
2、递归
if (start == end) return nums[start]; //递归出口:区间只剩一个元素,平均值就是自己 int mid = (start + end) / 2; int leftAvg = average(nums, start, mid); //递归算左半区间的返回值 int rightAvg = average(nums, mid + 1, end); //递归算右半区间的返回值 //把左右两段的“结果”当成两个数字,套上面位运算求平均 return (leftAvg & rightAvg) + ((leftAvg ^ rightAvg) >> 1);优缺点
优点
规避整型溢出
普通写法(x + y) / 2,当两个很大int正数相加,x+y会超出int表示范围发生溢出,得到错误负数。
这套位运算不会完整计算x+y,按比特拆分运算,中间不会产生超大的中间和,避免溢出。执行效率高
&^>>都是CPU原生位运算指令,指令周期很短,底层运行速度优于普通加减除法。
缺点
1.只适用于整数,不支持浮点数
2.由于递归可能造成栈溢出
3.结果向下取整
5. 全局还是局部!!!
原题代码
code
int i = 1; static int j = 15; int func() { int i = 10; if (i > 5) i++; printf("i = %d, j = %d\n", i, j); return i % j; } int main() { int a = func(); printf("a = %d\n", a); printf("i = %d, j = %d\n", i, j); return 0; }程序输出
i = 11, j = 15 a = 11 i = 1, j = 15运行分析
- 全局变量:
i=1,静态全局j=15。
- main调用
func():
func内部定义局部变量int i=10,局部i会屏蔽同名全局i,函数内部访问的i是局部的10,不是外面全局i=1。
i>5条件成立,i++,局部i变为11。
- printf打印函数内:
i = 11, j =15;j是全局静态变量,值15。
return i%j → 11 % 15 =11,赋值给main里的 a 。
- main中打印
a =11。
- main里面的printf,此时不在func函数内部,局部i已经销毁,这里的 i 是全局
i=1;j依然全局静态 15 ,输出i = 1, j = 15。
知识点
1.**局部变量屏蔽(名字遮蔽):**函数内部如果定义和全局同名局部变量,在该函数内,优先使用局部变量,全局同名变量被屏蔽。func里修改的只是局部i,完全不会改动全局i。
2. **static 全局变量:**静态全局变量,整个程序生命周期存活,作用域本文件,初始化只执行一次。
3.局部auto变量:进入函数创建,函数结束直接销毁。func的局部i,出func函数就消失。
6. 指针的修罗场:改还是不改,这是个问题
原题代码
code
int main(int argc, char **argv) { int a = 1, b = 2; const int *p = &a; int * const q = &b; *p = 3, q = &a; const int * const r = &a; *r = 4, r = &b; return 0; }程序改正
int main(int argc, char **argv) { int a = 1, b = 2; const int *p = &a; int * const q = &b; p = &b; *q = 10; const int * const r = &a; // *r = 4; 非法 // r = &b; 非法 return 0; }错误分析
*p = 3:错误。const int *p,禁止通过 *p 修改内存内容。
q = &a:错误。int * const q,q是常量指针,指针本身不允许重新赋值地址。
*r = 4:错误。const int * const r,禁止修改指向的数据。
r = &b:错误。const int * const r,指针r本身不允许更换指向。
知识点
const int *p:指向常量的指针
*p禁止修改指向的数据;指针变量p本身可以改变,可以指向别的地址。
int * const q:常量指针
- 指针q本身不可修改,不能更改指向; *q 可以修改指向的数据。
const int * const r:常量指针且指向常量
- *r 不能修改内容,指针r本身也不能更换指向,两处都只读。
7. 物极必反?
原题代码
code
```int main(int argc, char *argv[]) { while (argc++ > 0); int a = 1, b = argc, c = 0; if (--a || b++ && c--) for (int i = 0; argv[i] != NULL; i++) printf("argv[%d] = %s\n", i, argv[i]); printf("a = %d, b = %d, c = %d\n", a, b, c); return 0; } ### 程序输出 ```dotnetcli argv[0] = ./a.out a = 0, b = -2147483647, c = -1运行分析
初始 argc=1
while (argc++ > 0);
argc++ 是后置自增:先用原值比较,比较完成后变量+1。
循环条件一直成立, argc 持续不断自增;有符号int不断加1,直到溢出,变为负数, 负数>0的结果为假,循环终止。
有符号整数溢出属于C语言未定义行为,循环结束后 argc 得到溢出后的负值。
- 变量初始化: a=1 , b 接收溢出后的负值, c=0 。
if (--a || b++ && c--)
运算符优先级:&&高于||
--a:前置自减, a = 0 。
||短路规则:左侧表达式为假,才计算右侧b++ && c--。
b++`:取b的负值参与逻辑判断(非0为真),判断结束b自增1。
&&左边为真,执行c-- , c = -1。
if整体条件为真,进入for循环。
- for循环遍历 argv 数组,打印全部命令行参数。无参数运行输出:
argc/argv
- argc (argument count):命令行参数的总个数,程序自身文件名算作第1个;无额外参数时, argc=1 。
- argv (argument vector):char* 指针数组,存储每个命令行参数字符串; argv[argc] 固定等于 NULL 标记数组结束。
8. 指针?数组?数组指针?指针数组?
原题代码
code
int main() { int a[2] = {4, 8}; int(*b)[2] = &a; int *c[2] = {a, a + 1}; return 0; } a, a + 1, &a, &a + 1, *(a + 1), sizeof(a), sizeof(&a) *b, *b + 1, b, b + 1, *(*b + 1), sizeof(*b), sizeof(b) c, c + 1, &c, &c + 1, **(c + 1), sizeof(c), sizeof(&c)程序输出
`
| 表达式 | 输出结果 |
|---|---|
a | a[0]地址 |
a + 1 | a[1]地址 |
&a | 数组a起始地址 |
&a + 1 | a数组后面地址 |
*(a + 1) | 8 |
sizeof(a) | 8 |
sizeof(&a) | 8 |
*b | a[0]地址 |
*b + 1 | a[1]地址 |
b | 数组a起始地址 |
b + 1 | a数组后面地址 |
*(*b + 1) | 8 |
sizeof(*b) | 8 |
sizeof(b) | 8 |
c | c[0]地址 |
c + 1 | c[1]地址 |
&c | 数组c起始地址 |
&c + 1 | c数组后面地址 |
**(c + 1) | 8 |
sizeof(c) | 16 |
sizeof(&c) | 8 |
分析
- a :数组名退化,得到 &a[0] ,a[0]的地址
- a+1 :首元素地址+1,移动1个 int (4字节) → &a[1]
- &a :取整个数组地址,数值和 a 一样,类型为数组指针
- &a+1 :数组指针+1,跨过整个数组(8字节),指向数组后面内存
- *(a+1) :对 &a[1] 解引用,取值 8
- sizeof(a) :计算整个数组内存, 2*4=8 字节
- sizeof(&a) : &a 是数组指针,64位指针占8字节
- *b : b 是数组指针, *b 等价 a ,得到 &a[0]
- *b+1 :等价 a+1 ,移动1个int,得到 &a[1]
- b :数组指针变量,存 &a ,数组a起始地址
- b+1 :数组指针+1,跨过整个 int[2] 数组,指向数组后方
- *(*b+1) :等价 *(a+1) ,取值 8
- sizeof(*b) : *b 类型是 int[2] ,数组大小8字节
- sizeof(b) : b 是指针,占8字节
- c :指针数组数组名,退化为 &c[0] ,c[0]的地址
- c+1 :数组首元素地址+1,跨过1个 int* (8字节),得到 &c[1]
- &c :取整个指针数组的地址
- &c+1 :跨过整个 int*[2] 数组(16字节),指向数组后面
- **(c+1)
- c+1 → &c[1]
- *(c+1) → c[1] ,也就是 a+1
- **(c+1) → *(a+1) ,取值 8
- sizeof© :指针数组,2个指针, 2*8=16 字节
- sizeof(&c) : &c 是数组指针,占8字节
知识点
- 数组名两大特例: sizeof(数组名) 、 &数组名 代表整个数组;其余场景数组名退化为首元素地址。
- int (*b)[2] :数组指针,专门指向完整数组; +1 的步长等于一整个数组的字节数。
- int *c[2] :指针数组,普通数组,数组内部存储的元素是指针。
- a 、 &a 、 *b 地址数值相同,但是类型不同,进行 +1 运算移动的内存字节完全不一样。
9. 宏函数指针嘻嘻哈哈,好玩好玩
原题代码
code
#define SQUARE(x) x *x #define MAX(a, b) (a > b) ? a : b; #define PRINT(x) printf("嘻嘻,结果你猜对了吗,包%d滴\n", x); #define CONCAT(a, b) a##b int main() { int CONCAT(x, 1) = 5; int CONCAT(y, 2) = 3; int max = MAX(SQUARE(x1 + 1), SQUARE(y2)) PRINT(max) return 0; }程序输出
嘻嘻,结果你猜对了吗,包11滴分析
- CONCAT(x,1) 经 ## 拼接得到 x1 ,定义 int x1 = 5;
- CONCAT(y,2) 经 ## 拼接得到 y2 ,定义 int y2 = 3;
- SQUARE(x1 + 1) 直接文本替换展开为: x1 + 1 * x1 + 1
- SQUARE(y2) 展开为: y2 * y2
- MAX 宏展开: int max = (x1 + 1 * x1 + 1 > y2 * y2) ? x1 + 1 * x1 + 1 : y2 * y2;
- PRINT(max) 展开为printf函数调用。
10. 我写的排序最快
原题代码
code
int main() { int arr1[] = {2, 3, 1, 3, 2, 4, 6, 7, 9, 2, 10}; int arr2[] = {2, 1, 4, 3, 9, 6, 8}; int len1 = sizeof(arr1) / sizeof(arr1[0]); int len2 = sizeof(arr2) / sizeof(arr2[0]); result result; your_sort(arr1, len1, arr2, len2, &result); for (int i = 0; i < result.len; i++) { printf("%d ", result.arr[i]); } free(result.arr); return 0; }程序
void your_sort(int *arr1, int len1, int *arr2, int len2, result *res) { //1.先把两个数组全部拷贝到临时空间 int total = len1 + len2; int *tmp = (int *)malloc(total * sizeof(int)); int k = 0; for(int i = 0; i < len1; i++) tmp[k++] = arr1[i]; for(int i = 0; i < len2; i++) tmp[k++] = arr2[i]; //2.冒泡排序 升序 for(int i = 0; i < total - 1; i++) { for(int j = 0; j < total - 1 - i; j++) { if(tmp[j] > tmp[j+1]) { int t = tmp[j]; tmp[j] = tmp[j+1]; tmp[j+1] = t; } } } //3.去重 int *out = (int *)malloc(total * sizeof(int)); int out_len = 0; for(int i = 0; i < total; i++) { //第一个元素,或者和上一个不相等才存入 if(out_len == 0 || tmp[i] != out[out_len - 1]) { out[out_len++] = tmp[i]; } } free(tmp); //4.赋值给返回结构体 res->arr = out; res->len = out_len; }分析
- 将arr1、arr2全部拷贝临时数组tmp: {2,3,1,3,2,4,6,7,2,1,4,3,9,6,8}
- 冒泡升序排序tmp: {1,1,2,2,2,3,3,3,4,4,6,6,7,8,9}
- 遍历排序后的数组,相邻去重: {1,2,3,4,6,7,8,9}
- malloc开辟堆内存,把去重结果存入 res->arr , res->len 记录有效元素个数。
- main函数循环打印;main执行 free(result.arr) 释放堆内存。
11. 猜猜我是谁
原题代码
code
int main() { void *a[] = {(void *)1, (void *)2, (void *)3, (void *)4, (void *)5}; printf("%d\n", *((char *)a + 1)); printf("%d\n", *(int *)(char *)a + 1); printf("%d\n", *((int *)a + 2)); printf("%lld\n", *((long long *)a + 3)); printf("%d\n", *((short *)a + 4)); return 0; }程序输出
0 2 2 4 2运行分析
环境:x86‑64 Linux, sizeof(void)=8 ,小端序。void *a[]是指针数组,每个元素占8字节。(void *)1完整8字节十六进制: 0x0000000000000001
小端内存字节排布(低地址到高地址):01 00 00 00 00 00 00 00 | 02 00 00 00 00 00 00 00 …
*((char *)a + 1)
转为 char* ,步长1字节;+1偏移1字节,读取地址1字节内容 00 ,输出0。
*(int *)(char *)a + 1
从数组起始地址读取4字节,解析得到数值1;运算符优先级,先解引用,结果再加1。1+1=2,输出2。
*((int *)a + 2)
int 占4字节,+2偏移2*4=8字节,跳到 a[1] 起始位置,取值2,输出2。
*((long long *)a + 3)
long long 占8字节,+3偏移 3*8=24 字节,定位到 a[3] ,取值4,输出4。
*((short *)a + 4)
short 占2字节,+4偏移 4*2=8 字节,跳到 a[1] 起始位置,取值2,输出2。
知识点
- 指针运算步长: T* p; p+n ,地址偏移字节数 = n * sizeof(T) 。偏移多少由指针类型决定。
- 强制类型转换:只改变内存的解读方式,原始内存字节不会发生修改。
- 大小端作用范围
- 大小端只对占用大于1字节的数据生效,控制单元内部多个字节的摆放顺序(小端:低字节存低地址)。
- char 占1字节,不存在高低字节,不受大小端影响。
- 分块规则:按 sizeof(类型) 把连续内存划分成数据单元;大小端只管单元内部字节顺序,不分块。
- 运算符优先级: *p + 1 与 *(p + 1) 完全不一样。 *p+1 是取出p的值再加1; *(p+1) 是指针先偏移再解引用。
- 64位系统,指针 void* 大小为8字节
12. 结构体变小写奇遇记
原题代码
code
union data { int a; double b; short c; }; typedef struct node { long long a; union data b; void (*change)( struct node *n); char string[0]; } Node; void func(Node *node) { for (size_t i = 0; node->string[i] != '\0'; i++) node->string[i] = tolower(node->string[i]); } int main() { const char *s = "WELCOME TO XIYOULINUX_GROUP!"; Node *P = (Node *)malloc(sizeof(Node) + (strlen(s) + 1) * sizeof(char)); strcpy(P->string, s); P->change = func; P->change(P); printf("%s\n", P->string); return 0; }程序输出
welcome to xiyoulinux_group!运行分析
环境:x86‑64 Linux,8字节对齐。
- 计算 union data 大小
联合体大小为内部最大成员大小, double b 占8字节,故 union data 占8字节。
- 计算 sizeof(Node)
- long long a :8字节
- union data b :8字节
- 函数指针 void (*change)(struct node *n) :64位指针占8字节
- char string[0] :柔性零长数组,不计入结构体sizeof大小
sizeof(Node) = 8 + 8 + 8 = 24 字节。
- malloc(sizeof(Node)+strlen(s)+1)
分配24字节结构体头部,额外追加字符串需要的内存;
零长数组 string[0] 地址紧贴结构体末尾,访问 P->string[i] 就是访问malloc额外申请的那片内存。
- strcpy(P->string, s) ,把大写字符串拷贝到柔性数组对应的内存。
- 将函数指针 P->change 赋值为 func ,通过函数指针调用 func§ 。
- func循环遍历 string , tolower() 把大写字母全部转为小写。
- printf打印处理后的小写字符串。
知识点
- union联合体
- 全部成员共享同一块起始内存。
- 联合体大小 = 内部最大成员大小,同时满足对齐要求。
- 同一时刻一般只使用其中一个成员,写入一个成员会覆盖其他成员的数据。
- 柔性零长数组 char string[0](GNU扩展)
- 数组本身不占用结构体内存, sizeof 不会计算。
- 数组起始地址紧贴结构体内存末尾。
- malloc公式: malloc(结构体大小 + 变长数据字节数) 。
- 适合:结构体头部 + 后面紧跟一段长度不确定的数据。
- 注意:不能用栈上结构体配合柔性数组,必须malloc堆内存。
- 函数指针
- 保存函数入口地址,可以间接调用函数。
- x86‑64下函数指针大小8字节。
- 语法: 返回值 (*指针名)(参数类型) 。
- 内存对齐规则
- 结构体每个成员的偏移,必须是自身类型大小的整数倍。
- 结构体整体大小,必须等于内部最大基础类型大小的整数倍。
- 字符串处理要点
- strlen 只统计可见字符,不含末尾 \0 ;存放字符串必须多分配1字节给结束符。
- tolower() 只转换大写英文字母,其他字符不改动。
- 字符串字面量 const char* 存放在只读段,不可以原地修改;本题拷贝到malloc堆内存,才允许修改字符。
- 内存模型
malloc得到一整块连续堆内存;前部分存结构体头部,后面存变长字符串,柔性数组充当桥梁访问后面变长区域。