西邮Linux兴趣小组2023纳新面试题浅析

学长寄语:长期以来,西邮Linux兴趣小组的面试题以难度之高名扬西邮校内。我们作为出题人也清楚的知道这份试题略有难度。请你动手敲一敲代码。别担心,若有同学能完成一半的题目,就已经十分优秀。其次,相比于题目的答案,我们对你的思路和过程更感兴趣,或许你的答案略有瑕疵,但你正确的思路和对知识的理解足以为你赢得绝大多数的分数。最后,做题的过程也是学习和成长的过程,相信本试题对你更加熟悉地掌握C语言一定有所帮助。祝你好运。我们东区逸夫楼FZ103见!

  • 本题目只作为西邮Linux兴趣小组2022纳新面试的有限参考。
  • 为节省版面,本试题的程序源码省去了#include指令。
  • 本试题中的程序源码仅用于考察C语言基础,不应当作为C语言「代码风格」的范例。
    题目难度随机排列。
  • 所有题目编译并运行于x86_64 GNU/Linux环境。

0.鼠鼠我啊,要被祸害了

有 1000 瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水,24 小时后就会准时死亡。
至少要多少只小白鼠才能在 24 小时内鉴别出哪瓶水有毒?

二进制来表示,将每只小白鼠喝与不喝看作1和0,这样n个小白鼠就会有2n种可能,每一种可能对应一瓶水,当有10只小白鼠时,210 = 1024 > 1000 1024 > 1000 1024>1000,所以至少为10只小白鼠。


1.先预测一下~

按照函数要求输入自己的姓名试试~

char* welcome() {
    //请你返回自己的名字
}
int main(void) {
    char* a = welcome();
    printf("Hi, 我相信 %s 可以面试成功!\n", a);
    return 0;
}

考点:函数的返回值和指针与数组的基本运用
可以看到,这个函数的返回值类型为char类型的指针

#include<stdio.h>
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);
}

考点:对指针与数组的理解和printf的返回值
首先我们应该明确一点,两个“Welcome to Xiyou Linux!”并不是同一个字符串 ,第一个是字符串字面量,储存在静态区,第二个是数组复制的副本,储存在栈中,所以两个指针指向的地址不相同,但 *ptr0 == *ptr1为真,这是因为对这两个指针解引用后同为W,所以为真。所以执行if语句。
printf的返回值为正确输入的字符,这个语句的执行逻辑时由内而外的,先从最里面的printf开始,因为无输入字符,所以返回值为0,第二个printf输入的字符就变为0,写入的字符个数为23个,所以最后输出为Xiyou Linux Group - 2023
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()strlen()的深度理解,对sprintf()的基本运用(\t是tab的转义字符,意在美化输出)
这里需要注意的是,sizeof()数组在一起时有一些特殊用法,如果不清楚的话,建议去详细查询一下。
sizeof(*&arr):可以理解为&arr是int(*)[4]类型的数组指针,如果解引用,访问的就是4个char的数组,大小是7个字节。
sizeof(arr + 0) : 这里的arr + 0 表示的是地址,也可理解为指针,而指针的大小在64位的系统下为8个字节。
sizeof(num = num2 + 4) : 这里sizeof()所关注的是num,而num是short类型的,所以为2个字节。
sprintf的作用是将num的值转换为16进制,以字符串的形式存储,所以sprintf(str, "0x%x", num) == num为假,返回值为0.
strlen(&str[0] + 1) : str是字符串“0x208”,&str[0] + 1指向了’x’,strlen识别的’\0’,所以值为4。
strlen(arr + 0) : arr + 0表示arr数组的首地址,所以值为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类型,有符号位的8位bit,在进行位运算时,不要忘记转换成补码进行运算,负数的补码要格外注意,运算完后,不要忘记在转换回原码,转成10进制后,最后注意是否超出char类型范围才是最终答案。

