西邮Linux兴趣小组2023年面试题解析

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

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

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

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

有1000瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水,24小时后就会准时死亡。至少要多少只小白鼠才能在24小时内鉴别出哪瓶水有毒?
这个问题是典型的“二进制测试”问题。你可以通过给每只小白鼠喂不同组合的水来缩小毒水的范围,利用小白鼠的生死状态来推导出哪瓶水有毒。

分析

方法思路:

  • 假设有 ( n ) 只小白鼠,每只小白鼠的状态有两种:存活或死亡,这相当于一个二进制位可以表示两个状态。
  • 我们要通过 ( n ) 只小白鼠的生死状态,确定是哪一瓶水有毒,这相当于使用 ( n ) 位的二进制数来标记水的编号。
  • 1000 瓶水的编号从 0 到 999,一共 1000 个数。我们需要 ( n ) 个二进制位,能够唯一标识这 1000 个数,也就是说 ( 2^n \geq 1000 )。

计算:

我们求 ( n ) 使得 ( 2^n \geq 1000 )。

  • ( 2^9 = 512 ),不够。
  • ( 2^{10} = 1024 ),够了。

因此,至少需要 10 只小白鼠 才能在 24 小时内鉴别出哪瓶水有毒。

具体操作:

  1. 给每瓶水编号,用二进制表示。例如第 1 瓶水是 0000000001,第 2 瓶水是 0000000010,依此类推到第 1000 瓶水的编号(01111111000)。
  2. 每只小白鼠负责其中一位的测试:
    • 比如编号的第一位为 1 的瓶子,给第一只小白鼠喂;第二位为 1 的瓶子,给第二只小白鼠喂,依此类推。
  3. 24 小时后,根据死亡的小白鼠编号,拼出对应的二进制数,转为十进制后,就能知道哪瓶水有毒。

1. 先预测一下~

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

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

分析

你可以通过修改 welcome() 函数,使其返回一个字符串常量(即你的姓名)。下面是完整的代码示例:

#include <stdio.h>

char *welcome() {
    return "你的姓名";  // 返回一个指向字符串常量的指针
}

int main(void) {
    char *a = welcome();  // a 是指向字符串的指针
    printf("Hi, 我相信 %s 可以面试成功!\n", a);  // 通过 a 访问字符串内容
    return 0;
}

例如,如果你想返回一个具体的名字,比如 “张三”,那么代码会变成:

#include <stdio.h>

char *welcome() {
    return "张三";
}

int main(void) {
    char *a = welcome();
    printf("Hi, 我相信 %s 可以面试成功!\n", a);
    return 0;
}

运行后,输出将会是:

Hi, 我相信 张三 可以面试成功!

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

分析

我们发现最终的输出结果是

Hello, Linux Group - 2023
Pointer Difference: 1431668020

这段代码的输出有几个有趣的点,涉及指针、数组以及 printf 的返回值等概念。让我们分解分析一下每个部分。

1. if (*ptr0 == *ptr1) 判断条件

char *ptr0 = "Welcome to Xiyou Linux!";
char ptr1[] = "Welcome to Xiyou Linux!";
  • ptr0 是一个指向字符串常量的指针,它指向字符串 "Welcome to Xiyou Linux!" 的首地址。
  • ptr1 是一个字符数组,数组中存储了字符串 "Welcome to Xiyou Linux!" 的每个字符。

if (*ptr0 == *ptr1) 里,*ptr0*ptr1 都是取首地址处的第一个字符,也就是 'W'。因此,这里的条件判断为 ,因为 'W' == 'W'

2. 指针差异 ptr0 - ptr1

int diff = ptr0 - ptr1;
printf("Pointer Difference: %d\n", diff);
  • ptr0 是指向字符串常量的指针,它指向全局数据段中的只读区域。
  • ptr1 是一个字符数组,它在栈中分配了内存。因此,ptr0ptr1 指向的内存区域是不同的。
int diff = ptr0 - ptr1;
printf("Pointer Difference: %d\n", diff);

