西邮Linux兴趣小组2023纳新面试题
学长寄语:长期以来,西邮Linux兴趣小组的面试题以难度之高名扬西邮校内。我们作为出题人也清楚的知道这份试题略有难度。请你动手敲一下代码。别担心,若有同学能完成一半的题目,就已经十分优秀。其次,相比于题目的答案,我们对你的思路和过程更感兴趣,或许你的答案略有瑕疵,但你正确的思路和对知识的理解足以为你赢得绝大多数的分数。最后,做题的过程也是学习和成长的过程,相信本试题对你更加熟悉的掌握C语言的一定有所帮助。祝你好运。我们东区逸夫楼FZ103见!
- 本题目只作为西邮Linux兴趣小组2023纳新面试的有限参考。
- 为节省版面,本试题的程序源码省去了
#include指令。 - 本试题中的程序源码仅用于考察C语言基础,不应当作为C语言「代码风格」的范例。
- 所有题目编译并运行于
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 瓶水是 0000000001,第 2 瓶水是 0000000010,依此类推到第 1000 瓶水的编号(01111111000)。
- 每只小白鼠负责其中一位的测试:
- 比如编号的第一位为 1 的瓶子,给第一只小白鼠喂;第二位为 1 的瓶子,给第二只小白鼠喂,依此类推。
- 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是一个字符数组,它在栈中分配了内存。因此,ptr0和ptr1指向的内存区域是不同的。
int diff = ptr0 - ptr1;
printf("Pointer Difference: %d\n", diff);
这里是计算两个指针 ptr0 和 ptr1 之间的差异。由于:
ptr0是一个指向字符串常量的指针,指向程序的常量区(通常在只读数据段)。ptr1是一个字符数组,存储在栈中(位于栈段)。
由于这两个指针所指的内存区域不同,它们相减的结果是基于它们的地址差异。这里打印出来的差值 1431668020 是指针之间的地址差,单位是字节数。
指针差值之所以这么大,原因是:
- 栈和全局(或常量)数据段之间的内存位置差异较大,导致指针相减得到一个非常大的值。
- 在不同的系统和运行环境中,这个差值会有所不同。值 1431668020 反映了你所在系统的内存布局,但不具有固定的意义。
3.输出分析
-
字符串输出的字符数
- 输出字符串
"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 个字符。
- 字符数计算:
- 输出字符串
-
嵌套
printf解析- 内层
printf("")返回值:- 返回 0(不输出任何内容)。
- 中间的
printf("Hello, Linux Group - 2%d", 0):- 这个
printf实际上是输出23,因为它输出的内容是 “Hello, Linux Group - 20”(如上分析,23 个字符)。
- 这个
- 最外层的
printf("%d\n", 23)将输出 23,但这是因为它打印的整型值是内层printf的返回值。
- 内层
代码流程和最终输出
因此,代码的具体输出过程如下:
- 内层
printf("")输出 0,不显示。 - 中间的
printf("Hello, Linux Group - 2%d", 0)输出 “Hello, Linux Group - 20”,返回值 23。 - 最外层的
printf("%d\n", 23)输出 23。
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));
}
程序代码
#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));
-
sizeof(*&arr):arr是一个字符数组,*&arr取arr的地址并解引用,得到共7个字符,包括'\0',即 7 字节。
-
sizeof(arr + 0):arr + 0仍然指向arr的首地址,然而在sizeof中,由于其类型是指针,sizeof(arr + 0)返回的是指针类型的大小(通常为 4 或 8 字节,取决于编译器和系统架构)。- 在大多数 64 位系统上,返回的结果是 8 字节。
-
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(包括0x和208),所以sprintf返回值与num(520)并不相等。因此输出是 0。
第三行输出
printf("%zu\t%zu\n", strlen(&str[0] + 1), strlen(arr + 0));
-
strlen(&str[0] + 1):&str[0] + 1指向str的第二个字符,即0x208中的x。- 字符串
0x208的长度为 4(即x,2,0,8)。
-
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);
}
逐行分析
-
char a = 64 & 127;- 计算位与(&):
64的二进制表示:01000000127的二进制表示:01111111- 位与操作:
01000000 01111111 ---------- 01000000 (即 64)
- 所以
a的值是 64。
- 计算位与(&):
-
char b = 64 ^ 127;- 计算位异或(^):
- 位异或操作:
01000000 01111111 ---------- 00111111 (即 63)
- 位异或操作:
- 所以
b的值是 63。
- 计算位异或(^):
-
char c = -64 >> 6;-64的二进制表示(假设使用 8 位二进制补码表示):64的二进制:00111111- 取反加一(得到
-64):取反: 11000000 加一: 11000001 (即 -64)
- 进行右移操作
-64 >> 6:- 右移 6 位,结果:
11000001 (补码) -> 11111111 (逻辑右移,填充符号位)
- 右移 6 位,结果:
c的值为 -1(在补码中表示)。
-
char ch = a + b - c;- 计算
ch的值: a + b - c = 64 + 63 - (-1) = 64 + 63 + 1 = 128- 然而,
ch是char类型,通常为有符号类型,其值的范围是 -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() 函数的主要功能是计算两个整数 a 和 b 的和。它使用递归和位运算来实现加法。
函数实现分析
-
参数:
a和b:需要相加的两个整数。
-
基准情况:
if (!a) return b;:如果a为 0,直接返回b。这是因为任何数加 0 等于它本身。
-
递归情况:
return func((a & b) << 1, a ^ b);:a & b:计算a和b的位与运算,得到的结果是所有相同位为 1 的部分(即进位)。(a & b) << 1:将进位左移一位,以便在下一次递归中加到正确的位置。a ^ b:计算a和b的位异或运算,得到不考虑进位的结果。func((a & b) << 1, a ^ b):递归调用func,将进位和当前的和作为新的参数继续计算。
计算过程示例
以 a = 4 和 b = 9 为例进行计算:
-
第一次调用:
a = 4(二进制0100)b = 9(二进制1001)a & b = 0(没有进位)a ^ b = 13(二进制1101)- 调用
func(0, 13)。
-
第二次调用:
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)));
}
-
内部计算:
func(b, c):b = 9,c = -7- 计算过程会得到
9 + (-7) = 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() 函数,我们需要遵循以下步骤:
- 定义函数指针类型:这是一个接受整数并返回整数的函数指针类型。
- 遍历输入数组:对输入数组中的每个元素应用给定的条件(通过函数指针),以确定是否将其包含在输出数组中。
- 动态分配新数组:根据满足条件的元素数量,为新数组分配内存。
- 返回结果:返回新数组,并更新结果长度。
下面是完整的代码实现:
#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;
}
代码说明
-
函数原型:
int *filter(int *array, int length, Predicate predicate, int *resultLength);array: 输入的整数数组。length: 数组的长度。predicate: 函数指针,指向条件函数。resultLength: 指向整数的指针,用于返回结果数组的长度。
-
计算符合条件的元素数量:
- 使用
predicate(array[i])检查每个元素是否满足条件,如果满足,计数器count加 1。
- 使用
-
动态内存分配:
- 使用
malloc()为新数组分配空间。 - 检查
malloc()的返回值,以确保内存分配成功。
- 使用
-
填充新数组:
- 再次遍历输入数组,将满足条件的元素添加到新数组中。
-
返回新数组和长度:
- 返回新的数组指针,更新
resultLength指向的值。
- 返回新的数组指针,更新
运行示例
运行上述代码后,输出结果应为:
1 2 3 4 5 6
7. 静…态…
- 如何理解关键字
static? static与变量结合后有什么作用?static与函数结合后有什么作用?static与指针结合后有什么作用?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 类型的参数。
数据类型的含义总结
-
int (*p)[10];p是一个指向具有 10 个int元素的数组的指针。
-
const int* p[10];p是一个数组,包含 10 个指向const int的指针。
-
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 = 0,j = 1,k = 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表示在逻辑运算后的变量值,反映了如何通过短路求值影响了变量的最终状态。
总结
- 这个程序展示了如何使用命令行参数和逻辑运算符,并演示了逻辑短路的效果。
- 由于
argc在while循环中被人为修改,程序最终输出了一些可能不太直观的结果。
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);
}
宏的展开
- 宏定义:
CAL(a)被定义为a * a * a。- 当我们在
if语句中使用CAL(2)时,它会展开为2 * 2 * 2。
计算条件
-
条件判断:
if (16 / CAL(2) == 2)- 展开后为:
if (16 / (2 * 2 * 2) == 2)- 由于宏没有加括号,运算会按照优先级进行计算:
16 / 2 * 2 * 2- 因此,计算过程如下:
16 / 2先计算,结果为8。- 然后
8 * 2,结果为16。 - 最后
16 * 2,结果为32。
这使得条件变为:
if (32 == 2)- 这个条件是 假 的。
执行流程
-
else语句:- 由于
if条件为假,程序执行else块中的代码:
int nums = MAGIC_CAL(++nums, 2);++nums将nums从1增加到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。
- 由于
-
最终输出:
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;
}
问题分析
-
内存分配:
- 在
initializeStudent函数中,s1.name和s2.name分别通过malloc动态分配了内存,存储学生的姓名。 - 这意味着
s1.name和s2.name指向不同的内存区域。
- 在
-
直接赋值的问题:
- 当执行
s1 = s2;时,整个struct Student结构体被复制。 - 这包括
age和name的指针。结果是s1.name现在指向与s2.name相同的内存地址。 - 由于
s2.name指向的内存是动态分配的,如果在后面free(s1.name);被调用,s2.name也会被错误地释放,导致悬挂指针(dangling pointer)问题。
- 当执行
-
内存释放的问题:
- 当
free(s1.name);被调用时,s2.name也被释放了。这将导致访问s2.name时出现未定义行为,因为它指向的内存已被释放。
- 当
如何解决这个问题
为了避免上述问题,可以选择以下方法之一:
-
手动复制字符串:在结构体赋值前手动复制字符串,以确保每个结构体都有自己的内存。
-
使用函数复制结构体:创建一个专门的函数来复制
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);
}
主要结构体分析
- 结构体定义:
struct structure中包含一个int foo、一个union node(包含一个int、一个char数组和一个指针)、一个short bar、一个long long baz以及一个长度为 7 的int array。- 由于
union的特性,node只会占用其最大成员的空间,因此node的大小等于void *的大小(在 32 位系统中通常是 4 字节,在 64 位系统中通常是 8 字节)。
数据安排
- 数组初始化:
arr数组包含多个int值,这些值以十六进制表示,实际上是在内存中按字节排列。- 每个
int占用 4 个字节,因此arr数组的总大小是 18 个int,即 72 字节。
指针转换与输出
-
类型转换:
((struct structure *)arr)将arr数组的地址转换为struct structure *类型。这意味着我们将访问arr的内存空间,就像它是一个struct structure类型的变量。
-
访问
node.string:((struct structure *)arr)->node.string试图访问node.string,而node.string是一个字符数组,长度为 11。- 由于
arr中的整数是用十六进制存储的,输出字符串实际上是从arr中相应的内存位置提取的字符,可能是 ASCII 字符。
输出结果
- 输出字符串:
printf("%s\n", ...)将输出node.string中的字符,这些字符的值由arr中的整数表示。- 由于
arr中的值被解释为字符,实际输出可能是一些 ASCII 字符的组合,具体取决于内存中这些值的字节表示。
内存敏感性
-
由于 C 语言直接操作内存,程序员必须对内存布局和结构体的字节对齐非常敏感。错误的内存操作可能导致未定义行为,例如:
- 溢出:未正确处理字符串末尾的空字符
\0。 - 内存对齐:不同数据类型可能有不同的对齐要求。
内存溢出和内存对齐是计算机内存管理中两个重要的概念。以下是对这两个概念的简述:
- 溢出:未正确处理字符串末尾的空字符
内存溢出(Memory Overflow)
定义:
内存溢出是指程序在运行时试图使用超过其分配的内存空间的情况。这通常会导致程序崩溃、数据损坏或不可预测的行为。
原因:
- 数组越界:访问数组的非法索引。
- 动态内存分配:使用
malloc、calloc等函数分配内存时,未检查返回值。 - 递归过深:过多的递归调用导致栈空间不足。
- 逻辑错误:程序逻辑错误导致分配的内存未被正确使用。
后果:
- 程序崩溃。
- 数据丢失或损坏。
- 安全漏洞(如缓冲区溢出攻击)。
内存对齐(Memory Alignment)
定义:
内存对齐是指将数据存放在内存中的特定边界上,以提高 CPU 存取数据的效率。不同数据类型通常有不同的对齐要求。
原因:
- 硬件架构:许多 CPU 在读取内存时,要求数据存储在特定的地址(如偶数地址)上,这样可以提高访问速度。
- 性能优化:对齐数据可以减少 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 ~:切换到当前用户的主目录。
符号的含义
-
/:- 表示根目录,是文件系统的最顶层。所有其他目录都在根目录下。
- 例如,
/home/user表示home目录下的user目录。
-
.:- 表示当前目录。
- 例如,
./file.txt指的是当前目录下的file.txt文件。
-
~:- 表示当前用户的主目录。
- 例如,
~/documents指的是当前用户主目录下的documents目录。
其他 GNU/Linux 相关知识
-
文件和目录权限:
- 每个文件和目录都有权限设置(如
rwx),控制谁可以读取、写入或执行。
- 每个文件和目录都有权限设置(如
-
管道和重定向:
- 使用
|管道符将一个命令的输出传递给另一个命令。 - 使用
>将输出重定向到文件,例如ls > file.txt。
- 使用
-
环境变量:
- 系统和用户定义的变量,影响程序的行为,例如
PATH环境变量决定可执行文件的搜索路径。
- 系统和用户定义的变量,影响程序的行为,例如
-
包管理器:
- 不同的 Linux 发行版有不同的包管理器(如
apt、yum、dnf)来安装和管理软件包。
- 不同的 Linux 发行版有不同的包管理器(如
-
Shell 脚本:
- 使用
.sh扩展名的脚本文件,可用于自动化任务。
- 使用
-
进程管理:
- 使用
ps查看当前运行的进程,使用kill命令结束进程。
- 使用
-
文件查找:
- 使用
find和locate命令查找文件和目录。
- 使用
-
网络命令:
- 使用
ping、curl、wget等命令进行网络测试和下载。
- 使用
:::tip 结语
恭喜你攻克所有难关!迎难而上的决心是我们更为看重的。
来到这里的人已是少数,莫踌躇在成功的门槛前。
自信一点,带上你的笔记本电脑,来东区逸夫楼FZ103面试吧!
:::
排版:纸鹿,有问题扣他鸡腿。{style=“font-size: .8em; opacity: .5;”}
743

被折叠的 条评论
为什么被折叠?



