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

西邮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为例)的方式,我在这里能提供这几种:

  1. return "noregret";

  2. static char *str="noregret";

    return str;

    这里我用static的原因是str是局部变量,局部变量在执行完后会立即清除,不会一直占用内存,因此我如果直接用return str,结果将会出现乱码。而我用static修饰局部变量将会延长它的生命周期到整个程序。

  3. 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);
  1. 数组指针:本质为一个指针,指向一个数组

  2. 指针数组:本质为一个数组,该数组存放指针

  3. 函数指针:本质为一个指针,指向一个函数

  4. 这里我们将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:指向常量的常量指针,表示指针存放的地址不能改变,指向的值也不能改变。

  5. 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的指针,然后用"->"运算符访问结构体中联合体nodestring。因为在结构体中,变量string前还有两个int类型的变量,一个int类型的变量占4个字节,因此在数组arr中,从第三个数开始才指向node.string。**计算机一般采用小端存储模式。**即从0x636c6557中的57开始翻译,对照ASCII码表,一直翻译到’\0’,结束翻译。
    这个输出结果是很有意义的,让我们来看一下:

Welcome to XUPT , welcome to Xiyou Linux Group [2023]

恭喜你做到这里!你的坚持战胜了绝大多数看到这份试题的同学。
或许你自己对答题的表现不满意,但别担心,请自信一点呐。
坚持到达这里已经证明了你的优秀。
还在等什么,快带上你的笔记本电脑,来FZ103面试吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值