这里是计算两个指针 ptr0ptr1 之间的差异。由于:

  • ptr0 是一个指向字符串常量的指针,指向程序的常量区(通常在只读数据段)。
  • ptr1 是一个字符数组,存储在栈中(位于栈段)。

由于这两个指针所指的内存区域不同,它们相减的结果是基于它们的地址差异。这里打印出来的差值 1431668020 是指针之间的地址差,单位是字节数。

指针差值之所以这么大,原因是:

  • 栈和全局(或常量)数据段之间的内存位置差异较大,导致指针相减得到一个非常大的值。
  • 在不同的系统和运行环境中,这个差值会有所不同。值 1431668020 反映了你所在系统的内存布局,但不具有固定的意义。

3.输出分析

  1. 字符串输出的字符数

    • 输出字符串 "Hello, Linux Group - 20"
      • 字符数计算
        • H (1) + e (1) + l (1) + l (1) + o (1) + , (1) + (1) + L (1) + i (1) + n (1) + u (1) + x (1) + (1) + G (1) + r (1) + o (1) + u (1) + p (1) + (1) + - (1) + (1) + 2 (1) + 0 (1)
        • 总共 23 个字符
  2. 嵌套 printf 解析

    • 内层 printf("") 返回值:
      • 返回 0(不输出任何内容)。
    • 中间的 printf("Hello, Linux Group - 2%d", 0)
      • 这个 printf 实际上是输出 23,因为它输出的内容是 “Hello, Linux Group - 20”(如上分析,23 个字符)。
    • 最外层的 printf("%d\n", 23) 将输出 23,但这是因为它打印的整型值是内层 printf 的返回值。
代码流程和最终输出

因此,代码的具体输出过程如下:

  1. 内层 printf("") 输出 0,不显示。
  2. 中间的 printf("Hello, Linux Group - 2%d", 0) 输出 “Hello, Linux Group - 20”,返回值 23
  3. 最外层的 printf("%d\n", 23) 输出 23

3. 一切都翻倍了吗

  1. 请尝试解释一下程序的输出。
  2. 请谈谈对sizeof()strlen()的理解吧。
  3. 什么是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));
}

程序代码

#include <stdio.h>

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

1. 程序输出分析

第一行输出
printf("%zu\t%zu\t%zu\n", sizeof(*&arr), sizeof(arr + 0), sizeof(num = num2 + 4));
  1. sizeof(*&arr)

    • arr 是一个字符数组,*&arrarr 的地址并解引用,得到共7个字符,包括 '\0',即 7 字节。
  2. sizeof(arr + 0)

    • arr + 0 仍然指向 arr 的首地址,然而在 sizeof 中,由于其类型是指针,sizeof(arr + 0) 返回的是指针类型的大小(通常为 4 或 8 字节,取决于编译器和系统架构)。
    • 在大多数 64 位系统上,返回的结果是 8 字节。
  3. sizeof(num = num2 + 4)

    • 这个表达式是一个赋值表达式。sizeof 只关心 num 的类型,而 num 的类型是 short,通常占用 2 字节。

结合上面的分析,第一行输出应该是:

7   8   2
第二行输出
printf("%d\n", sprintf(str, "0x%x", num) == num);
  • sprintf(str, "0x%x", num) 会把 num 的值(520)格式化为十六进制字符串 0x208
  • sprintf 返回的字符数是 5(包括 0x208),所以 sprintf 返回值与 num(520)并不相等。因此输出是 0
第三行输出
printf("%zu\t%zu\n", strlen(&str[0] + 1), strlen(arr + 0));
  1. strlen(&str[0] + 1)

    • &str[0] + 1 指向 str 的第二个字符,即 0x208 中的 x
    • 字符串 0x208 的长度为 4(即 x, 2, 0, 8)。
  2. strlen(arr + 0)

    • strlen()读取至第一个\0时停止,所以共读取Linux五个字符,返回5。

综上所述,输出结果应该是:

7   8   2
0
4   5

最终输出结果

所以,整段程序的最终输出应该是:

7       8       2
0
4       5

2. 对 sizeof()strlen() 的理解

sizeof()
  • sizeof 是一个运算符,用于返回数据类型或对象在内存中占用的字节数。
  • 对于数组,sizeof 返回整个数组的大小;对于指针,返回的是指针的大小(通常是 4 或 8 字节,取决于平台)。
strlen()
  • strlen 是一个函数,用于计算以 \0 结尾的字符串的长度(不包括结束的 \0)。
  • 它的返回值是 size_t 类型,表示字符串中的字符数。

3. sprintf() 的参数和返回值

sprintf()
  • sprintf 是一个标准库函数,类似于 printf,但它将格式化的数据输出到字符串中,而不是标准输出。

  • 参数

    • 第一个参数是目标字符串的指针(存储结果的缓冲区)。
    • 后续参数是格式化字符串及其相关的值(类似于 printf)。
  • 返回值

    • sprintf 返回写入 buffer 的字符数,不包括结束的 \0。如果发生错误,则返回一个负值。

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

分析

程序代码

#include <stdio.h>

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

逐行分析

  1. char a = 64 & 127;

    • 计算位与(&):
      • 64 的二进制表示:01000000
      • 127 的二进制表示:01111111
      • 位与操作:
        01000000
        01111111
        ----------
        01000000  (即 64)
        
    • 所以 a 的值是 64
  2. char b = 64 ^ 127;

    • 计算位异或(^):
      • 位异或操作:
        01000000
        01111111
        ----------
        00111111  (即 63)
        
    • 所以 b 的值是 63
  3. char c = -64 >> 6;

    • -64 的二进制表示(假设使用 8 位二进制补码表示):
      • 64 的二进制:00111111
      • 取反加一(得到 -64):
        取反: 11000000
        加一: 11000001  (即 -64)
        
    • 进行右移操作 -64 >> 6
      • 右移 6 位,结果:
        11000001 (补码) -> 11111111 (逻辑右移,填充符号位)
        
    • c 的值为 -1(在补码中表示)。
  4. char ch = a + b - c;

    • 计算 ch 的值:
    • a + b - c = 64 + 63 - (-1) = 64 + 63 + 1 = 128
    • 然而,chchar 类型,通常为有符号类型,其值的范围是 -128 到 127。因此:
      • ch 的值为 128 时,它会溢出,表现为:
        • 128 在 8 位二进制补码中的表示是 10000000,也就是 -128

输出结果

printf("a = %d b = %d c = %d\n", a, b, c);
printf("ch = %d\n", ch);

最终输出将是:

a = 64 b = 63 c = -1
ch = -128

总结

  • a 的值是 64,通过位与操作得到。
  • b 的值是 63,通过位异或操作得到。
  • c 的值是 -1,由于右移操作的符号扩展。
  • ch 的计算结果为 128,但由于溢出,在 8 位 char 类型中表现为 -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)));
}

分析

程序代码

#include <stdio.h>

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() 函数的功能

func() 函数的主要功能是计算两个整数 ab 的和。它使用递归和位运算来实现加法。

函数实现分析

  1. 参数

    • ab:需要相加的两个整数。
  2. 基准情况

    • if (!a) return b;:如果 a 为 0,直接返回 b。这是因为任何数加 0 等于它本身。
  3. 递归情况

    • return func((a & b) << 1, a ^ b);
      • a & b:计算 ab 的位与运算,得到的结果是所有相同位为 1 的部分(即进位)。
      • (a & b) << 1:将进位左移一位,以便在下一次递归中加到正确的位置。
      • a ^ b:计算 ab 的位异或运算,得到不考虑进位的结果。
      • func((a & b) << 1, a ^ b):递归调用 func,将进位和当前的和作为新的参数继续计算。

计算过程示例