//64的原码 0100 0000(正数的原码,反码,补码都一样)
//64的反码 0100 0000
//64的补码 0100 0000
//-64的原码 1100 0000
//-64的反码 1011 1111(除符号位,与原码相反)
//-64的补码 1100 0000(在反码的基础上+1)
//127的原码 0011 1111
//127的反码 0011 1111
//127的补码 0011 1111
//&——全为1才是1
//^——两数相同为0,相异为1
//>>——各二进制位全部右移若干位,对无符号位数,高位补0,有符号位数,高位补1
//最后可得a = 64 b = 63 c = -1
//ch = a + b - c = 128
//因为char范围为-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)));
}

考点:对递归的理解和位运算的奇妙运用
乍一看感觉很麻烦,要是硬算就吃亏了。我们可以先运行一下,结果为6,这时我们发现a + b + c也为6。
这是巧合吗?很明显不是。我们要知道,在计算机底层中是没有加减乘除的,只有位运算,于是先人用位运算模拟出加减乘除。我第一次学到这里时,不由得感叹先人的智慧,在这里我浅析一下位运算模拟加法的思路,希望同学们能去详细了解其中的奥妙。
a ^ b这一步进行异或运算时,你会发现其实是进行了a + b的无进位运算。
a & b这一步是为了获取详细的进位信息,后面的<< 1是为了将进位信息匹配到正确的地方。
当没有进位信息时,也就完成了整个的模拟加法运算。
所以,最后的结果为a + b + c
(除了加法,位运算还能模拟减法,乘法,除法。希望各位同学去了解一下)

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与函数指针的特殊用法与运用,malloc的运用和注意事项。
这串代码想实现的是将非正数过滤出去,留下整数。
再使用malloc函数时,一定要时刻关注两点:内存和指针。操作不当有可能内存泄漏和悬空指针的产生。

#include <stdio.h>
#include <stdlib.h>

typedef int (*Predicate)(int);//特殊用法,可以直接使用Predicate来代替int (*Predicate)(int)

int *filter(int *array, int length, Predicate predicate, int *resultLength) {
    int *result = (int *)malloc(length * sizeof(int));//因下面的free函数推测出用malloc进行内存分配
    int count = 0;
    for (int i = 0; i < length; i++) {
        if (predicate(array[i])) {
            result[count++] = array[i];//将整数存起来
        }
    }
    *resultLength = count;
    return result;
}

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;
}

7.静…态…

① 如何理解关键字 static?
② static 与变量结合后有什么作用?
③ static 与函数结合后有什么作用?
④ static 与指针结合后有什么作用?
⑤ static 如何影响内存分配?

考点:对static的应用和对生命周期和内存储存的理解

① 关键字static其主要作用是改变作用对象的作用域和生命周期。
② 当static与变量结合时,它会改变变量的作用域为文件作用域,即该变量只能在该文件中访问,对其他文件不可见。而且,static还会延长变量的生命周期,使其在程序运行期间一直存在,而不是在函数调用结束后被销毁。
③ 当static与函数结合时,它会改变函数的作用域为文件作用域,使得该函数只能在该文件中调用,对其他文件不可见。
④ 当static与指针结合时,它可以用于定义静态指针。静态指针的生命周期与程序的运行周期相同,不会随着函数的调用结束而销毁,可以在多个函数之间共享数据。
static关键字对内存分配有两种影响。对于静态变量和静态数组,它们在程序运行期间一直存在于静态存储区,不会随着函数的调用结束而销毁。对于静态局部变量,它们在程序运行期间被储存到静态存储区,不会因块作用域的消失而销毁。

8.救命!指针!

数组指针是什么?指针数组是什么?函数指针呢?用自己的话说出来更好哦,下面数据类
型的含义都是什么呢?

    int (*p)[10];
    const int* p[10];
    int (*f1(int))(int*, int);

考点:对指针,函数和数组的深层理解
int (*p)[10]是一个指针数组,指装了10个指针的数组。
const int* p[10]const指不能修改指针所指向的内容。
int (*f1(int))(int*, int)这个就比较复杂了,但是我们要冷静下来,一步步分析。
首先,我们能看出来这个框架是一个函数A指针,这个函数A带有两个参数,而奇怪的地方是函数名也带有一个参数,所以f1(int)应该是一个函数B,而这个函数B返回的是函数名,这就能解释通了。

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;
}

