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

前言

  • 本题目仅作西邮Linux兴趣小组2021纳新面试题的有限参考。

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

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

  • 题目难度与序号无关。

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

  • Copyright © 2021 西邮Linux兴趣小组, All Rights Reserved.
    本试题使用采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 link 进行许可。

1. 大小和长度竟然不是一个意思

  • sizeof()和strlen()有什么异同之处?
    他们对于不同参数的结果有什么不同?请试举例子说明。

    int main(void) {
        char s[] = "I love Linux\0\0\0";
        int a = sizeof(s);
        int b = strlen(s);
        printf("%d %d\n", a, b);
    }
    
    

sizeof()是一个操作符,用于获取一个数据类型或变量所占用的字节数。
sizeof()返回的结果是数据类型或变量占用的存储空间大小,以字节为单位,它不关心字符串的内容,只关心变量的数据类型和大小。

strlen()是一个函数,用于计算以终止字符(’\0’)结束的C字符串的字符数(即字符串长度)。
strlen()返回的结果是字符串的实际字符数,不包括终止字符。

a为字符数组[‘I’, ’ ', ‘l’, ‘o’, ‘v’, ‘e’, ’ ', ‘L’, ‘i’, ‘n’, ‘u’, ‘x’, ‘\0’, ‘\0’, ‘\0’]的大小;
b为字符数组"I love Linux"的长度。

2. 箱子的大小和装入物品的顺序有关

  • test1和test2都含有:1个short、1个int、1个double,那么sizeof(t1)和sizeof(t2)是否相等呢?这是为什么呢?

    struct test1 {
        int a;
        short b;
        double c;
    };
    struct test2 {
        short b;
        int a;
        double c;
    };
    int main(void) {
        struct test1 t1;
        struct test2 t2;
        printf("sizeof(t1): %d\n", sizeof(t1));
        printf("sizeof(t2): %d\n", sizeof(t2));
    }
    
    
 struct test1 {
     int a;          // 0 ~ 3
     short b;        // 4 ~ 5
     double c;       // 8 ~ 15
 } t1;               // 0 ~ 15 -> 16
 struct test2 {
     short b;        // 0 ~ 1
     int a;          // 4 ~ 7
     double c;       // 8 ~ 15
 } t2;               // 0 ~ 15 -> 16
 

C 编译器在分配内存时通常会在结构体的成员之间插入一些填充字节以对齐数据,以提高访问速度和性能。t1和t2内存对齐结果刚好相等。

3. 哦,又是函数

  • 想必在高数老师的教导下大家十分熟悉函数这个概念。那么你了解计算机程序设计中的函数吗?请编写一个func函数,用来输出二维数组arr中每个元素的值。

    /*在这里补全func函数的定义*/
    int main(void) {
        int arr[10][13];
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 13; j++) {
                arr[i][j] = rand();
            }
        }
        func(arr);
    }
    
    
 #include <stdio.h>
 
 void func(int arr[][13]) {
     for (int i = 0; i < 10; i++) {
         for (int j = 0; j < 13; j++) {
             printf("%d ", arr[i][j]);
         }
         printf("\n");
     }
 }
 
 int main(void) {
     int arr[10][13];
     for (int i = 0; i < 10; i++) {
         for (int j = 0; j < 13; j++) {
             arr[i][j] = rand();
         }
     }
     func(arr);
     return 0;
 }

4.就不能换个变量名吗?

  • 请结合下面的程序,简要谈谈传值和传址的区别。
  • 简要谈谈你对C语言中变量的生命周期的认识。
int ver = 123;
void func1(int ver) {
    ver++;
    printf("ver = %d\n", ver);
}
void func2(int *pr) {
    *pr = 1234;
    printf("*pr = %d\n", *pr);
    pr = 5678;
    printf("ver = %d\n", ver);
}
int main() {
    int a = 0;
    int ver = 1025;
    for (int a = 3; a < 4; a++) {
        static int a = 5;
        printf("a = %d\n", a);
        a = ver;
        func1(ver);
        int ver = 7;
        printf("ver = %d\n", ver);
        func2(&ver);
    }
    printf("a = %d\tver = %d\n", a, ver);
}

传值:

传值是指将函数参数的值拷贝给函数内部的变量,函数内部对参数的修改不会影响原始变量的值。
在代码中,func1 函数使用传值方式传递参数 ver,所以它在函数内部对 ver 的修改不会影响 main 函数中的 ver。

传址:

