Skip to content

Latest commit

 

History

History
464 lines (393 loc) · 21.7 KB

2023.md

File metadata and controls

464 lines (393 loc) · 21.7 KB

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

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

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

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

· Q :

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

· A :

  • 此题考察的知识点为二进制的灵活应用(至少需要10只小鼠);
  1. 首先将1000瓶水按二进制分别编号;
  2. 观察得最大位数为10;
  3. 取10只小鼠分别编号为1~10;
  4. 将对应位数为1的水喂给相应编号的小鼠;
  5. 将死去的小鼠编号记下,用二进制相应位拼凑得到有毒的那瓶水;

1. 先预测一下~

· Q :

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

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

· A :

  1. 首先函数要求返回的类型为(char*);
  2. 解决方法:
    1. 直接返回自己的姓名字符串return "自己的姓名";
    2. 定义一个指针字符串并返回(char *name = "自己的姓名");

2. 欢迎来到Linux兴趣小组

· Q :

有趣的输出,为什么会这样子呢~

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

· A :

  1. ptr0为一个指针字符串(是常量不可修改),ptr1为一个字符串数组(可后续修改);
  2. 对两个字符串解引用,结果均为第一个字母,故if语句条件成立;
  3. printf函数嵌套
    • 先打印最内层函数,因为最内层为空,故不打印并返回0;
    • 第二层函数接收返回值0并打印"Hello, Linux Group - 20",且此字符串长度为23,故返回23;
    • 最外层函数接收返回值23并打印;
  4. 则最后输出结果为Xiyou Linux Group - 2023

3. 一切都翻倍了吗

· Q :

请尝试解释一下程序的输出。
请谈谈对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));
}

· A :

  1. 程序的输出解释:

    • 第一个sizeof运算符内首先对arr字符串进行取地址,得到arr字符串的地址,后对&arr进行解引用,相当于arr,而一个字符占一个字节,共七个字符,故返回值为7
    • 第二个sizeof运算符内arr + 0,arr为字符串的地址,加0相当于字符串第一个元素的指针,在x64环境下,返回值为8
    • 第三个sizeof运算符内返回值为num的类型,它的类型为short,故返回2
    • sprintf函数内,将num的值转换为十六进制并储存在str字符串中,因字符串与num的值不相等,故返回0
    • 第一个strlen函数内,对str[0]取地址并+1,表示从字符串数组第二个元素开始计算长度,由sprintf函数得str内为"0x208",故返回4
    • 第二个strlen函数内,arr + 0为arr字符串内首元素的地址,故返回值为5
  2. sizeofstrlen的理解:

    • sizeof是一个运算符,计算时连带'\0',且可用类型做参数(计算结果为占空间内存字节数)
    • strlen是string.h中的一个函数,当计算到'\0'时停止,且只可用char*做参数(计算结果为字符串长度)
  3. sprintf的理解:

    • sprintf函数用于将所定义的变量类型装入所给字符串数组中;
    • 括号内依次为所存目的字符串,字符串本身以及所要存入的变量的类型,变量名称;
  4. 补充说明:

    • 各类型变量所占字节数:char 1; short 2; int 4; long 4; float 4; long long 8; double 8;

4. 奇怪的输出

· Q :

程序的输出结果是什么?解释一下为什么出现该结果吧~

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

· A :

- 此题考察位运算

  • (十进制的数均会转换为二进制并转换为补码进行后续运算)(正数的原、反、补码相同;负数的原码符号为为1,其余位计算规则与正数相同;负数的反码符号为不变,其余位据原码按位取反;负数的补码为反码加一):
    1. '&'为按位与,当二进制位都为1时结果为1,否则为0,故a的值为64
    2. '^'为按位异或,当二进制位不同时结果为1,否则为0,故b的值为63
    3. '>>'为右移操作符,表示将操作数的二进制位向右移动指定的位数,右移1位相当于除以2(注:无符号整数移动后高位补0,有符号整数移动后高位均补符号位),故c的值为-1;
    4. 综上所述,ch的值为-128;
    5. 补充说明:
      • char类型的变量范围为-128~127;
      • 对有符号整数和无符号整数的范围解释:
        • --注释:(此处为人工智能解答)
      1.  无符号整型范围
        • 对于n位无符号整型,它的所有位都用于表示数值大小。最小值是所有位为0,即0;最大值是所有位为1。
        • 例如8位无符号整型,最大值对应的二进制是11111111,转换为十进制就是2^8 - 1=255。一般地,n位无符号整型的范围是0到2^n - 1。
      2.  有符号整型范围
        • 对于n位有符号整型,最高位是符号位(0表示正,1表示负),剩下n - 1位用于表示数值。
        • 正数最大值是符号位为0,其余位为1,例如8位有符号整型中,最大正数对应的二进制是01111111,转换为十进制是2^7 - 1 = 127。
        • 负数最小值是符号位为1,其余位为0(以补码形式存储),例如8位有符号整型最小值对应的二进制(补码)是10000000,转换为十进制是-2^7=-128。一般地,n位有符号整型的范围是-2^{n - 1}到2^{n - 1}-1。

