西邮Linux兴趣小组2023纳新面试题题解
前言
- 本题目只作为西邮 Linux 兴趣小组 2023 纳新面试的有限参考。
- 为节省版面,本试题的程序源码省去了#include 指令。
- 本试题中的程序源码仅用于考察 C 语言基础,不应当作为 C 语言「代码风格」的范例。
- 所有题目编译并运行于 x86_64 GNU/Linux 环境。
- 小编是C语言初学者,若有知识性错误敬请谅解。
学长寄语:
长期以来,西邮Linux兴趣小组的面试题以难度之高名扬西邮校内。我们作为出题人也清楚的知道这份试题略有难度。请别担心。若有同学能完成一半的题目,就已经十分优秀。 其次,相比于题目的答案,我们对你的思路和过程更感兴趣,或许你的答案略有瑕疵,但你正确的思路和对知识的理解足以为你赢得绝大多数的分数。最后,做题的过程也是学习和成长的过程,相信本试题对你更加熟悉的掌握C语言的一定有所帮助。祝你好运。我们FZ103见!
0.鼠鼠我啊,要被祸害了
有 1000 瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水,24 小时后就会准时死亡。
至少要多少只小白鼠才能在 24 小时内鉴别出哪瓶水有毒?
将1000瓶水按1000,999,998…1排好编号,2的10次方为1024,将编号转换为二进制最多需要10位,取10只小鼠,每只小鼠从左向右依次对应1位,让每只小鼠喝下其对应位为1的水,24小时后,死亡的小鼠代表1,未死亡的小鼠代表0,例如:第七、八、九只小鼠死亡(其他都未死亡)的话,便可找到编号为0000001110(十进制即为14)的水是有毒的。
1.先预测一下~
按照函数要求输入自己的姓名试试~
char *welcome() {
// 请你返回自己的姓名
}
int main(void) {
char *a = welcome();
printf("Hi, 我相信 %s 可以面试成功!\n", a);
return 0;
}
-
上述代码前三行定义了一个返回值为“自己的姓名”的地址量,名称为welcome的指针函数。
-
在主函数中,定义了一个指针a,welcome( )调用了前三行定义的函数,即将welcome函数的返回值赋给a,现在,a就储存了“自己的名字”的地址量。在下一行,printf中%s根据a储存的地址输出字符串,即输出“自己的名字”。
-
至于welcome函数中返回自己姓名(以noregret为例)的方式,我在这里能提供这几种:
-
return "noregret";
-
static char *str="noregret";
return str;
这里我用static的原因是str是局部变量,局部变量在执行完后会立即清除,不会一直占用内存,因此我如果直接用return str,结果将会出现乱码。而我用static修饰局部变量将会延长它的生命周期到整个程序。
-
static char str[20];
strcpy(str,"noregret");
return str;
注意在使用strcpy函数的时候要引用数据库<string.h>
2.欢迎来到 Linux 兴趣小组
有趣的输出,为什么会这样子呢~
int main(void) {
char *ptr0 = "Welcome to Xiyou Linux!";
char ptr1[] = "Welcome to Xiyou Linux!";
if (*ptr0 == *ptr1) {
printf("%d\n", printf("Hello, Linux Group - 2%d", printf("")));
}
int diff = ptr0 - ptr1;
printf("Pointer Difference: %d\n", diff);
}
-
char *ptr0 = "Welcome to Xiyou Linux!";
定义了字符指针ptr0,指向一个字符串常量 “Welcome to Xiyou Linux!”,这个字符串常量存储在只读内存区域,所以 ptr0 指向了这个字符串。 -
char ptr1[] = "Welcome to Xiyou Linux!";
定义了字符数组 ptr1,并将其初始化为字符串 “Welcome to Xiyou Linux!”,在这里,ptr1是一个字符数组,它在栈上分配内存,用于存储字符串的副本。 -
对于 *ptr0,自然指向字符串首字母,而对于 *ptr1,数组名的值是数组首元素的地址,则 *ptr1也指向字符串首字母。因此 *ptr0 == *ptr1,执行if语句。在if语句中,有一个 printf 嵌套,由里向外执行,printf 函数返回值返回的是成功打印到标准输出的字符数,在 printf(" ")中,由于字符串中没有任何字符,因此打印的字符数为0,返回值为0。 if语句将会打印出 Hello, Linux Group - 2023。
-
int diff = ptr0 - ptr1;
这一行计算了指针 ptr0 和 ptr1 之间的差值,即它们指向的内存地址之间的偏移量。由于 ptr0 指向一个字符串常量,而 ptr1 指向一个栈上的字符数组,它们指向不同的内存区域,所以 diff 的值将取决于它们之间的地址差异。
3.一切都翻倍了吗
① 请尝试解释一下程序的输出。
② 请谈谈对 sizeof()和 strlen()的理解吧。
③ 什么是 sprintf(),它的参数以及返回值又是什么呢?
int main(void) {
char arr[] = {'L', 'i', 'n', 'u', 'x', '\0', '!'}, str[20];
short num = 520;
int num2 = 1314;
printf("%zu\t%zu\t%zu\n", sizeof(*&arr), sizeof(arr + 0),
sizeof(num = num2 + 4));
printf("%d\n", sprintf(str, "0x%x", num) == num);
printf("%zu\t%zu\n", strlen(&str[0] + 1), strlen(arr + 0));
}
首先回答第②个问题:
- sizeof是一种单目运算符,以字节为单位返回运算对象的大小。运算对象可以是具体的数据对象或类型。如果运算对象是类型,则必须用圆括号括起来。
运算对象是表达式:sizeof只关心内存长度,不计算表达式。表达式如果是一个混合类型,结果取返回值类型宽度。
运算对象是函数:结果取函数声明返回值类型宽度,函数并不会被调用。
- strlen()函数用于统计字符串的长度(不包含"\0"),其中字符串以"\0"为结束标志。
-
sizeof(*&arr);
这里 *&arr 实际上等于 arr,因此它返回了 arr 的大小,即包含7个字符的字符数组大小。 -
sizeof(arr + 0);
对于单独的数组名,是一个指向数组第一个元素的指针,因此它返回指针的大小,指针大小在32位机中是4, 64位机中是8。 -
sizeof(num = num2 + 4));
这个表达式并没有进行运算, num 的值保持不变,它返回 short 类型的大小,通常为2个字节。 -
printf("%d\n", sprintf(str, "0x%x", num) == num);
这一行使用 sprintf 函数将十六进制格式的 num 写入字符数组 str,然后将结果与 num 进行比较。sprintf 返回写入 str 的字符数,如果它等于 num,则条件成立,返回1,否则返回0。 -
printf("%zu\t%zu\n", strlen(&str[0] + 1), strlen(arr + 0));
这一行使用 strlen 函数来计算两个字符串的长度:&str[0] + 1 是 str 数组中的一个偏移指针,从第二个字符开始,然后计算长度。因此它返回 str 中的字符串 “0x208”(num 的十六进制表示)从第二位开始的长度,即4。
arr + 0 返回 arr 中的字符串 “Linux” 的长度,即5。
4.奇怪的输出
程序的输出结果是什么?解释一下为什么出现该结果吧~
int main(void) {
char a = 64 & 127;
char b = 64 ^ 127;
char c = -64 >> 6;
char ch = a + b - c;
printf("a = %d b = %d c = %d\n", a, b, c);
printf("ch = %d\n", ch);
}
char a = 64 & 127;
这一行执行了一个位与操作。64 的二进制表示是 01000000,而 127的二进制表示是 01111111。位与操作会将两个操作数的对应位进行逻辑与运算,得到结果 01000000,即 64。所以,变量 a 被赋值为 64。char b = 64 ^ 127;
这一行执行了一个位异或操作。位异或操作会将两个操作数的对应位进行逻辑异或运算,得到结果 00111111,即 63。所以,变量 b 被赋值为 63。char c = -64 >> 6;
这一行执行了一个右移运算。-64的二进制表示是11000000,在有符号整数的情况下,右移会用符号位来填充左侧,将其向右移6位得到11111111,即-1。所以,变量c被赋值为-1。char ch = a + b - c;
计算a+b-c,得到128,但由于char类型占1字节,就是8位,它的取值范围为-128~127,这里产生数据溢出,ch将会被赋值为-128。
5.乍一看就不想看的函数
“人们常说互联网凛冬已至,要提高自己的竞争力,可我怎么卷都卷不过别人,只好用一些奇技淫巧让我的代码变得高深莫测。”
这个 func()函数的功能是什么?是如何实现的?
int func(int a, int b) {
if (!a) return b;
return func((a & b) << 1, a ^ b);
}
int main(void) {
int a = 4, b = 9, c = -7;
printf("%d\n", func(a, func(b, c)));
}
-
在func()函数中,对其自身进行调用,实现了递归,这里我们对这个递归函数进行分析:
寻找递归结束条件:只有当a等于0时,函数才能结束,返回值为b。
那么这个函数就是在让a不断逼近于0。
-
计算func(4, func(9,-7))结果为6。
6.自定义过滤
请实现 filter()函数:过滤满足条件的数组元素。
提示:使用函数指针作为函数参数并且你需要为新数组分配空间。
typedef int (*Predicate)(int);
int *filter(int *array, int length, Predicate predicate,
int *resultLength); /*补全函数*/
int isPositive(int num) { return num > 0; }
int main(void) {
int array[] = {-3, -2, -1, 0, 1, 2, 3, 4, 5, 6};
int length = sizeof(array) / sizeof(array[0]);
int resultLength;
int *filteredNumbers = filter(array, length, isPositive,
&resultLength);
for (int i = 0; i < resultLength; i++) {
printf("%d ", filteredNumbers[i]);
}
printf("\n");
free(filteredNumbers);
return 0;
}
上代码:
#include<stdio.h>
#include<stdlib.h>
typedef int (*Predicate)(int);//将Predicate定义为一个函数指针,指向的函数接收1个整数为参数并返回1个整数
int *filter(int *array, int length, Predicate predicate, int *resultLength) {
int *filteredArray = (int *)malloc(length * sizeof(int));//为新数组分配空间
int count = 0;
for (int i = 0; i < length; i++) {
if (predicate(array[i])) {
filteredArray[count] = array[i];
count++;
}
}
*resultLength = count;
return filteredArray;
}
int isPositive(int num) {
return num > 0;
}//筛选正数
int main(void) {
int array[] = {-3, -2, -1, 0, 1, 2, 3, 4, 5, 6};
int length = sizeof(array) / sizeof(array[0]);
int resultLength;
int *filteredNumbers = filter(array, length, isPositive, &resultLength);
for (int i = 0; i < resultLength; i++) {
printf("%d ", filteredNumbers[i]);
}
printf("\n");
free(filteredNumbers);//!!!释放内存
return 0;
}
关于typedef:
在这里定义了一个类型别名,这个类型别名的目的是为了让你能够更方便地声明和使用函数指针,特别是在你需要传递函数指针作为参数给其他函数时。它使代码更加清晰和易于理解,同时也提高了代码的可维护性。在这段代码中,使用 Predicate
类型来声明和传递isPositive
函数。
想了解更详细的用法,点这里
7.静…态…
① 如何理解关键字 static?
② static 与变量结合后有什么作用?
③ static 与函数结合后有什么作用?
④ static 与指针结合后有什么作用?
⑤ static 如何影响内存分配?
①static代表静态的,在C语言中,static是一个用来修饰变量与函数的关键字,被修饰对象的某些性质将发生根本性的变化,而这些变化从某种意义上又“契合”了静态这个概念。
② 与变量结合:根据变量的作用域(指变量可以被使用的区间)范围可以把变量划分为局部变量和全局变量。而static与这两种变量结合有不同的作用,下面我们进行分类讨论:
- 与局部变量结合:局部变量出了作用域其值便会被销毁,而static修饰局部变量时,会将局部变量的生命周期延长为全局变量的生命周期,但并不会改变其作用域。
- 与全局变量结合:**全局变量具有外部链接属性。**而static修饰全局变量时,会将其外部链接属性变为内部连接属性(将external变为internal),即在其他源文件(.c)中不能再使用该变量。像是将全局变量的作用域缩小了。
③与函数结合(与全局变量类似):**函数也具有外部链接属性。**static修饰函数时,会将其外部链接属性变为内部连接属性(也可以说是将external变为inexternal),即在其他源文件(.c)中不能再使用该函数。像是将函数的作用域缩小了。
④与指针结合:
- 静态指针变量:如果指针变量声明为 static,则该指针在程序的整个生命周期内保持不变,其作用域被限制在声明它的源文件中。
- 静态指针指向静态变量:如果一个指针指向一个声明为 static 的变量,那么这个指针在其他文件中是不可见的。
⑤内存中的存储区域包括:
程序代码区:存放函数体的二进制代码
静态/全局存储区:存放全局变量和静态变量
动态存储区:栈区:由编译器自动分配释放,存放局部变量
堆区:程序员分配释放
被static修饰的变量,函数等等都会被存储在静态存储区。
8.救命!指针!
数组指针是什么?指针数组是什么?函数指针呢?用自己的话说出来更好哦,下面数据类
型的含义都是什么呢?
int (*p)[10];
const int* p[10];
int (*f1(int))(int*, int);
-
数组指针:本质为一个指针,指向一个数组
-
指针数组:本质为一个数组,该数组存放指针
-
函数指针:本质为一个指针,指向一个函数
-
这里我们将
int (*p)[10]
与const int* p[10]
对比分析:可以看出它们只有一个括号的区别,由于优先级:( ( )>[]>* ),如果有括号,p会先和*结合,成为一个指针;如果没有括号,则p会先和[]结合,成为一个数组。因此:
int (*p)[10]
是一个数组指针,指向一个存放10个 int 类型数据的数组;const int* p[10]
是一个指针数组,存放10个 (int *) 类型的指针,指针指向为一个整型数常量。补充一个知识:
const*
与*const
:const*
:指针常量,表示指针指向的地址可以变,而指向的值不能变;*const
:常量指针,表示指针存放的地址不可以变,但是这个地址对应的值是可以改变;const * const
:指向常量的常量指针,表示指针存放的地址不能改变,指向的值也不能改变。 -
int (*f1(int))(int*, int)
:- f1( int )是一个函数,它接受一个int型参数
- int (*f1(int))表示f1函数返回一个指针,该指针指向一个函数
(int*, int)
表示f1返回指针指向的函数接受一个int*
型参数和一个int 型参数
9.咋不循环了
程序直接运行,输出的内容是什么意思?
int main(int argc, char* argv[]) {
printf("[%d]\n", argc);
while (argc) {
++argc;
}
int i = -1, j = argc, k = 1;
i++ && j++ || k++;
printf("i = %d, j = %d, k = %d\n", i, j, k);
return EXIT_SUCCESS;
}
-
我们经常使用的main函数是不带参数的,但它也可以有参数,C语言规定,main函数只能有两个参数,第一个是命令行参数(这里我们叫做argc),第二个参数是存放指向命令行参数字符串的指针的数组(这里我们叫做argv)。
-
while (argc) { ++argc; }
在这个程序中argc的值为1,而在这个循环,argc的值不断增加,直至达到int类型的最大范围,int类型的取值范围为
(-2^31,2^31-1)
,即argc=2^31-1
之后再自增产生数据溢出,argc将会变为-2^31
,因此,argc总有等于0的时候,那时将会退出循环,而并不会造成死循环。 -
i++ && j++ || k++;
先来了解一下逻辑运算符:
-
&既是逻辑运算符也是位运算符,作为取地址符时还是单目运算符;&&只是逻辑运算符和双目运算符。
&不具有短路效果,即左边Flase,右边还会执行;&&具有短路效果,左边为Flase,右边则不执行。
-
| 既是逻辑运算符也是位运算符;||只是逻辑运算符和双目运算符。
|不具有短路效果,即左边True,右边还会执行;||具有短路效果,左边为True,右边则不执行。
-
平常实际运用中,用&&和 || 作逻辑运算符多一些,因为具有短路效果,提升了程序的运行效率,起到程序优化作用。
接着我们对这段代码进行分析:
先计算
i++&&j++
,自增符号在变量右边,表示先参与运算后自增,i的初始值为-1,为真,因此会执行j++,i++&&j++
的值为0,然后i和j自增。接着计算0||k++
,值为1,然后k自增。 -
-
return EXIT_SUCCESS 本质就是 return 0,EXIT_SUCCESS 是头文件 stdlib.h 中定义的一个符号常量。
10.到底是不是 TWO
#define CAL(a) a * a * a
#define MAGIC_CAL(a, b) CAL(a) + CAL(b)
int main(void) {
int nums = 1;
if(16 / CAL(2) == 2) {
printf("I'm TWO(ノ>ω<)ノ\n");
} else {
int nums = MAGIC_CAL(++nums, 2);
}
printf("%d\n", nums);
}
#define CAL(a) a * a * a
宏定义了一个函数 CAL(a),当出现CAL(a)时,其将会被替换为a*a*a
#define MAGIC_CAL(a, b) CAL(a) + CAL(b)
执行宏嵌套,当出现MAGIC_CAL(a, b)时,其将会被替换为a*a*a+b*b*b
16 / CAL(2)
即16/2*2*2
,运算符/与*的优先级一样,从左向右执行,值为32,因此if语句的判断条件为假,执行else语句,这里重新定义了一个nums,它的作用域只在else语句中,并不会影响这之外nums的值,因此最后nums的打印值依旧为1。
11.克隆困境
试着运行一下程序,为什么会出现这样的结果?
直接将 s2 赋值给 s1 会出现哪些问题,应该如何解决?请写出相应代码。
struct Student {
char *name;
int age;
};
void initializeStudent(struct Student *student, const char *name, int age) {
student->name = (char *)malloc(strlen(name) + 1);
strcpy(student->name, name);
student->age = age;
}
int main(void) {
struct Student s1, s2;
initializeStudent(&s1, "Tom", 18);
initializeStudent(&s2, "Jerry", 28);
s1 = s2;
printf("s1 的姓名: %s 年龄: %d\n", s1.name, s1.age);
printf("s2 的姓名: %s 年龄: %d\n", s2.name, s2.age);
free(s1.name);
free(s2.name);
return 0;
}
有关->
与.
的异同,这里
void initializeStudent(struct Student *student, const char *name,
int age) {
student->name = (char *)malloc(strlen(name) + 1);
strcpy(student->name, name);
student->age = age;
}
- 这个函数用于初始化 Student 结构体,它接收一个指向 struct 的指针,一个字符串 name,一个整数 age。
- 首先为 student->name 分配内存空间,然后使用strcpy函数将字符串 name 复制给 student->name。
- 然后将整数 age 赋值给 student->age。
下面重点分析s1 = s2;
造成的后果:
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
而在这里,直接将s2赋值给s1,就是进行了浅拷贝。这样在释放s1.name的内存后,就会使s2.name成为悬挂指针。
当指针指向的内存空间已经被释放,但是该指针没有任何的改变,以至于仍然指向已经被回收的内存地址,这种情况下该指针就被称为悬挂指针。
这是修改后的代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Student {
char *name;
int age;
};
void initializeStudent(struct Student *student, const char *name, int age) {
student->name = (char *)malloc(strlen(name) + 1);
strcpy(student->name, name);
student->age = age;
}
void copyStudent(struct Student *s1, const struct Student *s2) {
s1->name = (char *)malloc(strlen(src->name) + 1);
strcpy(s1->name, s2->name);
s1->age = s2->age;
}//深拷贝
int main(void) {
struct Student s1, s2;
initializeStudent(&s1, "Tom", 18);
initializeStudent(&s2, "Jerry", 28);
copyStudent(&s1, &s2);
printf("s1 的姓名: %s 年龄: %d\n", s1.name, s1.age);
printf("s2 的姓名: %s 年龄: %d\n", s2.name, s2.age);
free(s1.name);
free(s2.name);
return 0;
}
12.你好,我是内存
作为一名合格的 C-Coder,一定对内存很敏感吧~来尝试理解这个程序吧!
struct structure {
int foo;
union {
int integer;
char string[11];
void *pointer;
} node;
short bar;
long long baz;
int array[7];
};
int main(void) {
int arr[] = {0x590ff23c, 0x2fbc5a4d, 0x636c6557, 0x20656d6f,
0x58206f74, 0x20545055, 0x6577202c, 0x6d6f636c,
0x6f742065, 0x79695820, 0x4c20756f, 0x78756e69,
0x6f724720, 0x5b207075, 0x33323032, 0x7825005d,
0x636c6557, 0x64fd6d1d};
printf("%s\n", ((struct structure *)arr)->node.string);
}
要理解这个程序,首先我们需要了解字节序和大小端:
在计算机中,有种叫字节序的东西。
字节顺序,又称端序或尾序,在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。
在几乎所有的机器上,多字节对象都被存储为连续的字节序列。例如在C语言中,一个类型为int的变量x地址为0x100,那么其对应地址表达式&x的值为0x100。且x的四个字节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置。
字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。
理解不了?看图说话:
下面我们来分析一下这段代码:
-
首先,定义了一个数组arr,这个数组中储存了多个16进制数,这里我们需要知道一个16进制数占多少字节:
1个字节是8位,1个2进制数是8位:范围从0000 0000~1111 1111,即0~255。一位16进制数(用二进制表示是xxxx)最多只表示到15(即16进制中的F),要表示到255,就还需要第二位。所以1个字节=2个十六进制字符,一个16进制位=0.5个字节。
那么该数组中1个16进制数就占4个字节。
((struct structure *)arr)->node.string
:首先将数组 arr 强制转换为一个指向结构体structure
的指针,然后用"->"运算符访问结构体中联合体node
的string
。因为在结构体中,变量string
前还有两个int
类型的变量,一个int
类型的变量占4个字节,因此在数组arr中,从第三个数开始才指向node.string
。**计算机一般采用小端存储模式。**即从0x636c6557
中的57
开始翻译,对照ASCII码表,一直翻译到’\0’,结束翻译。
这个输出结果是很有意义的,让我们来看一下:
Welcome to XUPT , welcome to Xiyou Linux Group [2023]
恭喜你做到这里!你的坚持战胜了绝大多数看到这份试题的同学。
或许你自己对答题的表现不满意,但别担心,请自信一点呐。
坚持到达这里已经证明了你的优秀。
还在等什么,快带上你的笔记本电脑,来FZ103面试吧!