传址是指将参数的地址传递给函数,函数可以通过该地址访问和修改原始变量的值。
在代码中,func2 函数使用传址方式传递参数 &ver,因此它可以通过指针 pr 修改 ver 的值,但需要注意,尝试修改指针 pr 本身并不会影响 ver 的地址,只会影响指针的值。

C语言中变量的生命周期取决于其作用域和存储类型:

局部变量:局部变量的生命周期在其所在的代码块内,它们在进入块时创建,离开块时销毁。在代码中,a 和 ver 在 main 函数中是局部变量,它们的生命周期与 main 函数的执行周期相关。

静态变量:静态变量的生命周期从程序启动到结束,它们在首次访问时创建,在程序结束时销毁。在代码中,static int a 是一个静态变量,它的生命周期是整个程序运行期间。

全局变量:全局变量的生命周期与程序的生命周期一样长,它们在程序启动时创建,在程序结束时销毁。

总结:

C语言中的变量生命周期由作用域和存储类型决定,传值和传址是两种不同的参数传递方式,对于函数参数的修改和变量的生命周期都会受到这些因素的影响。

5. 套娃真好玩!

  • 请说明下面的程序是如何完成求和的?

    unsigned sum(unsigned n) { return n ? sum(n - 1) + n : 0; }
    int main(void) { printf("%u\n", sum(100)); }
    
    

这个程序使用递归的方式来计算从1到n的所有整数的和。

原理:

sum 函数接受一个无符号整数 n 作为参数,并返回一个无符号整数。

在 sum 函数内部,首先检查参数 n 是否为零。如果 n 不为零,那么它会调用自身 sum(n - 1),然后将结果与 n 相加。这个递归调用会继续,直到 n 变为零。

当 n 变为零时,递归停止,函数返回0。

在 main 函数中,程序调用 sum(100) 来计算从1到100的所有整数的和,即不断地减小参数 n 的值,并将其与原始值累加,直到 n 变为零,并将结果打印到标准输出。

6. 算不对的算术

void func(void) {
    short a = -2;
    unsigned int b = 1;
    b += a;
    int c = -1;
    unsigned short d = c * 256;
    c <<= 4;
    int e = 2;
    e = ~e | 6;
    d = (d & 0xff) + 0x2022;
    printf("a=0x%hx\tb=0x%x\td=0x%hx\te=0x%x\n", a, b, d, e);
    printf("c=Ox%hhx\t\n", (signed char)c);
}
  1. short a = -2;:将-2赋值给short变量a。在内存中以2的补码表示,因此a的二进制表示为1111 1111 1111 1110,16位有符号短整数。
  1. unsigned int b = 1;:将1赋值给无符号整数b。
  1. b += a;:将a的值(-2)加到b中。由于b是无符号整数,所以会进行无符号整数溢出,结果是2^32-1。
  1. int c = -1;:将-1赋值给整数c。
  1. unsigned short d = c * 256;:将c乘以256赋值给无符号短整数d。这将导致整数溢出,结果是d = 1111 1111 0000 0000。
  1. c <<= 4;:将c左移4位。这将c的值变成了1111111111110000。
  1. int e = 2;:将2赋值给整数e。
  1. e = ~e | 6;:将e的按位取反(~2),然后与6进行按位或操作。e的初始值2的二进制表示是0000000000000010,按位取反后变成1111111111111101,与6进行按位或操作得到1111111111111111。
  1. d = (d & 0xff) + 0x2022;:将d与0xff进行按位与操作,然后加上0x2022。d的二进制表示是1111111100000000,与0xff进行按位与操作得到0000000000000000,加上0x2022得到0x2022。

最终输出的结果将是:

a=0xfffe    b=0xffffffff    d=0x2022    e=0xffffffff
c=Oxf0

7. 指针和数组的恩怨情仇

int main(void) {
    int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int(*b)[3] = a;
    ++b;
    b[1][1] = 10;
    int *ptr = (int *)(&a + 1);
    printf("%d %d %d \n", a[2][1], **(a + 1), *(ptr - 1));

}

创建一个3x3的二维整数数组 a,并初始化。

创建一个指向包含3个整数的一维数组的指针 b,并将它指向数组 a 的第一个元素的地址。

++b; 将指针 b 向后移动一个一维数组的位置,现在它指向了 a 的第二行。

b[1][1] = 10; 修改了 a 的第三行第二列的值,将其设置为10。

int *ptr = (int *)(&a + 1); 这行代码将数组 a 的地址加上1(数组大小为3x3,所以加1后跳过整个数组),然后将结果强制类型转换为整数指针,将其赋值给 ptr。这行代码将指针 ptr 指向了一个未定义的内存区域。