a = 4b = 9 为例进行计算:

  1. 第一次调用

    • a = 4 (二进制 0100)
    • b = 9 (二进制 1001)
    • a & b = 0 (没有进位)
    • a ^ b = 13 (二进制 1101)
    • 调用 func(0, 13)
  2. 第二次调用

    • a = 0,此时返回 b = 13

因此,func(4, 9) 的结果是 13

main() 函数的执行

int main(void) {
    int a = 4, b = 9, c = -7;
    printf("%d\n", func(a, func(b, c)));
}
  1. 内部计算

    • func(b, c)
      • b = 9c = -7
      • 计算过程会得到 9 + (-7) = 2
  2. 外部计算

    • func(a, func(b, c)) 等价于 func(4, 2)
      • 计算过程会得到 4 + 2 = 6

最终输出

因此,最终输出将是 6

总结

  • func() 函数通过位运算和递归实现两个整数的加法,使用位与和位异或运算计算进位和当前和。
  • 该方法有效地避免了使用普通的加法运算符,通过不断递归直到进位为 0 来求和。

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

为了实现 filter() 函数,我们需要遵循以下步骤:

  1. 定义函数指针类型:这是一个接受整数并返回整数的函数指针类型。
  2. 遍历输入数组:对输入数组中的每个元素应用给定的条件(通过函数指针),以确定是否将其包含在输出数组中。
  3. 动态分配新数组:根据满足条件的元素数量,为新数组分配内存。
  4. 返回结果:返回新数组,并更新结果长度。

下面是完整的代码实现:

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

typedef int (*Predicate)(int);

