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

前言

  • 本题目只作为 Xiyou Linux兴趣小组2022纳新面试的有限参考。

  • 为节省版面,本试题的程序源码省去了#include指令。

  • 本试题中的程序源码仅用于考察C语言基础,不应当作为C语言「代码风格」的范例。

  • 题目难度随机排列。

  • 所有题目编译并运行于x86_64 GNU/Linux环境。

  • 学长寄语:
    长期以来,西邮Linux兴趣小组的面试题以难度之高名扬西邮校内。我们作为出题人也清楚的知道这份试题略有难度。请别担心。若有同学能完成一半的题目,就已经十分优秀。 其次,相比于题目的答案,我们对你的思路和过程更感兴趣,或许你的答案略有瑕疵,但你正确的思路和对知识的理解足以为你赢得绝大多数的分数。最后,做题的过程也是学习和成长的过程,相信本试题对你更加熟悉的掌握C语言的一定有所帮助。祝你好运。我们FZ103见!
  • Copyright © 2022 西邮Linux兴趣小组, All Rights Reserved.
    本试题使用采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 link 进行许可。

0. 我的计算器坏了?!

  • 2^10 = 1024对应于十进制的4位,那么2^10000对应于十进制的多少位呢?

计算2^10000对应于十进制的位数,使用对数的性质来估算。

位数 = floor(lg(2^10000)) + 1
floor表示向下取整,+1用于考虑位数最左侧的1位。

2^10000的位数:
位数 = floor(lg(2^10000)) + 1
位数 = floor(10000 * lg(2)) + 1

lg(2) 约为 0.3010
位数 = floor(10000 * 0.3010) + 1
位数 = floor(3010) + 1
位数 = 3010 + 1
位数 = 3011

所以,2^10000对应于十进制的位数为3011位。

1. printf还能这么玩?

  • 尝试着解释程序的输出。
int main(void) {
    if ((3 + 2 < 2) > (3 + 2 > 2))
        printf("Welcome to Xiyou Linux Group\n");
    else
        printf("%d\n", printf("Xiyou Linux Group - 2%d", printf("")));
}

1.条件语句:
(3 + 2 < 2) 为 0,5 不小于 2。
(3 + 2 > 2) 为 1, 5 大于 2。
条件 (0 > 1) 是 假,所以程序执行 else 块。

2.else块printf 嵌套:
printf("") 打印一个空字符串,但printf 返回打印的字符数,因此返回0接下来是 printf(“Xiyou Linux Group - 2%d”, 0),这里 %d 需要一个整数参数来替代。在这种情况下,整数参数是0。所以它将打印 “Xiyou Linux Group - 20”。

3.最终,printf("%d\n", 22) 打印 “22”。

Xiyou Linux Group - 2022

2. 你好你好你好呀!

  • 程序的输出有点奇怪,请尝试解释一下程序的输出吧。
  • 请谈谈对sizeof()及strlen()的理解吧。
int main(void) {
    char p0[] = "Hello,Linux";
    char *p1 = "Hello,Linux";
    char p2[11] = "Hello,Linux";
    printf("p0 == p1: %d, strcmp(p0, p2): %d\n", p0 == p1, strcmp(p0, p2));
    printf("sizeof(p0): %zu, sizeof(p1): %zu, sizeof(*p2): %zu\n",
           sizeof(p0), sizeof(p1), sizeof(*p2));
    printf("strlen(p0): %zu, strlen(p1): %zu\n", strlen(p0), strlen(p1));
}

代码创建了三个字符数组:p0、p1 和 p2。

p0 是一个字符数组,分配了足够的空间来存储字符串 “Hello,Linux” 及其空字符结束符\0。

p1 是一个指向常量字符串 “Hello,Linux” 的指针。

p2 也是一个字符数组,但是指定了大小为 11,这里的字符串"Hello,Linux" 和空字符结束符总共需要 12 个字符的空间,所以它的大小可能会造成截断。

结果:

p0 == p1 打印为 0,因为它们是不同的类型: p0 是一个字符数组,而 p1 是指向常量字符串的指针。