5. 乍一看就不想看的函数

· Q :

“人们常说互联网凛冬已至,要提高自己的竞争力,可我怎么卷都卷不过别人,只好用一些奇技淫巧让我的代码变得高深莫测。”

这个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)));
}

· A :

- 此题考察递归函数和位运算的实际应用

  1. 首先观察func函数易发现,这是个递归函数,目的为计算a与b的和;
  2. if语句为若a为0,则返回b;
  3. 下一步的返回值为fuc函数,函数括号里第一个数为a与b的进位部分所得数,第二个数为a与b不进位加法所得数;
  4. 经过一次次递,直至a的值为0,开始进行归(也就是返回值开始逐步运算);
  5. 经过计算,得到此代码的输出值为6

6. 自定义过滤

· Q :

请实现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;
}

· A :

typedef int (*Predicate)(int);
int *filter(int *array, int length, Predicate predicate,
            int *resultLength)
{
    int *numbers = malloc(length * sizeof(int));//为number指针数组分配适当内存大小
    int j = 0;
    for(int i = 0;i < length; i++)
    {
        if(isPositive(array[i]))//判断是否符合筛选条件
        {
            numbers[j] = array[i];//将符合条件的数存入指针数组
            j++;
        }
    }
    *resultLength = j;//将resultLength整型变量定义值为指针数组的长度
    return numbers;//返回指针数组
}
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. 静…态…

· Q :

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

· A :

  • 此题考察static的理解与应用
  1. 可控制变量或函数的作用域链接属性
  2. 变量结合:
    1. 与局部变量:
      • 静态存储持续性,不会像一般局部变量在函数调用结束后消失;
      • 作用域与一般局部变量相同,不能被其他函数直接访问并应用;
      • 再次调用时会在上一次运算基础上计算,而不会重新初始化;
    2. 与全局变量:
      • 作用域被限制在所定义的文件内,其余文件均无法引用此变量;
      • 从外部链接属性变为内部链接属性
  3. 函数结合:
    • 同样改为内部链接属性
    • 作用域被限制在定义文件内部,其余文件均无法引用此函数;
  4. 指针结合:
    • 由于指针本质上也为变量,故与指针结合等同于与变量结合(同样也区分全局与局部);
  5. static对内存分配的影响:
    • static关键字修饰后,会储存在静态储存区,在整个程序运行过程中均存在;而不被修饰的,则储存在栈区,进函数时存在,出函数后销毁;

8. 救命!指针!

· Q :

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

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

· A :

  • 此题考察指针的概念与应用
  1. 首先int (*p)[10]是一个数组指针,它本质上是一个指针,而这个指针指向一个特定的数组;
  2. const int* p[10]是一个指针数组,其内部各元素均为一个指针,指向一个常量整型;
  3. int (*f1(int))(int*, int)首先为一个函数指针,返回函数的地址,函数f1的返回值为int类型的整型,函数内部接收两个参数,一个为int类型的指针,一个为整型;

9. 咋不循环了

· Q :

程序直接运行,输出的内容是什么意思?

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

· A :

  • 此题考察argc、argv以及前(后)置++与逻辑操作符
  1. argc是一个整数,表示命令行参数的个数(包括程序名本身);argv是一个字符指针数组,每一个元素都是一个指向字符串的指针,字符串为命令行参数(argv[0]表示程序名);
  2. 因为涵盖程序名本身,故argc起始值至少为1;
  3. while循环内,实现使argc由本身的值开始前置++直到为0循环终止;
  4. 后置++为先使用变量后+1,则i++判断为真,向后执行,j为0,故逻辑与判断为假;逻辑或则继续向后,k不为0,则逻辑或为真;
  5. 执行完上述操作后,i为0,j为1,k为2;

10. 到底是不是TWO

· Q :

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