int *filter(int *array, int length, Predicate predicate, int *resultLength) {
    // 计算满足条件的元素数量
    int count = 0;
    for (int i = 0; i < length; i++) {
        if (predicate(array[i])) {
            count++;
        }
    }
    
    // 为新数组分配内存
    int *result = (int *)malloc(count * sizeof(int));
    if (result == NULL) {
        // 处理内存分配失败的情况
        *resultLength = 0;
        return NULL;
    }
    
    // 填充新数组
    int index = 0;
    for (int i = 0; i < length; i++) {
        if (predicate(array[i])) {
            result[index++] = 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);
    if (filteredNumbers == NULL) {
        printf("Memory allocation failed!\n");
        return 1;  // 退出程序,表示发生了错误
    }

    for (int i = 0; i < resultLength; i++) {
        printf("%d ", filteredNumbers[i]);
    }
    printf("\n");
    
    free(filteredNumbers);
    return 0;
}

代码说明

  1. 函数原型

    int *filter(int *array, int length, Predicate predicate, int *resultLength);
    
    • array: 输入的整数数组。
    • length: 数组的长度。
    • predicate: 函数指针,指向条件函数。
    • resultLength: 指向整数的指针,用于返回结果数组的长度。
  2. 计算符合条件的元素数量

    • 使用 predicate(array[i]) 检查每个元素是否满足条件,如果满足,计数器 count 加 1。
  3. 动态内存分配

    • 使用 malloc() 为新数组分配空间。
    • 检查 malloc() 的返回值,以确保内存分配成功。
  4. 填充新数组

    • 再次遍历输入数组,将满足条件的元素添加到新数组中。
  5. 返回新数组和长度

    • 返回新的数组指针,更新 resultLength 指向的值。

运行示例

运行上述代码后,输出结果应为:

1 2 3 4 5 6 

7. 静…态…

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

分析

static 是 C 语言中的一个关键字,用于指定变量或函数的存储类别。它影响变量的生命周期、可见性和内存分配方式。以下是对你问题的详细解释:

1. 如何理解关键字 static

  • static 关键字用于定义静态存储期的变量或函数。与局部变量不同,使用 static 定义的变量在程序的整个运行期间都存在,即使它们在作用域外也不会被销毁。static 关键字还限制了变量或函数的可见性,使其只能在定义它们的文件或函数内部访问。

2. static 与变量结合后有什么作用?

  • 静态局部变量

    • 使用 static 定义的局部变量在函数调用之间保持其值,而不是每次调用时重新初始化。
    • 例如:
      void example() {
          static int count = 0; // 初始化仅在第一次调用时执行
          count++;
          printf("%d\n", count);
      }
      
    • 每次调用 example() 函数时,count 的值都会增加,而不是从 0 开始。
  • 静态全局变量

    • 使用 static 定义的全局变量只能在定义它的文件中访问,外部文件无法访问。
    • 这有助于避免命名冲突。

3. static 与函数结合后有什么作用?

  • 静态函数
    • 使用 static 定义的函数只能在定义它的文件中使用,外部文件无法调用。这用于限制函数的作用域,确保其他文件中的代码不会干扰这个函数的实现。
    • 例如:
      static void myFunction() {
          // 只有当前文件可以调用
      }
      

4. static 与指针结合后有什么作用?

  • static 关键字本身并不直接影响指针,但可以与指针类型结合使用。
  • 例如,声明一个指向静态变量的指针:
    static int value = 10;
    static int *ptr = &value; // ptr 是一个指向静态变量的指针
    
  • 指针本身可以是静态的,意味着它在程序的整个运行期间都存在,且其值不会在函数调用之间被改变。

5. static 如何影响内存分配?

  • 使用 static 定义的变量在 静态存储区 中分配内存,而不是在 中。
  • 静态存储区
    • 包含所有静态变量、全局变量和常量,其生命周期与程序的运行周期相同。
    • 静态变量在程序启动时分配内存,并在程序结束时释放,不管变量是在函数内还是外定义。

总结

  • static 关键字影响变量和函数的生命周期、可见性和内存分配。
  • 静态局部变量在函数调用之间保持其值,静态全局变量和静态函数限制了其作用域。
  • 所有使用 static 关键字的变量都分配在静态存储区,具有更长的生命周期。

8. 救命!指针!

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

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

分析

让我们逐一解析这些概念:数组指针、指针数组、函数指针,并解释给出的数据类型含义。

1. 数组指针

数组指针是一个指针,它指向整个数组。具体来说,它是指向数组的指针变量,而不是数组的单个元素。通常情况下,数组指针的语法如下:

int (*p)[10];

这表示 p 是一个指向包含 10 个 int 类型元素的数组的指针。

示例
int arr[10];
int (*p)[10] = &arr; // p 指向 arr 数组

在这里,p 指向整个数组 arr

2. 指针数组

指针数组是一个数组,其中的每个元素都是指针。换句话说,它是一个数组,数组中的每个元素都指向一个特定的数据类型。

通常情况下,指针数组的语法如下:

const int* p[10];

这表示 p 是一个包含 10 个指向 const int 类型的指针的数组。

示例
const int* p[10]; // p 是一个数组,包含 10 个指向 const int 的指针

在这里,p 可以存储 10 个指向常量整数的指针。

3. 函数指针

函数指针是一个指向函数的指针。它允许我们通过指针调用函数,而不仅仅是通过函数名称。函数指针可以用于实现回调机制和动态函数调用。

通常情况下,函数指针的语法如下:

int (*f1(int))(int*, int);

这表示 f1 是一个接受 int 类型参数并返回一个指向函数的指针,该函数接受两个参数(int*int)并返回 int 类型的值。

示例
int add(int* a, int b) {
    return *a + b;
}

int (*f1(int))(int*, int) {
    return add; // 返回指向 add 函数的指针
}

在这个例子中,f1 返回一个指向 add 函数的指针,该函数接受一个 int* 和一个 int 类型的参数。

数据类型的含义总结

  1. int (*p)[10];

    • p 是一个指向具有 10 个 int 元素的数组的指针。
  2. const int* p[10];

    • p 是一个数组,包含 10 个指向 const int 的指针。
  3. int (*f1(int))(int*, int);

    • f1 是一个接受 int 类型参数的函数,返回一个指向接受两个参数(int*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;
}

分析

程序分析

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;
}
1. argc 的含义
  • argc 是命令行参数的计数,表示传递给程序的参数个数。通常情况下,如果没有提供任何参数,argc 将是 1,因为程序名称本身也算作一个参数。