strcmp(p0, p2) :当读到p0的\0时,此时p2的下一个字符为内存中的随机值,结果不定。

sizeof(p0) 打印为 12,因为 p0 是一个包含 “Hello,Linux” 及其空字符结束符的字符数组。

sizeof(p1) 打印为指针的大小8,不会受到指针指向的字符串长度的影响。

sizeof(*p2) 打印字符’H’的大小1。

strlen(p0) 打印为 11,因为它返回字符串长度,不包括空字符结束符。

strlen(p1) 打印为 11,因为它也返回指向的字符串的长度,不包括空字符结束符。

3. 换个变量名不行吗?

  • 请结合本题,分别谈谈你对C语言中「全局变量」和「局部变量」的「生命周期」理解。
int a = 3;
void test() {
    int a = 1;
    a += 1;
    {
        int a = a + 1;
        printf("a = %d\n", a);
    }
    printf("a = %d\n", a);
}
int main(void) {
    test();
    printf("a= %d\n", a);
}
全局变量:

全局变量是在程序的全局范围内声明的变量,在函数外部声明。int a = 3; 是一个全局变量,它在函数外部声明。全局变量的生命周期从程序启动时开始,一直持续到程序结束。全局变量在整个程序执行期间都是可访问的。

局部变量:

局部变量是在特定函数内部声明的变量,只在该函数的作用域内可见。int a = 1; 是test()函数内的局部变量。局部变量的生命周期从函数被调用时开始,直到函数执行完毕时结束。它们只在函数执行期间可访问,并且在函数执行完毕后将被销毁。

test()函数内有多个不同作用域的局部变量a。在内部最深层的花括号中,使用a = a + 1;,但这里的a在内部作用域中声明,并不影响外部作用域中的a。

总结:

全局变量在整个程序执行期间都可访问,其生命周期与程序的生命周期相同。
局部变量只在其声明的函数内可见,生命周期从函数被调用时开始,直到函数执行完毕时结束。不同作用域内的同名局部变量互相独立,不会相互影响。

4. 内存对不齐

  • union与struct各有什么特点呢,你了解他们的内存分配模式吗。
typedef union {
    long l;
    int i[5];
    char c;
} UNION;
typedef struct {
    int like;
    UNION coin;
    double collect;
} STRUCT;
int main(void) {
    printf("sizeof (UNION) = %zu\n", sizeof(UNION)); 
    printf("sizeof (STRUCT) = %zu\n", sizeof(STRUCT));
}

union 和 struct 都是 用于组合不同类型的数据成员的数据结构。

union :

union 只能存储其中的一个成员的值。
所有成员共享同一块内存,因此 union 的大小等于它最大的成员的大小。
由于所有成员共享内存,改变一个成员的值可能会影响其他成员。
union 适合用于节省内存空间或者在不同的数据类型之间进行转换。

struct :

所有成员在内存中依次排列,占据各自的内存空间。
struct 的大小是所有成员大小之和,可能会因为内存对齐而有所增加。
每个成员可以同时保存值,互不影响。

// 若是4字节对齐
typedef union {
    long l;         // 0 ~ 3
    int i[5];       // 0 ~ 19
    char c;         // 0 ~ 7
} UNION;            // 0 ~ 19 -> 20
typedef struct {
    int like;       // 0 ~ 3
    UNION coin;     // 4 ~ 23
    double collect; // 23 ~ 30
} STRUCT;           // 0 ~ 31 -> 32
// 若是8字节对齐
typedef union {
    long l;         // 0 ~ 3
    int i[5];       // 0 ~ 19
    char c;         // 0 ~ 7
} UNION;            // 0 ~ 23 -> 24
typedef struct {
    int like;       // 0 ~ 3
    UNION coin;     // 8 ~ 27
    double collect; // 32 ~ 39
} STRUCT;           // 0 ~ 39 -> 40


5. Bitwise

  • 请使用纸笔推导出程序的输出结果。
  • 请谈谈你对位运算的理解。