printf("%d %d %d \n", a[2][1], **(a + 1), *(ptr - 1)); 这行代码打印了三个值:

a[2][1] 打印的是 a 的第三行的第二列,输出为10。
**(a + 1) 打印的是 a 的第二行的第一个元素,输出为4。
*(ptr - 1) =9。

8. 移形换位之术

下面有a、b、c三个变量和4个相似的函数。

  • 你能说出使用这三个变量的值或地址作为参数分别调用这5个函数,在语法上是否正确吗?

  • 请找出下面的代码中的错误。

  • const int和int const是否有区别?如果有区别,请谈谈他们的区别。

  • const int *和int const *是否有区别?如果有区别,请谈谈他们的区别。

    int a = 1;
    int const b = 2;
    const int c = 3;
    void func0(int n) {
        n += 1;
        n = a;
    }
    void func1(int *n) {
        *n += 1;
        n = &a;
    }
    void func2(const int *n) {
        *n += 1;
        n = &a;
    }
    void func3(int *const n) {
        *n += 1;
        n = &a;
    }
    void func4(const int *const n) {
        *n += 1;
        n = &a;
    }
    
    

void func2(const int *n) {//const修饰指针*n,表示*n指向的值不能被改变
 *n += 1;//这里错误
 n = &a;
}
void func3(int *const n) {//const修饰地址,表示指针的地址不能被改变
 *n += 1;
 n = &a;//这里错误
}
void func4(const int *const n) {//const修饰指针和地址,两者均不能改变
 *n += 1;//这里错误
 n = &a;//这里错误
}


对于 const int 和 int const,它们是等效的,都表示一个常量整数。
对于 const int * 和 int const *,它们也是等效的,都表示一个指向常量整数的指针。