2. 第一行输出
  • printf("[%d]\n", argc);
  • 这行代码输出了 argc 的值。在这个例子中,输出为 [1],说明程序运行时没有传递额外的命令行参数。
3. while (argc) { ++argc; }
  • 这个 while 循环的目的是使 argc 变为 0。循环会不断增加 argc 的值,直到其为 0。在这里,由于 argc 最开始是 1,循环执行一次后将其变为 2,然后执行到 3,最后达到 0 时退出循环。
  • 注意:此时 argc 的值不再代表命令行参数的个数,而是被人为修改为 0。
4. 变量初始化
int i = -1, j = argc, k = 1;
  • 此时 i 被初始化为 -1,j 被初始化为 argc(现在为 0),k 被初始化为 1。
5. 逻辑运算
i++ && j++ || k++;
  • 这是一个复杂的逻辑表达式。我们来分开分析:
    • i++ 会返回 -1(但会在表达式求值后增加到 0)。
    • j++ 不会被执行,因为在 C 语言中,逻辑与 (&&) 的短路特性会导致 j++i++ 为假时不被计算。
    • 因此,k++ 将被执行。由于 j 由于 argc 的影响,保持在 0,所以整个表达式的值为真(k++ 被执行),但在输出 k 的值时,它被增加到 2。
6. 输出结果
  • printf("i = %d, j = %d, k = %d\n", i, j, k);
  • 输出 i = 0j = 1k = 2
    • i 最后为 0(因为 i++ 被执行后,值变为 0)。
    • j 没有被增加,保持为 0(但 j++ 在逻辑判断中并未执行)。
    • k 最终被增加到 2。

最终输出内容

[1]
i = 0, j = 1, k = 2
  • [1] 表示程序开始时的 argc 值,表明没有额外的命令行参数。
  • i = 0, j = 1, k = 2 表示在逻辑运算后的变量值,反映了如何通过短路求值影响了变量的最终状态。

总结

  • 这个程序展示了如何使用命令行参数和逻辑运算符,并演示了逻辑短路的效果。
  • 由于 argcwhile 循环中被人为修改,程序最终输出了一些可能不太直观的结果。

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

宏的展开

  1. 宏定义
    • CAL(a) 被定义为 a * a * a
    • 当我们在 if 语句中使用 CAL(2) 时,它会展开为 2 * 2 * 2

计算条件

  1. 条件判断

    if (16 / CAL(2) == 2)
    
    • 展开后为:
    if (16 / (2 * 2 * 2) == 2)
    
    • 由于宏没有加括号,运算会按照优先级进行计算:
    16 / 2 * 2 * 2
    
    • 因此,计算过程如下:
      1. 16 / 2 先计算,结果为 8
      2. 然后 8 * 2,结果为 16
      3. 最后 16 * 2,结果为 32

    这使得条件变为:

    if (32 == 2)
    
    • 这个条件是 的。

执行流程

  1. else 语句

    • 由于 if 条件为假,程序执行 else 块中的代码:
    int nums = MAGIC_CAL(++nums, 2);
    
    • ++numsnums1 增加到 2,然后计算 MAGIC_CAL(2, 2),即:
    CAL(2) + CAL(2)
    
    • 计算 CAL(2)
      • CAL(2) = 2 * 2 * 2 = 8,因此:
      • MAGIC_CAL(2, 2) = 8 + 8 = 16
    • 此时,nums 的新值为 16
  2. 最终输出

    printf("%d\n", nums);
    
    • 然而,由于 else 中的 nums 是一个新的局部变量(与外部的 nums 没有关系),而外部的 nums 从未被修改,仍然是 1

结果总结

最终输出结果是:

1

总结

  • if 条件由于宏展开后的运算结果为 32,导致条件为假,进入 else 块。
  • else 中的 int nums 定义了一个新的局部变量 nums,它与外部的 nums 不同,外部的 nums 的值保持为 1
  • 最终输出仍然是外部 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;
}