int main(void) {
    unsigned char a = 4 | 7;
    a <<= 3;
    unsigned char b = 5 & 7;
    b >>= 3;
    unsigned char c = 6 ^ 7;
    c = ~c;
    unsigned short d = (a ^ c) << 3;
    signed char e = -63;
    e <<= 2;
    printf("a: %d, b: %d, c: %d, d: %d\n", a, b, c, (char)d);
    printf("e: %#x\n", e);
}

1.unsigned char a = 4 | 7; 执行按位或操作,4的二进制表示是 00000100,7的二进制表示是 00000111,按位或操作得到 00000111,对应的十进制是7。

2.a <<= 3; 左移操作,将a的二进制值 00000111 左移3位,变成 00111000,对应的十进制是56。

3.unsigned char b = 5 & 7; 执行按位与操作,5的二进制表示是 00000101,7的二进制表示是 00000111,按位与操作得到 00000101,对应的十进制是5。

4.b >>= 3; 右移操作,将b的二进制值 00000101 右移3位,变成 00000000,对应的十进制是0。

5.unsigned char c = 6 ^ 7; 执行按位异或操作,6的二进制表示是 00000110,7的二进制表示是 00000111,按位异或操作得到 00000001,对应的十进制是1。

6.c = ~c; 执行按位取反操作,将c的二进制值 00000001 取反,变成 11111110,对应的十进制是254。

7.unsigned short d = (a ^ c) << 3; 首先执行按位异或操作,a的值是56,c的值是254,它们的二进制异或结果是 11000110,这对应的十进制是 198。然后将这个结果左移3位,得到 0000011000110000,对应的十进制是1584。

8.signed char e = -63; 给e赋值为-63。

9.e <<= 2; 左移操作,将e的值 -63 左移2位,得到 4。

a: 56, b: 0, c: 254, d: 48
e: 0x4

6. 英译汉

  • 请说说下面数据类型的含义,谈谈const的作用。
    • char *const p。
    • char const *p。
    • const char *p。
1.char *const p:

声明了一个指向字符(char)类型的常量指针(const指针)。
p是一个常量指针,一旦指向了某个内存地址,就无法再改变p指向的地址。
但可以通过p来修改所指向地址上的字符内容。

2.char const *p:

这是一个指向字符(char)类型的指针,指向的字符是常量。
p是一个指向常量字符的指针,不能通过p来修改所指向地址上的字符内容。
但可以改变p指向的地址,即可以将p指向不同的常量字符。

3.const char *p:

这也是一个指向字符(char)类型的指针,指向的字符是常量。
与2相同,p是一个指向常量字符的指针,通过p来修改所指向地址上的字符内容。
同样,它可以改变p指向的地址,即可以将p指向不同的常量字符。

7. 汉译英

  • 请用变量p给出下面的定义:
    • 含有10个指向int的指针的数组。
    • 指向含有10个int数组的指针。
    • 含有3个「指向函数的指针」的数组,被指向的函数有1个int参数并返回int。
int* p[10]; 

int (*p)[10]; 

int (*p[3])(int);

8. 混乱中建立秩序

  • 你对排序算法了解多少呢?
    请谈谈你所了解的排序算法的思想、稳定性、时间复杂度、空间复杂度。

提示:动动你的小手敲出来更好哦~

冒泡排序(Bubble Sort):

思想:通过相邻元素的比较和交换来将最大(或最小)的元素逐步"冒泡"到列表的末尾。

