西邮Linux兴趣小组2023纳新
西邮 Linux 兴趣小组 2023 纳新面试题
学长寄语:长期以来,西邮Linux兴趣小组的面试题以难度之高名扬西邮校内。我们作为出题人也清楚的知道这份试题略有难度。请你动手敲一下代码。别担心,若有同学能完成一半的题目,就已经十分优秀。其次,相比于题目的答案,我们对你的思路和过程更感兴趣,或许你的答案略有瑕疵,但你正确的思路和对知识的理解足以为你赢得绝大多数的分数。最后,做题的过程也是学习和成长的过程,相信本试题对你更加熟悉的掌握C语言的一定有所帮助。祝你好运。我们东区逸夫楼FZ103见!
- 本题目只作为西邮Linux兴趣小组2023纳新面试的有限参考。
- 为节省版面,本试题的程序源码省去了
#include指令。 - 本试题中的程序源码仅用于考察C语言基础,不应当作为C语言「代码风格」的范例。
- 所有题目编译并运行于
x86_64 GNU/Linux环境。
0. 鼠鼠我啊,要被祸害了
有1000瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水,24小时后就会准时死亡。至少要多少只小白鼠才能在24小时内鉴别出哪瓶水有毒?
题解:
要解决1000瓶水的测毒问题,至少需要10只小白鼠,核心原理是二进制编码——用老鼠的“存活/死亡”状态对应二进制位的“0/1”,从而通过组合结果定位有毒瓶。
每只小白鼠对应二进制数的一位(比如第1只对应第1位,第2只对应第2位……第10只对应第10位)。
给老鼠分配“喝法”
对每一瓶水,将其编号转为10位二进制数
若某一位是1,则让对应位置的老鼠喝这瓶水的一点样本;
若某一位是0,则不让对应老鼠喝。
例如:
第5瓶水(二进制0000000101):让第1只和第3只老鼠喝;
第100瓶水(二进制0001100100):让第3只、第6只、第7只老鼠喝。
根据死亡情况反推有毒瓶
24小时后,观察老鼠的存活状态:
若某只老鼠死亡,则对应二进制位记为1;
若存活,则记为0。
将所有位组合成二进制数,再转为十进制,就是有毒瓶的编号。
例如:
若第1、3只老鼠死亡,其他存活 → 二进制0000000101 → 对应第5瓶有毒。
结论验证:
10位二进制能覆盖1024种状态(2¹⁰ = 1024),完全满足1000瓶的需求,因此10只老鼠是最小值(若用9只,仅能覆盖512种状态,不够1000瓶)。
1. 先预测一下~
按照函数要求输入自己的姓名试试~
char *welcome() {
// 请你返回自己的姓名
}
int main(void) {
char *a = welcome();
printf("Hi, 我相信 %s 可以面试成功!\n", a);
return 0;
}
题解:
【温馨提示:若系统程序报错建议在将char类型改为const char类型
原因:可能的“假报错”说明
如果添加头文件后仍有警告(非错误),比如「returning ‘const char*’ from a function with return type ‘char*’」
(返回const char* 但函数声明为char * )
——这是编译器的“善意提醒”(字符串常量默认是const类型),但不影响代码运行(多数编译器会兼容这种写法)。
若想彻底消除警告,可将函数返回类型改为const char* :】
- 程序启动,进入main函数
main函数是C程序的入口,所有代码从这里开始执行。此时程序的执行上下文(如变量、函数地址)被加载到内存中。 - 调用welcome函数,获取姓名字符串的地址
执行代码char * a = welcome(); 时,程序会跳转到welcome函数:执行return “叶露斌”; 语句。
返回字符串地址:"叶露斌"是字符串常量(固定不可修改的字符串),存储在程序的静态存储区(不会随函数结束而销毁)。return语句将这个字符串的首地址(即字符’叶’的内存地址)返回给main函数。
赋值给变量a:char * a表示a是一个字符指针(指向字符的地址),此时a指向字符串"叶露斌"的首地址。 - 调用printf函数,输出鼓励语
执行代码printf(“Hi, 我相信 %s 可以面试成功!\n”, a); 时:
printf是标准库函数(来自<stdio.h>),用于输出文本。
%s是格式符,表示“输出字符指针指向的字符串”:
程序会从a指向的地址(即"叶露斌"的首地址)开始,逐个读取字符并输出,直到遇到字符串的结束符’\0’(字符串常量默认在末尾隐藏’\0’,用于标记字符串结束)。
最终输出结果:Hi, 我相信 叶露斌 可以面试成功! - 程序正常结束
执行return 0; 时,main函数返回0(表示程序无错误),整个程序运行完毕。
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);
}
题解:
1.if语句成立:* ptr0和* ptr1分别表示指针指向的第一个字符:
- ptr0 → 首字符’W’;
- ptr1 → 首字符’W’;
两者字符值相同,因此if条件成立,进入分支执行嵌套printf。
2.嵌套printf:
最内层开始,输出0
然后输出Hello, Linux Group - 20
紧接着因为共23个字符,所以输出Hello, Linux Group - 2023
3.ptr0与ptr1的地址差
ptr0指向静态区,ptr1指向栈区,两者的地址差由编译器的内存布局决定
(栈向下生长,静态区在代码区之后,因此ptr0的地址通常大于ptr1,差值为正数或负数均有可能)。
即diff可为任意值
4.最终输出:(其中一种结果)
Hello, Linux Group - 2023
Pointer Difference : 1538110064
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));
}
题解:
1.第一个printf的输出
sizeof(&arr):&arr是数组的地址(类型为char()[7],指向整个数组),&arr等价于arr本身(数组名)。arr是7个char的数组,每个char占1字节,因此结果为7。
sizeof(arr + 0):数组名arr与整数0相加时,会退化为指针(指向数组第一个元素,类型为char )。64位系统中指针占8字节,因此结果为8。
sizeof(num = num2 + 4):sizeof是编译时运算符,只关注变量的类型,不执行赋值操作。num是short类型(占2字节),因此结果为2。
输出:7 8 2
2. 第二个printf的输出
sprintf的作用:将格式化字符串写入str数组。此处格式是"0x%x"(十六进制,前缀0x),num=520转十六进制为0x208,因此str会被写入"0x208"(共5个字符:‘0’、‘x’、‘2’、‘0’、‘8’)。
sprintf的返回值:成功写入的字符数(不包括终止的’\0’),此处返回5。
条件判断:5 == 520为假,因此输出0。
3. 第三个printf的输出
核心是strlen的功能(计算字符串长度,到’\0’为止):
strlen(&str[0] + 1):& str[0]是str第一个元素的地址, + 1后指向第二个元素(即’x’)。str中的内容是"0x208",跳过第一个字符后剩余"x208",长度为4。
strlen(arr + 0):arr + 0是arr的首地址,arr中的’\0’位于第5个元素(索引5),因此字符串有效长度为5(“Linux”)。
输出:4 5
4.故最终输出: 7 8 2
0
4 5
5.sizeof()与strlen()的区别有:
1.sizeof()会读取字符串中的\0,而strlen不会读取\0;
2.本质不同:sizeof是关键字或运算符(编译时计算),strlen是函数(运行时计算)
3.sizeof计算的是字节数,strlen计算的是字符数。
4.适用范围不同:sizeof适用于所有类型(数组、指针、结构体等)strlen仅适用于以\0 结尾的字符串(char*/char数组)
6.sprintf是C标准库中的字符串格式化函数,用于将“格式化的数据”写入字符数组(类似printf,但输出到字符串而非终端)。
1. 函数原型
int sprintf(char* str, const char* format, …);
2. 参数说明
char* str:目标字符数组(必须有足够空间存储结果,否则会溢出导致未定义行为)。
const char* format:格式字符串(与printf一致,如"0x%x"表示十六进制并前缀0x)。
…:可变参数(对应格式字符串中的占位符,如num对应 % x)。
3. 返回值
成功:返回写入的字符数(不包括终止的’\0’)。例如sprintf(str, “0x%x”, 520)返回5(对应字符串"0x208"的长度)。
失败:返回负数(如格式字符串错误、目标数组溢出等,但sprintf不检查溢出,需手动保证缓冲区大小)。
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);
}
题解:
首先明确:1.^为按位异或运算,相同为0,不同为1
2.&为按位与运算,相同位都是1才是1,否则为0
3.>>右移
1.计算a = 64 & 127
二进制转换:
64 → 01000000(正数,原码 = 补码);
127 → 01111111(正数,原码 = 补码)。
计算结果:01000000 → 64(在char范围内,直接存储)。
2.计算b = 64 ^ 127
二进制转换:
64 → 01000000;
127 → 01111111。
计算结果:00111111 → 63(在char范围内,直接存储)。
- 计算c = -64 >> 6
关键规则:有符号数右移是“算术右移”(符号位填充高位,保持数值正负)。
步骤:
- 64的补码:signed char中,64的原码是01000000,取反加1得到补码11000000(最高位1表示负数)。
右移6位:算术右移时,符号位(1)填充高位,右移6位后结果为11111111(补码)。
补码转原码:11111111是 - 1的补码(signed char中, - 1的补码 = 原码 = 全1)。
最终结果:c = -1。
- 计算ch = a + b - c
代入数值:ch = 64 + 63 - (-1) = 64 + 63 + 1 = 128。
关键问题:signed char的溢出(范围 - 128 ~127,128超出上限)。
溢出处理:signed char溢出后,按模256规则循环(计算机底层的补码特性)。128的二进制是10000000,而signed char中10000000恰好是 * *-128 * *的补码(signed char的最小值)。
5.对于11111111的十进制是-1(有符号运算下)的反证:
若x = -1,则x的补码 = 原码(10000001)取反加1 → 11111110 + 1 = 11111111;
反推:11111111是补码,要转十进制,需看符号位(1→负数),则原码 = 补码取反加1:
取反:11111111 → 00000000;
加1:00000000 + 1 = 00000001;
原码是10000001(符号位1 + 数值位1),对应十进制 - 1。
6.故最终输出:a = 64 b = 63 c = -1
ch = -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)));
}
题解:
4的二进制:00000100
9的二进制:00001001
-7的二进制:11111001
1.首先明确:
不进位加法:用** 异或(^ )** 实现——只有当两位不同时为1时,才会产生“不进位的和”(比如1 + 0 = 1、0 + 1 = 1、0 + 0 = 0,而1 + 1 = 0(不进位))。
进位计算:用 * *与( & ) + 左移1位( << 1) * *实现——只有当两位都为1时,才会产生进位,左移1位表示“进位到高位”(比如1 + 1 = 10,进位是10)。
2.递归终止条件
函数的递归逻辑是:不断把“进位”加到“不进位和”上,直到没有进位为止(此时“不进位和”就是最终结果)。
当a == 0时,说明没有进位了,直接返回当前的“不进位和”b。
3.递归核心步骤
(1)计算新的进位:(a & b) << 1(当前进位左移,传递到高位);
(2)计算新的不进位和:a ^ b(当前的不进位结果);
(3)用“新的进位”和“新的不进位和”再次调用func,直到进位为0。
4.题目代码程序逻辑:
首先计算func(b, c) = func(9, -7)
初始值:a = 9(二进制00000000 00000000 00000000 00001001),b = -7(补码11111111 11111111 11111111 11111001)。
第一次递归:
进位:(9 & -7) << 1 = 0x09 << 1 = 0x12 (二进制00010010);
不进位和:9 ^ -7 = 0xFFFFFFF0(二进制11111111 11111111 11111111 11110000);
调用func(0x12, 0xFFFFFFF0)。
多次递归后:进位最终会变成0,此时不进位和就是2(9 + (-7)的结果)。
第二步:计算func(a, 2) = func(4, 2)
初始值:a = 4(00000100),b = 2(00000010)。
进位:(4 & 2) << 1 = 0 << 1 = 0(无进位);
不进位和:4 ^ 2 = 6(00000110);
因为a = 0,直接返回6。
5.故最终输出:6
6.题目过程补充:func(9, -7)的递归过程(具体过程)
我们的目标是计算9 + (-7) = 2,递归的每一步都在用“新的进位”和“新的不进位结果”重新计算加法,直到进位为0(此时不进位结果就是最终和)。
1. 第一次递归:func(9, -7)
条件判断:a = 9≠0(存在进位,需继续处理);
计算进位:(9 & -7) << 1
9 & -7:按位与,仅当两位均为1时结果为1。9的二进制是00001001, - 7的补码是11111001,按位与得00001001(即9);
左移1位:9 << 1 = 18(进位是18,需加到高位);
计算不进位结果:9 ^ -7
异或,两位不同时结果为1。9的00001001与 - 7的11111001异或得11110000(即 - 16,对应“不进位的加法结果”);
递归调用:func(18, -16)(此时18 + (-16) = 2,和原问题的和一致)。
2. 第二次递归:func(18, -16)
条件判断:a = 18≠0(仍有进位);
计算进位:(18 & -16) << 1
18 & -16:18的二进制是00010010, - 16的补码是11110000,按位与得00010000(即16);
左移1位:16 << 1 = 32(进位是32);
计算不进位结果:18 ^ -16
异或得11100010(即 - 30,对应“不进位的加法结果”);
递归调用:func(32, -30)(此时32 + (-30) = 2,和不变)。
3. 第三次递归:func(32, -30)
条件判断:a = 32≠0(但此次进位会消失);
计算进位:(32 & -30) << 1
32 & -30:32的二进制是00100000, - 30的补码是11100010,无共同的1位,按位与得0;
左移1位仍为0(进位消失);
计算不进位结果:32 ^ -30
此时进位为0,根据恒等式,32 ^ -30 = 32 + (-30) = 2(不进位结果就是最终和);
递归调用:func(0, 2)(触发终止条件)。
4. 第四次递归:func(0, 2)
条件判断:a = 0(无进位,无需继续);
直接返回:b = 2(此时“不进位结果”就是9 + (-7)的最终和)。
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;
}
题解:#include <stdlib.h> 包含malloc、free的头文件
typedef int (*Predicate)(int);
int* filter(int* array, int length, Predicate predicate, int* resultLength) {
步骤1:统计满足条件的元素个数(确定结果数组的长度)
int count = 0;
for (int i = 0; i < length; i++) {
if (predicate(array[i])) { 函数指针判断元素是否满足条件(非0为真)
count++;
}
}
步骤2:将结果数组的长度通过指针返回给调用者
*resultLength = count;
步骤3:为结果数组动态分配内存(大小为count个int)
int* result = (int*)malloc(count * sizeof(int));
if (result == NULL) { (可选)处理内存分配失败的情况
return NULL;
}
步骤4:填充结果数组(再次遍历原数组,复制满足条件的元素)
int j = 0; 结果数组的索引
for (int i = 0; i < length; i++) {
if (predicate(array[i])) {
result[j++] = array[i];
}
}
步骤5:返回结果数组的指针
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);
for (int i = 0; i < resultLength; i++) {
printf(“%d “, filteredNumbers[i]);
}
printf(”\n”);
free(filteredNumbers);
return 0;
}
7. 静…态…
- 如何理解关键字
static? static与变量结合后有什么作用?static与函数结合后有什么作用?static与指针结合后有什么作用?static如何影响内存分配?
题解:
(1)关键字static能够让变量能够被引用的范围改变(作用域改变),有时能使其他文件无法调用其修饰的对象。
其次,static使得变量仅初始化一次,即在某些循环中static修饰的变量与未被其修饰的变量最后输出的值大不相同
(2)static修饰变量时,会改变变量的存储位置和作用域,分两种情况:
1. 全局静态变量
定义:在函数外(全局作用域)用static修饰的变量
作用:
限制作用域:仅能在定义它的源文件(.c)内访问,其他文件无法通过extern引用(避免不同文件的同名全局变量冲突);
存储周期:属于静态存储区,程序启动时分配内存,程序结束时释放,初始化仅一次(默认初始化为0)。
2. 局部静态变量
定义:在函数内(局部作用域)用static修饰的变量
作用:
延长存储周期:变量存储在静态存储区,而非函数栈,因此函数调用结束后,变量值会保留(下次调用时不会重新初始化);
作用域不变:仍仅能在定义它的函数内访问。
(3)定义:用static修饰的函数,也称为内部函数。
作用:
限制作用域:仅能在定义它的源文件内被调用,其他文件无法通过extern引用(避免不同文件的同名函数冲突);
存储周期:函数代码本身存储在代码段(只读),static不改变代码的存储位置,仅限制访问范围。
(4)static修饰指针变量时,本质是修改指针变量本身的存储属性,而非指针指向的对象。分两种常见情况:
1. 局部静态指针
定义:函数内用static修饰的指针变量(如void func() { static int* p; })。
作用:指针变量p的存储周期是静态的(程序启动到结束),函数返回后p不会被销毁,下次调用时仍保留上次的地址。
2. 全局静态指针
定义:函数外用static修饰的指针变量(如static int* g_p; )。
作用:指针变量的作用域限制为本文件,存储周期为静态(程序启动到结束),指向的对象可以是动态内存或静态变量。
(5)static如何影响内存分配?
C语言中变量的存储位置分为三类:
变量类型 存储区域 分配时机 释放时机 初始化默认值
自动变量(auto) 栈(Stack) 函数调用时 函数返回时 随机值
静态变量(static) 静态存储区(Data / BSS) 程序加载时 程序结束时 0
动态变量(malloc) 堆(Heap) 调用malloc时 调用free时 随机值
static变量的内存分配:
静态变量(无论全局还是局部)都存储在静态存储区,程序启动时由操作系统分配内存,初始化仅一次;
静态存储区的大小在程序编译时确定,不会随函数调用 / 返回而变化,因此静态变量的生命周期是整个程序运行期间。
8. 救命!指针!
数组指针是什么?指针数组是什么?函数指针呢?用自己的话说出来更好哦,下面数据类型的含义都是什么呢?
int (*p)[10];
const int* p[10];
int (*f1(int))(int*, int);
题解:
1.数组指针就是指向数组的指针
2.指针数组就是存储指针的数组
3.函数指针就是指向函数的指针(指向函数的代码在内存中的起始地址,通过它能直接调用函数。
关键:指针指向的是“函数的代码段”,不是变量。)
int (p)[10] 是数组指针(指向“10个int的数组”)
const int p[10] 是指针数组(装了10个“指向const int的指针”)
int (f1(int))(int, int) 是函数指针(返回“函数指针”的函数))
(最内层:f1(int)→f1是一个函数,参数是int(比如f1(5));
中间层:(f1(int))→这个函数的返回值是一个指针( 表示指针);
最外层:int(…) (int*, int)→指针指向的是另一个函数:
这个函数的参数是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;
}
题解:一、先明确argc的初始值
main函数的argc是命令行参数的数量:
如果直接运行程序(没有额外参数),argc默认是1(程序本身是第一个参数)。
二、循环为什么会结束?
while (argc)的条件是“argc非0”,但argc是有符号int(默认32位),会经历以下过程:
正数阶段:argc从1开始,++argc不断增加到INT_MAX(2147483647,32位int的最大值);
溢出阶段:INT_MAX + 1会触发溢出,变成INT_MIN( - 2147483648,最小负整数);
负数递增到0:argc从INT_MIN继续++,逐渐增加到 - 1,再++就变成0——此时while条件不满足,循环结束。
三、关键变量的变化(循环结束后)
循环结束时,argc = 0,接下来执行:
int i = -1, j = argc, k = 1; i=-1,j=0,k=1
四、逻辑表达式i++ && j++ || k++的运算(重点!)
C语言中, && (逻辑与)和 || (逻辑或)具有短路特性:
A && B:如果A为假,直接跳过B(因为结果肯定是假);
A || B:如果A为真,直接跳过B(因为结果肯定是真)。
逐步计算:
先算i++ && j++:
i++是后置递增:先取i = -1(非0,真),再把i变成0;
因为i++为真,需要继续算j++:取j = 0(假),再把j变成1;
所以i++ && j++的结果是 - 1 && 0 → 假(0)。
再算(假) || k++:
左边是假,需要继续算k++:取k = 1(非0,真),再把k变成2;
所以0 || 1的结果是真(1)。
五、最终输出结果
循环结束后,变量的值是:
i = 0(i++后);
j = 1(j++后);
k = 2(k++后)。
总结程序的输出
程序运行后(无命令行参数),最终输出:
[1] 初始argc=1的输出
i = 0, j = 1, k = 2
注:循环过程在实际中会消耗大量时间(需要数亿次迭代),看起来像“卡住”,但理论上会结束。
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);
}
题解:从main函数开始,得知nums为1,进入if判断,因为调用宏是没有加括号,最终运算结果为(16/2)22,不符合条件,
进入else语句,对nums变量重新赋值,nums三次自增,自增到4,最终表达式为444+222结果为72
所以最终输出72
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;
}
题解:
要理解这段代码的问题,需要先明确结构体“浅拷贝”的致命缺陷,再通过深拷贝解决问题。
一、原程序的问题:浅拷贝导致“双重释放”
直接执行s1 = s2; 是结构体浅拷贝——即逐个复制结构体的成员,但指针成员仅复制地址,不复制地址指向的内容。
-
原程序的执行流程与错误根源
我们结合代码逐行分析: -
初始化s1和s2:为name动态分配内存
initializeStudent(&s1, “Tom”, 18); s1.name指向"Tom"的内存(地址A)
initializeStudent(&s2, "Jerry", 28); s2.name指向"Jerry"的内存(地址B)
- 浅拷贝:s1 = s2
s1 = s2; 逐个复制成员:
- s1.age = s2.age(没问题,值复制)
- s1.name = s2.name(指针复制!s1.name现在也指向地址B)
- 打印:s1和s2的name都指向"Jerry"
printf(“s1的姓名: %s 年龄: %d\n”, s1.name, s1.age); 输出s1.name=“Jerry”
printf("s2的姓名: %s 年龄: %d\n", s2.name, s2.age); 输出s2.name="Jerry"
- 致命错误:双重释放同一块内存
free(s1.name); 释放地址B的内存(正确)
free(s2.name); 再次释放地址B的内存!!!(未定义行为:程序崩溃/异常)
-
核心问题总结
直接赋值(s1 = s2)是浅拷贝,指针成员name仅复制地址,而非地址指向的内容。
最终s1.name和s2.name指向同一块动态分配的内存,导致free时重复释放(Double Free),触发程序崩溃或不可预测的错误。
二、解决方法:实现“深拷贝”
结构体中存在动态分配的成员(如name)时,必须手动实现深拷贝——为目标结构体的指针成员重新分配内存,并复制源内容。 -
步骤1:编写深拷贝函数
定义copyStudent函数,为目标结构体的name重新分配内存,复制源结构体的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;}
深拷贝函数:将src的内容深拷贝到destvoid copyStudent(struct Student* dest, const struct Student* src) {1. 复制age(值复制,直接赋值)dest->age = src->age;
2. 复制name(深拷贝:重新分配内存+复制内容)
先释放dest原有的name内存(如果有的话,避免内存泄漏)
if (dest->name != NULL) {
free(dest->name);
}
dest->name = (char*)malloc(strlen(src->name) + 1); 分配新内存
strcpy(dest->name, src->name); 复制字符串内容
}
2. 步骤2:用深拷贝代替直接赋值在main函数中,用copyStudent(&s1, &s2)代替s1 = s2; ,确保s1.name有独立的内存:
int main(void) {struct Student s1, s2;initializeStudent(&s1, "Tom", 18);initializeStudent(&s2, "Jerry", 28);
用深拷贝代替直接赋值
copyStudent(&s1, &s2); s1的name将指向新分配的"Jerry"内存
printf("s1的姓名: %s 年龄: %d\n", s1.name, s1.age); s1.name="Jerry"
printf("s2的姓名: %s 年龄: %d\n", s2.name, s2.age); s2.name="Jerry"
现在free不会重复释放!
free(s1.name); 释放s1的name内存(独立)
free(s2.name); 释放s2的name内存(独立)
return 0;
}
三、关键结论
结构体直接赋值(浅拷贝):仅适用于无动态分配成员的结构体(如仅包含int、char等基本类型)。
结构体深拷贝:当结构体包含动态分配的指针成员(如malloc的字符串)时,必须手动实现深拷贝函数,确保指针成员指向独立的内存块。
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);
}
题解:
一、结构体星际地图(64位系统)
struct structure {
int foo; 坐标0x0 (4字节小行星)
union node { 坐标0x4 (12字节星云)
char string[11]; 主要观测目标✨
…
};
short bar; 坐标0x10 (2字节流星+2字节暗物质填充)
long long baz; 坐标0x18 (8字节恒星)
int array[7]; 坐标0x20 (28字节星团)
}; 总跨度60光年
二、关键数据虫洞
((struct structure*)arr)->node.string
这行代码创建了一个「星际虫洞」:
将整型数组的0x4~0xF区域(12字节)映射到联合体的字符数组空间
三、星图解码手册
原始十六进制数据:
0x590ff23c, 0x2fbc5a4d, 0x636c6557->前3个int = 12字节
按小端序展开为ASCII:
0x3c 0xf2 0x0f 0x59 → “<→→Y” (干扰区)
0x4d 0x5a 0xbc 0x2f → “MZ»/”
0x57 0x65 0x6c 0x63 → “Welc”
…后续数据拼接…
有效字符串从第三个int开始:
57 65 6c 63 → W e l c
6f 6d 65 20 → o m e[空格]
…最终组合成…
“Welcome to XTPU, we welcome you to Linux Group [2023]”
13. GNU/Linux (选做)
注:嘿!你或许对Linux命令不是很熟悉,甚至你没听说过Linux。但别担心,这是选做题,了解Linux是加分项,但不了解也不扣分哦!
你知道cd命令的用法与 / . ~ 这些符号的含义吗?
请问你还懂得哪些与 GNU/Linux 相关的知识呢~
GNU / Linux 核心知识全景图
一、路径符号
符号 名称 含义 应用场景示例
/ 根目录 文件系统的绝对起点 cd / usr / local
.当前目录 代表命令执行的上下文位置 . / configure(执行当前目录脚本)
…父目录 返回上一级目录的快捷方式 cd …
~用户主目录 快速回到当前用户专属空间 vim ~/ .bashrc
- 前工作目录 在两个目录间快速切换 cd -
二、cd 命令时空跃迁指南
基础跃迁
$cd / var / log 绝对路径跃迁(跨星系传送)
$
cd … / src 相对路径跃迁(本地时空穿梭)
高阶技巧
$ cd !$ # 重用上条命令的路径参数
$ cd $(dirname $(which gcc)) # 跳转到gcc所在的父目录
apt / yum / dnf(Debian / RedHat系)
编译安装三部曲:. / configure && make && make install
开发工具链
gcc编译流程:预处理->编译->汇编->链接
gdb调试器核心命令:break / next / print / backtrace
所有评论(0)