分析

代码分析

#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;                                // 设置年龄
}

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

问题分析

  1. 内存分配

    • initializeStudent 函数中,s1.names2.name 分别通过 malloc 动态分配了内存,存储学生的姓名。
    • 这意味着 s1.names2.name 指向不同的内存区域。
  2. 直接赋值的问题

    • 当执行 s1 = s2; 时,整个 struct Student 结构体被复制。
    • 这包括 agename 的指针。结果是 s1.name 现在指向与 s2.name 相同的内存地址。
    • 由于 s2.name 指向的内存是动态分配的,如果在后面 free(s1.name); 被调用,s2.name 也会被错误地释放,导致悬挂指针(dangling pointer)问题。
  3. 内存释放的问题

    • free(s1.name); 被调用时,s2.name 也被释放了。这将导致访问 s2.name 时出现未定义行为,因为它指向的内存已被释放。

如何解决这个问题

为了避免上述问题,可以选择以下方法之一:

  1. 手动复制字符串:在结构体赋值前手动复制字符串,以确保每个结构体都有自己的内存。

  2. 使用函数复制结构体:创建一个专门的函数来复制 struct Student,并确保对 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;
}

// 自定义的深拷贝函数
struct Student copyStudent(const struct Student *src) {
    struct Student dest;
    dest.name = (char *)malloc(strlen(src->name) + 1);
    strcpy(dest.name, src->name);
    dest.age = src->age;
    return dest;
}