void BubbleSort(int arr[], int len)
{
    int i, j, tmp;
    for (i = 0; i < len - 1; i++)
        for (j = 0; j < len - 1 - i; j++)
            if (arr[j] > arr[j + 1])
            {
                tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
}

选择排序(Selection Sort):

思想:通过在未排序部分选择最小(或最大)的元素并将其放在已排序部分的末尾。

void SelectionSort(int arr[], int len)
{
    int i, j,min;
    for (i = 0; i < len - 1; i++)
    {
         min = i;
        for (j = i + 1; j < len; j++)
            if (arr[j] < arr[min])
                min = j;
        int tmp = arr[min];
        arr[min] = &arr[i];
        &arr[i] = tmp;
    }
}

9. 手脑并用

  • 请实现ConvertAndMerge函数:
    拼接输入的两个字符串,并翻转拼接后得到的新字符串中所有字母的大小写。

提示:你需要为新字符串分配空间。

char* convertAndMerge(/*补全签名*/);
int main(void) {
    char words[2][20] = {"Welcome to Xiyou ", "Linux Group 2022"};
    printf("%s\n", words[0]);
    printf("%s\n", words[1]);
    char *str = convertAndMerge(words);
    printf("str = %s\n", str);
    free(str);
}
#include <stdio.h>
#include <string.h>
#include <malloc.h>
char *convertAndMerge(char strs[2][20])
{
   int totalLength = strlen(strs[0]) + strlen(strs[1]);

char* result = (char*)malloc(totalLength + 1);
    strcpy(result, strs[0]);
    strcat(result, strs[1]);
    for (int i = 0; i < totalLength; i++)
    {
        if (result[i] >= 'A' && result[i] <= 'Z')
            result[i] += 32;
        else if (result[i] >= 'a' && result[i] <= 'z')
            result[i] -= 32;
    }
    return result;
}
int main(void)
{
    char words[2][20] = {"Welcome to Xiyou ", "Linux Group 2022"};
    printf("%s\n", words[0]);
    printf("%s\n", words[1]);
    char *str = convertAndMerge(words);
    printf("str = %s\n", str);
    free(str);
}

先计算了两个输入字符串的总长度,然后分配足够的内存来存储拼接后的字符串。接着,使用strcpy和strcat函数将两个输入字符串拼接在一起,并在最后循环遍历拼接后的字符串,翻转其中所有字母的大小写。

10. 给你我的指针,访问我的心声

  • 程序的输出有点奇怪,请尝试解释一下程序的输出吧。
int main(int argc, char **argv) {
    int arr[5][5];
    int a = 0;
    for (int i = 0; i < 5; i++) {
        int *temp = *(arr + i);
        for (; temp < arr[5]; temp++) *temp = a++;
    }
    for (int i = 0; i < 5; i++) {
        for (int j = 0; j < 5; j++) {
            printf("%d\t", arr[i][j]);
        }
    }
}

输出:

0       1       2       3       4
25      26      27      28      29
45      46      47      48      49
60      61      62      63      64
70      71      72      73      74

第一个for每循环一次第二个for循环次数都会少5,因此为如上结果。

11. 奇怪的参数

  • 你了解argc和argv吗?
  • 直接运行程序argc的值为什么是1?
  • 程序会出现死循环吗?
#include <stdio.h>
int main(int argc, char **argv) {
    printf("argc = %d\n", argc);
    while (1) {
        argc++;
        if (argc < 0) {
            printf("%s\n", (char *)argv[0]);
            break;
        }
    }
}

argc是一个整数,代表命令行参数的数量,包括程序名称本身。argv是一个指向字符指针数组的指针,每个指针指向一个字符串,表示一个命令行参数。

argc的值为1是因为直接运行程序,没有提供任何命令行参数。程序名称本身会作为第一个参数传递给main函数。

使用一个无限循环 while (1) 来递增argc的值,然后在检查argc是否小于0的情况下,打印argv[0](程序名称),最后整型argc会自增到溢出并退出循环。

12. 奇怪的字符

  • 程序的输出有点奇怪,请尝试解释一下程序的输出吧。
int main(int argc, char **argv) {
    int data1[2][3] = {{0x636c6557, 0x20656d6f, 0x58206f74},
                       {0x756f7969, 0x6e694c20, 0x00000000}};
    int data2[] = {0x47207875, 0x70756f72, 0x32303220, 0x00000a32};
    char *a = (char *)data1;
    char *b = (char *)data2;
    char buf[1024];
    strcpy(buf, a);
    strcat(buf, b);
    printf("%s \n", buf);
}

定义了一个main函数,然后初始化了两个整数数组data1和data2,将它们转换为字符指针,然后使用strcpy和strcat函数将它们连接到一个字符数组buf中,最后打印buf的内容。

data1是一个二维整数数组,data2是一个一维整数数组。
将data1和data2转换为字符指针a和b,它们将被解释为字符数据,而不是整数数据。

使用strcpy函数将a中的数据复制到buf中,然后使用strcat函数将b中的数据追加到buf的末尾。
最后,将buf中的字符串打印出来。

13. 小试宏刀

  • 请谈谈你对#define的理解。

  • 请尝试着解释程序的输出。

    #define SWAP(a, b, t) t = a; a = b; b = t
    #define SQUARE(a) a *a
    #define SWAPWHEN(a, b, t, cond) if (cond) SWAP(a, b, t)
    int main() {
        int tmp;
        int x = 1;
        int y = 2;
        int z = 3;
        int w = 3;
        SWAP(x, y, tmp);
        printf("x = %d, y = %d, tmp = %d\n", x, y, tmp);
        if (x > y) SWAP(x, y, tmp);
        printf("x = %d, y = %d, tmp = %d\n", x, y, tmp);
        SWAPWHEN(x, y, tmp, SQUARE(1 + 2 + z++ + ++w) == 100);
        printf("x = %d, y = %d\n", x, y, tmp);
        printf("z = %d, w = %d, tmp = %d\n", z, w, tmp);
    }
    

宏定义只能实现简单的文本替换,并不会自动为表达式补充圆括号或者为代码块补充花括号。

#include <stdio.h>
int main()
{
    int tmp;
    int x = 1;
    int y = 2;
    int z = 3;
    int w = 3;
    // SWAP(x, y, tmp);
    tmp = x;
    x = y;
    y = tmp;
    printf("x = %d, y = %d, tmp = %d\n", x, y, tmp);
    // x = 2, y = 1, tmp = 1
    // if (x > y) SWAP(x, y, tmp);
    if (x > y)
        tmp = x;
    // 无论如何以下两行都执行
    x = y;
    y = tmp;
    printf("x = %d, y = %d, tmp = %d\n", x, y, tmp);
    // x = 1, y = 2, tmp = 2
    // SWAPWHEN(x, y, tmp, SQUARE(1 + 2 + z++ + ++w) == 100);
    if (1 + 2 + z++ + ++w * 1 + 2 + z++ + ++w == 100)
        tmp = x;
    x = y;
    y = tmp;
    printf("x = %d, y = %d", x, y, tmp);
    // x = 2, y = 2
    printf("z = %d, w = %d, tmp = %d\n", z, w, tmp);
    // z = 5, w = 5 ,tmp = 2
}

14. GNU/Linux命令 (选做)

  • 你知道以下命令的含义和用法吗:

    注:

    嘿!你或许对Linux命令不是很熟悉,甚至你没听说过Linux。
    但别担心,这是选做题,不会对你的面试产生很大的影响!
    了解Linux是加分项,但不了解也不扣分哦!

    • ls
    • rm
    • whoami
  • 请问你还了解哪些GNU/Linux的命令呢。

ls:ls用于列出目录中的文件和子目录。它通常用于命令行终端,可以带有不同的选项以控制列出的详细信息。例如,ls -l会列出文件和目录的详细信息,包括权限、所有者、大小等。

rm:rm用于删除文件或目录。它会永久删除文件,无法撤销。例如,要删除一个文件,可以使用rm 文件名,要删除一个目录及其内容,可以使用rm -r 目录名。

whoami:whoami命令用于显示当前登录用户的用户名。它在命令行终端上执行时,会返回当前用户的用户名。

除了这些,还有很多其他GNU/Linux命令:

pwd:显示当前工作目录的路径。
cd:用于改变当前工作目录。
mkdir:创建新目录。
touch:创建空文件或更新文件的时间戳。
cp:复制文件或目录。
mv:移动文件或目录,也可用于重命名文件。
cat:用于查看文件内容。
grep:用于在文件中搜索文本模式。
chmod:更改文件或目录的权限。
chown:更改文件或目录的所有者。
ps:显示当前运行进程的列表。
kill:终止进程。
top:显示系统资源使用情况。

等等

结束

  • 恭喜你做到这里!你的坚持战胜了绝大多数看到这份试题的同学。
    或许你自己对答题的表现不满意,但别担心,请自信一点呐。
    坚持到达这里已经证明了你的优秀。
    还在等什么,快带上你的笔记本电脑,来FZ103面试吧!
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值