9. 听说翻转字母大小写不影响英文的阅读?

  • 请编写convert函数用来将作为参数的字符串中的大写字母转换为小写字母,将小写字母转换为大写字母。返回转换完成得到的新字符串。

    char *convert(const char *s);
    int main(void) {
        char *str = "XiyouLinux Group 2022";
        char *temp = convert(str);
        puts(temp);
    }
    
    
 #include <stdio.h>
 #include <string.h>
 #include <malloc.h>
 char *convert(const char *s)
 {
     int len = strlen(s);
     char *result = malloc(sizeof(char) * (len + 1));
     strcpy(result, s);
     for (int i = 0; i < len; 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 *str = "XiyouLinux Group 2022";
     char *temp = convert(str);
     puts(temp);
 }
 

10. 交换礼物的方式

  • 请判断下面的三种Swap的正误,分别分析他们的优缺点。

  • 你知道这里的do {…} while(0)的作用吗?

  • 你还有其他的方式实现Swap功能吗?

    #define Swap1(a, b, t)   \
        do {                 \
            t = a;           \
            a = b;           \
            b = t;           \
        } while (0)
    #define Swap2(a, b)      \
        do {                 \
            int t = a;       \
            a = b;           \
            b = t;           \
        } while (0)
    void Swap3(int a, int b) {
        int t = a;
        a = b;
        b = t;
    }
    
    

swap1:

直接从原函数中传递参数,方便。它需要传递一个额外的临时变量 t 作为参数,这个变量用来保存一个中间值,以便交换 a 和 b 的值。do { … } while (0) 是一个常见的技巧,它可以确保多个语句在一个作用域内使用,而不会引入额外的副作用。

swap2:

相比于Swap1 ,省略了额外的参数 t,而是在宏内部声明了一个局部变量 t。它的优点是更加简洁,不需要额外的参数。同样,do { … } while (0) 的技巧确保了宏的多个语句在同一个作用域内执行。

swap3:

它的优点是更加清晰和易读,不需要使用宏或额外的 do { … } while (0) 结构。但它的缺点是它只能交换局部变量的值,无法直接修改传递给函数的参数值。

do {…} while(0)的作用:实现局部作用域(为了在宏定义中使用多个语句块而不会受大括号和分号的影响)

11. 据说有个东西叫参数

  • 你知道argc和argv的含义吗?请解释下面的程序。你能在不使用argc的前提下,完成对argv的遍历吗?

    int main(int argc, char *argv[]) {
        printf("argc = %d\n", argc);
        for (int i = 0; i < argc; i++)
            printf("%s\n", argv[i]);
    }
    
    

argc 是一个整数,表示命令行参数的数量(包括程序名称本身作为参数0)。
argv 是一个指向字符指针数组的指针,每个字符指针指向一个字符串,这些字符串包含命令行参数的值。

此程序首先打印参数的数量(argc),然后使用循环遍历并打印每个参数的值(argv[i])。

如果不想使用 argc 来获取参数数量,可以通过在 argv 中查找一个空指针(NULL)来确定何时到达参数的末尾。通常,argv 数组的最后一个元素是一个空指针,所以可以使用如下代码完成对 argv 的遍历:

int main(int argc, char *argv[]) {
    printf("Arguments:\n");
    for (int i = 0; argv[i] != NULL; i++) {
        printf("%s\n", argv[i]);
    }
}

这段代码会一直遍历 argv 直到遇到一个空指针,然后停止。这种方法可以用来遍历所有的命令行参数而不需要显式使用 argc。

12. 人去楼空

  • 这段代码有是否存在错误?谈一谈静态变量与其他变量的异同。

    int *func1(void) {
        static int n = 0;
        n = 1;
        return &n;
    }
    int *func2(void) {
        int *p = (int *)malloc(sizeof(int));
        *p = 3;
        return p;
    }
    int *func3(void) {
        int n = 4;
        return &n;
    }
    int main(void) {
        *func1() = 4;
        *func2() = 5;
        *func3() = 6;
    }
    
    
func3 的问题:

func3 函数返回了一个指向局部变量 n 的指针。当函数返回后,n 将会被销毁,因此返回的指针将指向一个不再有效的内存位置。这会导致在 main 函数中访问无效内存,可能导致未定义的行为。

静态变量:使用 static 关键字声明的变量,在函数调用之间保持其值,具有静态存储期。它们在程序启动时初始化,仅初始化一次,可以被多次调用函数使用。
其他变量:通常指的是自动变量(局部变量)和动态分配的内存(使用 malloc 分配的内存)。这些变量的生命周期与它们的作用域和函数调用相关,局部变量在函数调用结束时销毁,而动态分配的内存需要手动释放。
静态变量与其他变量的主要区别在于生命周期和作用域。静态变量在函数调用之间保持状态,而其他变量的生命周期受函数调用的影响。

13. 奇怪的输出

int main(void) {
    int data[] = {0x636c6557, 0x20656d6f, 0x78206f74,
                  0x756f7969, 0x6e694c20, 0x67207875,
                  0x70756f72, 0x32303220, 0x00000a31};
    puts((const char*)data);
}

大端序:
在大端序中,多字节数据的高位字节(即数据的"大头")被存储在低地址上,其余字节按照内存地址递增的顺序降序排列。
例如,假设有一个16位的数0x1234,在大端序存储中,0x12是高位字节,将被存储在较低的地址处,0x34是低位字节,将被存储在较高的地址处。
小端序:
在小端序中,多字节数据的低位字节(即数据的"小头")被存储在低地址上,其余字节按照内存地址递增的顺序升序排列。
同样的16位数0x1234,在小端序存储中,0x34是低位字节,将被存储在较低的地址处,0x12是高位字节,将被存储在较高的地址处。

Welcome to xiyou Linux group 2021

14. 请谈谈对从「C语言文件到可执行文件」的过程的理解

1.预处理:这是编译过程的第一步,由预处理器完成。
处理源代码文件中的预处理指令,如#include,#define和条件编译指令等。
展开宏,移除注释,添加编译器需要的特定标记。

2.编译:预处理后的代码被送入编译器。
编译器检查语法并将C代码转换成汇编代码。
这个过程中会进行优化,提高代码效率。

3.汇编:将编译器生成的汇编代码转换为机器码。
汇编器将汇编指令转换成处理器能够理解的指令(机器语言)。

4.链接:链接器将多个对象文件(编译器和汇编器生成的)合并成一个单一的可执行文件。
解决程序中的符号引用,例如函数和全局变量。
链接可以是静态的,也可以是动态的。静态链接是将所有必要的库代码添加到最终的可执行文件中;动态链接是在程序运行时由操作系统加载所需的库。

这个过程中,可能会有额外的步骤,如优化和调试信息的生成。最终产生的可执行文件可以在操作系统中直接运行。

结束

  • 恭喜你做完了整套面试题,快来参加西邮Linux兴趣小组的面试吧!

    西邮 Linux兴趣小组面试时间:
    2021年10月25日至2021年10月31日晚8点。
    听说面试来的早一点更能获得学长学姐的好感哦。

    我们在FZ103等你!

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值