int main(void) {
    struct Student s1, s2;
    initializeStudent(&s1, "Tom", 18);
    initializeStudent(&s2, "Jerry", 28);
    
    // 使用深拷贝函数
    s1 = copyStudent(&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;
}

关键点

  • 使用自定义的 copyStudent 函数确保 name 字符串的深拷贝,从而避免了内存冲突和悬挂指针的问题。
  • 这种方法确保每个 Student 结构体都有自己的 name 字符串,防止释放内存时出现错误。

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

分析

代码分析

#include <stdio.h>

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

主要结构体分析

  1. 结构体定义
    • struct structure 中包含一个 int foo、一个 union node(包含一个 int、一个 char 数组和一个指针)、一个 short bar、一个 long long baz 以及一个长度为 7 的 int array
    • 由于 union 的特性,node 只会占用其最大成员的空间,因此 node 的大小等于 void * 的大小(在 32 位系统中通常是 4 字节,在 64 位系统中通常是 8 字节)。

数据安排

  1. 数组初始化
    • arr 数组包含多个 int 值,这些值以十六进制表示,实际上是在内存中按字节排列。
    • 每个 int 占用 4 个字节,因此 arr 数组的总大小是 18 个 int,即 72 字节。

指针转换与输出

  1. 类型转换

    • ((struct structure *)arr)arr 数组的地址转换为 struct structure * 类型。这意味着我们将访问 arr 的内存空间,就像它是一个 struct structure 类型的变量。
  2. 访问 node.string

    • ((struct structure *)arr)->node.string 试图访问 node.string,而 node.string 是一个字符数组,长度为 11。
    • 由于 arr 中的整数是用十六进制存储的,输出字符串实际上是从 arr 中相应的内存位置提取的字符,可能是 ASCII 字符。

输出结果

  1. 输出字符串
    • printf("%s\n", ...) 将输出 node.string 中的字符,这些字符的值由 arr 中的整数表示。
    • 由于 arr 中的值被解释为字符,实际输出可能是一些 ASCII 字符的组合,具体取决于内存中这些值的字节表示。

内存敏感性

  • 由于 C 语言直接操作内存,程序员必须对内存布局和结构体的字节对齐非常敏感。错误的内存操作可能导致未定义行为,例如:

    • 溢出:未正确处理字符串末尾的空字符 \0
    • 内存对齐:不同数据类型可能有不同的对齐要求。

    内存溢出和内存对齐是计算机内存管理中两个重要的概念。以下是对这两个概念的简述:

内存溢出(Memory Overflow)

定义:

内存溢出是指程序在运行时试图使用超过其分配的内存空间的情况。这通常会导致程序崩溃、数据损坏或不可预测的行为。

原因:
  1. 数组越界:访问数组的非法索引。
  2. 动态内存分配:使用 malloccalloc 等函数分配内存时,未检查返回值。
  3. 递归过深:过多的递归调用导致栈空间不足。
  4. 逻辑错误:程序逻辑错误导致分配的内存未被正确使用。
后果:
  • 程序崩溃。
  • 数据丢失或损坏。
  • 安全漏洞(如缓冲区溢出攻击)。

内存对齐(Memory Alignment)

定义:

内存对齐是指将数据存放在内存中的特定边界上,以提高 CPU 存取数据的效率。不同数据类型通常有不同的对齐要求。

原因:
  1. 硬件架构:许多 CPU 在读取内存时,要求数据存储在特定的地址(如偶数地址)上,这样可以提高访问速度。
  2. 性能优化:对齐数据可以减少 CPU 在访问数据时的复杂性,提升内存访问性能。
规则:
  • 常见的对齐规则为:
    • char 类型通常是 1 字节对齐。
    • int 类型通常是 4 字节对齐。
    • double 类型通常是 8 字节对齐。
  • 结构体的对齐需要考虑其最大成员的对齐要求。
示例:

假设有一个结构体,包含不同类型的成员:

struct Example {
    char c;      // 1 byte
    int i;       // 4 bytes
    short s;     // 2 bytes
};
  • 为了满足内存对齐的要求,int 类型的成员可能会被放置在地址为 4 的倍数的位置,因此 char 后会填充 3 个字节的空白,以确保 int 成员在合适的地址上。

总结

  • 内存溢出 是一种运行时错误,可能导致程序异常或崩溃,通常与错误的内存访问有关。
  • 内存对齐 是一种设计原则,用于优化数据在内存中的存储方式,提高访问效率。

13. GNU/Linux (选做)

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

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

解答

以下是关于 cd 命令及符号的详细说明:

cd 命令的用法

  • cd(change directory)命令用于更改当前工作目录。

  • 基本语法:

    cd [目录]
    
  • 常用选项

    • cd ..:返回上一级目录。
    • cd -:返回到上一个工作目录。
    • cd ~:切换到当前用户的主目录。

符号的含义

  1. /

    • 表示根目录,是文件系统的最顶层。所有其他目录都在根目录下。
    • 例如,/home/user 表示 home 目录下的 user 目录。
  2. .

    • 表示当前目录。
    • 例如,./file.txt 指的是当前目录下的 file.txt 文件。
  3. ~

    • 表示当前用户的主目录。
    • 例如,~/documents 指的是当前用户主目录下的 documents 目录。

其他 GNU/Linux 相关知识

  • 文件和目录权限

    • 每个文件和目录都有权限设置(如 rwx),控制谁可以读取、写入或执行。
  • 管道和重定向

    • 使用 | 管道符将一个命令的输出传递给另一个命令。
    • 使用 > 将输出重定向到文件,例如 ls > file.txt
  • 环境变量

    • 系统和用户定义的变量,影响程序的行为,例如 PATH 环境变量决定可执行文件的搜索路径。
  • 包管理器

    • 不同的 Linux 发行版有不同的包管理器(如 aptyumdnf)来安装和管理软件包。
  • Shell 脚本

    • 使用 .sh 扩展名的脚本文件,可用于自动化任务。
  • 进程管理

    • 使用 ps 查看当前运行的进程,使用 kill 命令结束进程。
  • 文件查找

    • 使用 findlocate 命令查找文件和目录。
  • 网络命令

    • 使用 pingcurlwget 等命令进行网络测试和下载。

:::tip 结语

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

排版:纸鹿,有问题扣他鸡腿。{style=“font-size: .8em; opacity: .5;”}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值