① 本题目只作为西邮 Linux 兴趣小组 2023 纳新面试的有限参考。
② 为节省版面,本试题的程序源码省去了#include 指令。
③ 本试题中的程序源码仅用于考察 C 语言基础,不应当作为 C 语言「代码风格」的范例。
④ 所有题目编译并运行于 x86_64 GNU/Linux 环境。
0.鼠鼠我啊,要被祸害了
有 1000 瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水,24 小时后就会准时死亡。
至少要多少只小白鼠才能在 24 小时内鉴别出哪瓶水有毒?
10瓶
。
我们将1000瓶水用二进制标号,1000的二进制是111101000
。一共十位,第一瓶水是0000000001,以此类推,将第一位是1的所有瓶子里的水取出来一些混合,标上1,以此类推,得到十瓶混合的水,将十瓶水分别喂给十只鼠鼠
,24小时后,死掉的每只鼠鼠都对应着一个标号,对应一个十位的二进制数,相应标号位上的数为1,其他位上数为零,得到一个瓶子的标号,就是有毒的那瓶。
1.先预测一下~
按照函数要求输入自己的姓名试试~
char *welcome() {
// 请你返回自己的姓名
}
int main(void) {
char *a = welcome();
printf("Hi, 我相信 %s 可以面试成功!\n", a);
return 0;
}
最简单的方法是直接在welcome函数里return自己的名字,如下
char *welcome() {
return xxx;
}
int main(void) {
char *a = welcome();
printf("Hi, 我相信 %s 可以面试成功!\n", a);
return 0;
}
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);
}
if语句比较了两个字符数组的首元素值,W=W,所以执行语句.
printf函数嵌套,先执行括号里的,所以最里面的括号里的先执行,
什么也不打印,返回0
,
然后中层的printf打印双引号里的字符并将占位符%d替换为里面函数的返回值0。
最后,最外面的printf函数打印中间函数的返回值23
。
最后结果就为“Hello, LinuxGroup - 2023"。
最后一个printf打印两个地址之差
,由于 不指向同一个数组,c语言未定义这种情况,出现的值千奇百怪。
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运算符计算占空间字节数,
strlen函数计算字符串长度且不计\0
sizeof是运算符,strlen是函数,sizeof接收的参数是地址,streln接收的参数是字符串
第一行printf函数打印三个 size_t
类型的值,第一个解引用又取地址
,相当于打印arr数组占字节数,为72
;
第二个打印arr数组首元素地址+0
后的字节数,不论加什么,在64位系统中,地址占字节数都为8
;
第三个打印里面整个表达式的结果占字节数,结果num是short int 类型,所以为2
。
第二行printf函数打印sprintf函数的返回值与num相等是否为真的值。
sprintf函数至少有两个参数,若有三个,则第二个参数中有转换说明,这个sprintf函数将num的值拷贝到str数组里,然后返回拷贝的字符数,为3,与520不相等,所以printf函数打印0;
第三行,strlen函数与sizeof运算符不同的是,strlen函数接收地址,printf函数第一个参数为str从第二个元素开始计算的字符串长度,为4
,第二个参数为从第一个元素开始,为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);
}
- &运算符是同位都为1才是1其他情况都是0
- ^运算符是同位相同则为1,不同则为0
- 右移运算符,各二进位全部右移若干位,无符号数高位补0,负数高位补1 。
位运算,第一行为a,b,c三个位运算之后的值,分别为64,63,-1
,最后一个ch:因为是char类型,它的符号位是第八位,127原码为01111111
,1的原码为00000001
,相加后为10000000
,为-128的原码,所以答案为-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)));
}
打印出的值为6
。
func函数的功能是将传入的两个参数相加并返回相加结果
。
通过函数递归和位运算使得倒数第二次函数调用时第一个参数为零
,第二个参数为初始两参数和
。在最后一次调用里返回两初始参数和。
该函数涉及使用位运算实现加法。原理如下
位运算中的异或运算结果与二进制加法的结果是类似的
23异或36的结果是0110011,与加法的差异是第二位没有进位
。
要实现进位,我们可以加上两数与之后左移一位
的结果,23和46与之后左移一位是0001000。与异或结果相加和二进制加法结果0111011相同。
而如果我们可以使用加号的话,还不如直接两数相加,所以我们使用纯粹的位运算
来计算两数和。
如何使两数的异或结果和与后左移一位结果相加呢,我们不妨使用函数的递归,把这两个参数传给实现加法的这个函数本身
,递归以什么条件停止呢,直到最新的这两个参数相加时不用进位停止
,这个时候,第一个参数为0
。.
这就是这个函数实现的原理。
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;
}
实现如下
typedef int (*Predicate)(int);//定义一个函数指针类型
int* filter(int* array, int length, Predicate predicate,//传参时通过函数指针传递回调函数
int* resultLength)/*补全函数*/
{
int x = 0;//新数组的下标
int *filteredNumbers = (int*)malloc(10 * sizeof(int));//为新数组分配空间
for (int j = 0; j <= length; j++)//遍历原数组
if (predicate(array[j]))//过滤条件
{
filteredNumbers[x] = array[j];//符合条件的原数组被填入新数组
x++;//指向新数组下一个元素
}
*resultLength=x;//通过指针将新数组长度传递给主函数
return filteredNumbers;
}
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;
}
在这个函数实现时,我们将ispositive函数作为参数传递给filter函数,需要使用回调函数
,通过函数指针来传递函数。
使用typedef函数定义函数指针类型
更方便的使用了函数指针
7.静…态…
① 如何理解关键字 static?
② static 与变量结合后有什么作用?
③ static 与函数结合后有什么作用?
④ static 与指针结合后有什么作用?
⑤ static 如何影响内存分配?
变量从定义开始分配存储单元,到运行结束存储单元被回收,整个过程称为变量生命周期。
-
*static关键字可以将变量
静态化
,以static关键字修饰的值会被存储在静态区
。生命周期会延长
到整个程序。 -
与局部变量结合可以使它的
生命周期扩展到整个程序
。 -
与全局变量结合会让全局变量的
链接属性
改变,使它的作用域变小到本文件。 -
指针也是一种变量。
-
与函数结合也会使函数的
链接属性
改变,使其不能在其他文件中被调用。*
8.救命!指针!
数组指针是什么?指针数组是什么?函数指针呢?用自己的话说出来更好哦,下面数据类型的含义都是什么呢?
int (*p)[10];
const int* p[10];
int (*f1(int))(int*, int);
- 数组指针是
指向数组的指针
,本质是指针 - 指针数组是里面
元素都是指针的数组
本质是数组 - 函数指针是
指向函数的指针
本质是指针 - 第一个是
数组指针
,因为[]的优先级比\*高
,所以用括号括起来保证了p先和\*结合
,使得其本质为指针
,但是指向一个有十个int类型元素的数组
- 第二个是
指向const的指针数组
,p先和[]结合,则其本质为数组
,数组有十个元素,都是指向const的指针
,即指向的值无法被改变的指针。 - 第三个是一个
函数指针
,这个函数指针指向的函数是返回值为in
t,两个参数第一个是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;
}
考察argc和argv的含义以及短路与和短路或
argc
被程序唤起的命令行参数的数量,用于统计参数数量
argv
是一个指向一个指针数组argv[]
的指针,且这个数组储存的最后一个指针是空指针
,即NULL
。
指针数组argv[]
中的每个指针都指向一个参数
。每个参数都是一个字符串
,第一个参数是程序的路径地址
,
当然,作为一个字符串,其后是有\0的。
短路与&&
在前面的表达式为假
时,不会计算后面的表达式,
短路或||
在前面的表达式为真
时,不会计算后面的表达式。
输出分析
分析i,j,k的值,++和--++和--的运算符优先级 高于&&和||
,
所以i的值先使&&短路
,
然后i增为0,
而j的值因为argc溢出变为零
,
||没有短路
,所以j和k都递增,j为1
,k为2
。
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);
}
宏定义
宏定义是预处理命令的一种,在C语言源程序用一个标识符来表示一个字符串。
宏定义在被预处理时是直接替换,所以一定要注意括号的使用。
考察宏定义
if语句中,16/2×2×2的值为32而非2,所以执行else语句,
预处理时宏定义会被直接替换,所以尽量给参数都带上括号,否则可能结果和预计不符。
替换后MAGIC_CAL(++nums, 2)
变为++nums×+++nums++×++nums+2×2×2,
++运算符优先级更高
所以nums先被递增为4
,然后运算,结果为72
。
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;
}
考察深浅拷贝
*结构体的模板没有指针时,可以直接拷贝,但当结构体的模板有指针时,直接拷贝的话只拷贝了指针指向的地址
,这使得原指针和新指针指向同一个内存空间
,如果以后用原指针对地址进行操作可能导致新指针出现问题,需要重新分配内存空间拷贝
。
本题代码中有一个函数给两个成员都分配了内存,但s1=s2使得内存空间只用了一个,所以,用strcpy
将名字拷贝即可,或者只是不想弹出warning的话,只free一次空间也行,解决代码如下:*
1.
struct Student {
char* name;
int age;
};
void initializeStudent(struct Student* student, const char* name,int age) {
student->name = (char*)malloc(sizeof name + 1);
strcpy(student->name,name);
student->age = age;
}
int main(void) {
struct Student s1, s2;
initializeStudent(&s1, "Tom\0", 18);
initializeStudent(&s2, "Jerry\0", 28);
s1.age=s2.age;
strcpy(s1.name, s2.name);
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;
}
struct Student {
char* name;
int age;
};
void initializeStudent(struct Student* student, const char* name,
int age) {
student->name = (char*)malloc(sizeof(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);
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);
}
输出为Welcome to XUPT , welcome to Xiyou Linux Group [2023]
考察大小端和结构体的内存对齐
大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
x86系统采用小端
的模式,所以在int类型被转化为char类型时,从后面截断。比如arr[0],依次被截断成3c,f2,0f,59。参照ASCII码表,得出结果正是输出值。
13.GNU/Linux (选做)
注:嘿!你或许对 Linux 命令不是很熟悉,甚至你没听说过 Linux。但别担心,这是选做题,了解
Linux 是加分项,但不了解也不扣分哦!
你知道 cd 命令的用法与 / . ~ 这些符号的含义吗?
请问你还懂得哪些与 GNU/Linux 相关的知识呢~
cd命令是切换工作目录
的命令。
/是根目录
,.是当前目录
,~是主目录
,以根目录来看是/home/user,这里的user替换为用户的用户名
。