考点:argcargv的基本理解,数据类型范围的理解和逻辑运算符的应用
argc表示传递给程序的命令行参数的数量,它至少为1,因为第一个参数始终是程序的名称。
argv是一个指向字符串数组的指针,每个字符串表示一个命令行参数。
因为argc初始值为1,所以一直递增,一直到231-1,越界成-231,继续递增到0时停止循环。所以此时argc为0。
下面的逻辑运算符我们需要知道:
若&&左边条件成立右边也要执行,若左边条件不成立,右边条件不用执行。
若 || 左边条件成立右边不用执行,若左边条件不成立,右边条件需要执行。
所以 i = 0 ,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);
}

考点:宏定义的基本运用和块作用域的理解
宏定义我认为最重要的一点就是绝对替换,不要自作主张的加括号。快准狠就绝定没问题。
所以16 / CAL(2) = 16 / 2 * 2 * 2 = 32 != 2 所以条件为假。
执行else语句,而在此语句中对nums进行了重定义,等离开这个块作用域就会销毁。
所以nums最后为1(else语句中的多个++行为是未定义行为,不同编译器结果不同)

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;
}

考点:对结构体的基本了解,深拷贝与浅拷贝的理解和使用malloc的注意事项
问题一:释放内存时,只释放了s2的内存,s1的没有释放。
问题二:可能造成悬空指针的产生,要将指针指向NULL。
题中为浅拷贝,让两个指针指向了一个地方,导致只能释放s2的动态内存。

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 copy(struct Student *s1, const struct Student *s2) {
    strcpy(s1->name, s2->name);//深拷贝,直接将堆里的数据改变
    s1->age = s2->age;
}

int main(void) {
    struct Student s1, s2;
    initializeStudent(&s1, "Tom", 18);
    initializeStudent(&s2, "Jerry", 28);
    copy(&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);//这样能释放两个动态内存了
    s1.name = NULL;//以防万一,将用完的指针指向空指针。
    s2.name = NULL;
    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);
}

考点:结构体与联合体的应用,对内存对齐的理解,大小端的判断和十六进制和二进制之间的关系
以上的知识由于谈起来篇幅过长,我就不加以赘述,希望同学们能自行搜索,我就直接开始解题。
首先我们看到先将arr强制转换为struct structure*类型,将整型数组解释为一个结构体指针。然后,通过访问结构体指针的node.string成员,将其作为字符串打印出来。这时,最关键的事情来了,是如何打印的呢?
我们可以观察到,结构体中最大类型的字节数为8(long longvoid *pointer),首先是一个int类型,占了4个字节,而0x590ff23c正好为4个字节的长度,由于对齐数为8,而且接下来为char类型的数组,所以为了内存对齐要在空出4个字节(0x2fbc5a4d),到了0x636c6557,由于大部分电脑都是小端储存,所以就从后往前的读取数据。
0x57,0x65,0x6c,0x63……等等便将Welcome to XUPT , welcome to Xiyou Linux Group [2023]打印了出来。

13.GNU/Linux (选做)

注:嘿!你或许对 Linux 命令不是很熟悉,甚至你没听说过 Linux。但别担心,这是选做题,了解
Linux 是加分项,但不了解也不扣分哦!
你知道 cd 命令的用法与 / . ~ 这些符号的含义吗?
请问你还懂得哪些与 GNU/Linux 相关的知识呢~

单独一个cd 表式切换到当前用户的主目录
cd …表示切换到上一级目录
cd [目录名称] 表示切换到该目录
cd ~ 表示进入用户主目录
cd - 返回进入此目录之前所在的目录

结语

西邮Linux兴趣小组本着 open free share 的理念,愿意与大家分享知识,交流知识。如果我的文章哪里有什么问题希望与我共同交流,共同进步。(●’◡’●)ノ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

marsevilspirit

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值