· A :

  • 此题考察宏定义与变量定义的作用域
  1. 首先有两个宏定义,一个将传入的参数进行三次方计算,另一个将参数a与b传入第一个宏并相加(注意:宏运算时为直接替换);
  2. 主函数内,定义一个变量nums并初始化为1,if语句条件内部转义过来就是16 / 2 * 2 * 2,结果不为2,则进入else语句中;
  3. 在else语句内部定义一个新的变量nums,而次变量出else语句后会被销毁,故输出值nums仍为1;

11. 克隆困境

· Q :

试着运行一下程序,为什么会出现这样的结果?

直接将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;
}

· A :

  • 此题考察对<malloc函数的理解与使用和对结构体的简单运用
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 restart(struct Student *s1,const struct Student *s2)
{
    free(s1->name);
    s1->name = (char *)malloc(strlen(s2->name) + 1);
    if(s1->name != NULL)
    strcpy(s1->name,s2->name);
}
int main(void) {
    struct Student s1, s2;
    initializeStudent(&s1, "Tom", 18);
    initializeStudent(&s2, "Jerry", 28);
    s1.age = s2.age;
    restart(&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时,s1与s2所指向的内存变为相同,而不仅仅是值的改变,所以当程序执行到末尾时释放s1的动态内存,s2仍然指向此块内存,此时s2变为悬挂指针(野指针),可能会对系统造成伤害;
  • 解决办法与思路说明:
    • 思路:自定义一个函数,使s1结构体内的name只将值改变为s2中name的值,而不改变其内存指向(注意判断是否分配内存成功)(注意使用完后用free来释放内存);
    • 方法:如上述代码所示;

12. 你好,我是内存

· Q :

作为一名合格的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);
}

· A :

  • 此题考察大小端与结构体对齐数
  1. 结构体内最长类型为8字节,故对齐数为8;
  2. 输出中将arr强行转化为struct structure结构体类型指针,并指向联合体node.string;
    • 注意联合体所占内存为最大成员字节数,本应为11,但它位于结构体内,考虑内存对齐,联合体所占字节大小为16;
  3. 指针指向联合体,因为结构体内对齐数为8,故输出时应跳过前两个十六进制的数值从第三个开始输出;
    • 注意十六进制下 一位数占4个bite位,故此题中一个十六进制的数占四个字节;
  4. 大多电脑为小端储存,故高位字节存于高地址,低位字节存于低地址,由于电脑从低地址开始接收,所以将int类型的数截断为char类型后(如0x636c6557变为0x57,0x65,0x6c,0x63),应在每段从右向左依次输出,且字符串输出之至'\0'停止;
    • 注意输出的字符为将十六进制的数转换为十进制的数后对应ASCII表上的字符;

13. GNU/Linux (选做)

· Q :

注:嘿!你或许对Linux命令不是很熟悉,甚至你没听说过Linux。但别担心,这是选做题,了解Linux是加分项,但不了解也不扣分哦!

你知道cd命令的用法与 / . ~ 这些符号的含义吗?
请问你还懂得哪些与 GNU/Linux 相关的知识呢~

· A :

  1. 在Linux系统里, cd 命令用于切换当前工作目录

    基本用法:

    1.  cd 后接目标目录路径,如 cd /home/user 能进入 /home/user 目录。若目标目录在当前目录下,可直接写相对路径,像 cd Documents 。
    2. cd 命令还有一些特殊用法。 cd - 可在当前目录和上一次所在目录间切换,这在两个目录频繁操作时很方便。 cd.. 是回到上一级目录, cd../.. 则是回两级目录,以此类推。若不加参数直接用 cd ,会进入用户主目录,等同于 cd ~ ,在不同用户操作或回到默认工作环境时会用到。
  2. 在Linux系统中:

    • “/”是根目录的符号。它是整个文件系统的起始点,所有的文件和目录都在根目录下以层次结构组织起来。比如“/bin”是存储二进制可执行文件的目录,“/etc”存放系统配置文件。

    • “.”表示当前目录。在执行命令或指定路径等操作时,如果用到“.”,就代表操作是在当前所在目录进行。例如,“./script.sh”表示运行当前目录下的“script.sh”脚本。

    • ”代表当前用户的主目录。每个用户在系统中有一个自己的主目录,用于存放个人文件等。例如,用户“user1”登录后的主目录可能是“/home/user1”,使用“”就可以方便地引用这个目录,像“cp file1 ~”就是把“file1”复制到当前用户的主目录下。

(注:关于linux的答案为查阅人工智能所得)

结语

恭喜你攻克所有难关!迎难而上的决心是我们更为看重的。
来到这里的人已是少数,莫踌躇在成功的门槛前。
自信一点,带上你的笔记本电脑,来东区逸夫楼FZ103面试吧!