C 语言指针练手:解 4 嫌疑人谋杀案,从冗余到简洁的优化之路
逻辑推理 + C 语言(新手指针练习版)
作为刚接触 C 语言指针的新手,我总想找机会练手指针用法 —— 哪怕代码冗余也没关系,先把指针用起来!这次以 “4 嫌疑人谋杀案(3 真 1 假找凶手)” 为例,我刻意用指针写了初始版本,再结合查资料学到的指针知识一步步优化,既解了题,又吃透了指针的核心用法,分享一下新手练习指针的全过程。
一、题目背景:谋杀案逻辑推理题
1.1题目描述
某地发生谋杀案,警察确定凶手是 4 个嫌疑犯(A、B、C、D)中的一人。4 人的供词如下:
- A 说:不是我。
- B 说:是 C。
- C 说:是 D。
- D 说:C 在胡说。
已知条件:3 个人说了真话,1 个人说的是假话。请编写程序找出凶手。
1.2题目逻辑分析
写代码前先理逻辑,这是新手避免混乱的关键!我先给嫌疑人做索引映射(方便指针 / 数组操作):A=0、B=1、C=2、D=3,设凶手索引为killer_idx(取值 0-3),把供词转化为指针 / 数组能操作的逻辑表达式(1 = 真,0 = 假):
表格
| 嫌疑人 | 供词 | 逻辑表达式 |
|---|---|---|
| A(0) | 不是我 | killer_idx != 0 |
| B(1) | 是 C | killer_idx == 2 |
| C(2) | 是 D | killer_idx == 3 |
| D(3) | C 在胡说 | killer_idx != 3 |
核心条件:4 个表达式结果之和 = 3(恰好 3 真 1 假)。
1.3练指针的初衷
我写初始代码时,刻意绕了点路 —— 不用简单的数组下标,而是用指针*(p+i)操作数组,就是为了练习指针和数组的等价关系(arr[i] = *(arr+i)),哪怕代码冗余,先把指针用熟再说!
二、第一版代码:为练指针刻意保留冗余
初始版本我写得很 “臃肿”,但核心目的是练指针,所以保留了所有指针变量,只通过查资料修正了致命的语法错误,确保代码能跑。
2.1查资料踩的第一个坑
写sofe函数时,我误把判断相等的==写成了赋值=(if(arr[i]=1)),运行后程序完全不对。查 C 语言指针 + 条件判断的资料才发现:=是赋值,==才是判断,这是新手用指针 / 数组最容易踩的语法坑!
2.2第一版代码(刻意保留指针,仅修正语法)
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> // 打印凶手(用指针思路遍历数组,练*(arr+i)) void print(int arr[],int n) { // 练习指针:用指针遍历数组,替代arr[i] int *p = arr; for(int i=0;i<n;i++) { if(*(p+i) == 1) // 刻意用*(p+i),练指针偏移 { if(i==0) printf("凶手是:A\n"); else if(i==1) printf("凶手是:B\n"); else if(i==2) printf("凶手是:C\n"); else if(i==3) printf("凶手是:D\n"); } } } // 统计真话数量(查资料修正:赋值→判断) int sofe(int arr[],int n) { int count = 0; int *p = arr; // 练习指针:用指针遍历统计 for(int i=0;i<n;i++) { if(*(p+i) == 1) // 修正:把=改成== { count++; } } return count; } // 核心推理函数(全程用指针操作arr1,练*(p+i)) void Jud(int arr1[], int arr2[],int n) { int flag1 = 0; // 冗余变量,先保留,后续优化 int flag2 = 0; int* p2 = arr2; // 冗余指针,刻意保留,先练指针声明/赋值 int killer_idx = -1; // 新增:存储凶手索引 // 遍历每个嫌疑人,假设i是凶手 for(int i=0;i<n;i++) { int* p = arr1; // 核心:用指针操作arr1存储供词真假 // 练指针偏移:*(p+0)等价于arr1[0] *(p+0) = (i != 0); // A:不是我 *(p+1) = (i == 2); // B:是C *(p+2) = (i == 3); // C:是D *(p+3) = (i != 3); // D:C在胡说 flag2 = sofe(p, n); // 指针传参:p是arr1的地址 if (flag2 == 3) // 3真1假,找到凶手 { flag1 = 1; killer_idx = i; break; } } // 练指针重置数组:用指针把arr1所有元素置0 int *p_reset = arr1; for(int i=0;i<n;i++) { *(p_reset+i) = 0; } *(arr1 + killer_idx) = 1; // 指针标记凶手 print(arr1, n); } int main() { int arr1[4] = { 1,1,1,1 }; // 存储供词真假(指针操作的载体) int arr2[4] = { 0,0,0,0 }; // 冗余数组,练指针传参 int n = sizeof(arr1) / sizeof(arr1[0]); Jud(arr1, arr2,n); // 指针传参:数组名本质是首元素地址 return 0; }2.3第一版思考过程
- 刻意设计:全程用
*(p+i)替代arr[i],就是为了吃透 “指针偏移 = 数组下标” 这个核心知识点,哪怕代码绕弯; - 查资料修正:
- 最初写
*(p+i)=1判断时,程序统计结果全错,查 C 语言指针条件判断的资料,才发现把==写成了=; - 查 “逻辑推理题解法”,确认核心条件是 “3 真 1 假”,把
flag2==1改成flag2==3;
- 最初写
- 冗余保留:
p2、flag1、arr2这些没用的指针 / 数组,我先保留 —— 新手练指针,先 “能用”,再 “精简”,一步改太多容易忘知识点。
运行结果:凶手是:C,指针操作没出错,核心逻辑也对,第一步练指针的目标达成!
三、简化步骤 1:查资料优化指针使用(删冗余,保核心)
第一版能跑,但很多指针是 “无效练习”(比如p2全程没操作)。我查了 “C 语言指针优化技巧” 的资料,知道了 “指针要为功能服务,不是为了用而用”,开始删冗余指针 / 变量,保留核心指针用法。
3.1简化思路(结合指针知识点)
- 删冗余指针:
p2、flag1、p_reset(p_reset可以合并到原有循环,不用单独声明); - 保留核心指针:
*(p+i)操作数组、指针传参这些练手的核心用法; - 查资料学到:
arr1[0] = arr1[1] = arr1[2] = arr1[3] = 0等价于指针循环置 0,更简洁,但我仍保留指针写法(练手)。
3.2步骤 1 代码(优化指针冗余)
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> // 保留指针遍历打印 void print(int arr[],int n) { int *p = arr; for(int i=0;i<n;i++) { if(*(p+i) == 1) { if(i==0) printf("凶手是:A\n"); else if(i==1) printf("凶手是:B\n"); else if(i==2) printf("凶手是:C\n"); else if(i==3) printf("凶手是:D\n"); } } } // 保留指针统计逻辑 int sofe(int arr[],int n) { int count = 0; int *p = arr; for(int i=0;i<n;i++) { if(*(p+i) == 1) { count++; } } return count; } // 删冗余指针:p2、flag1;保留核心指针p void Jud(int arr1[], int arr2[],int n) { int flag2 = 0; int killer_idx = -1; for(int i=0;i<n;i++) { int* p = arr1; // 核心指针操作保留 *(p+0) = (i != 0); *(p+1) = (i == 2); *(p+2) = (i == 3); *(p+3) = (i != 3); flag2 = sofe(p, n); if (flag2 == 3) { killer_idx = i; break; } } // 简化指针置0:合并循环,删p_reset int *p = arr1; for(int i=0;i<n;i++) *(p+i) = 0; *(arr1 + killer_idx) = 1; print(arr1, n); } int main() { int arr1[4] = { 1,1,1,1 }; int arr2[4] = { 0,0,0,0 }; // 暂留,下一步优化 int n = sizeof(arr1) / sizeof(arr1[0]); Jud(arr1, arr2,n); return 0; }3.3步骤 1 思考过程(指针练习进阶)
- 查资料懂了:“冗余指针会增加代码复杂度”,比如
p2只声明没操作,完全是无效练习,删了不影响指针核心用法; - 保留
*(p+i):这是我练指针的核心,哪怕循环置 0 可以更简洁,我还是用指针写法 —— 新手练知识点,先熟练再简化; - 指针传参验证:
sofe(p, n)中p是arr1的首地址,传参后函数内操作的还是原数组,这一步验证了 “指针传参可以修改原数组” 的知识点(查资料确认的)。
四、简化步骤 2:查资料理解 “指针 & 函数设计”(精简参数,保留指针)
我查 “C 语言函数参数设计” 的资料时看到:“函数参数越少,逻辑越清晰,指针传参也要按需设计”。这一步我删无用的arr2,重构print函数,但仍保留指针核心用法。
4.1简化思路(指针 + 函数优化)
- 删
arr2:全程没用到,函数参数里留着只是浪费,指针传参也要 “按需传递”; - 重构
print:不用数组,直接传凶手索引,但我加了一个 “指针版 print” 的备选(练手); - 保留
Jud函数内的核心指针操作:*(p+i)赋值、指针传参给sofe。
4.2步骤 2 代码(指针 + 函数优化)
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> //简洁版print void print(int killer_idx) { // 练指针:用字符数组+指针打印,替代多个if char suspects[] = {'A','B','C','D'}; char *p = suspects; // 指针指向字符数组首地址 printf("凶手是:%c\n", *(p + killer_idx)); // 指针偏移找凶手 } // 保留指针统计逻辑 int sofe(int arr[],int n) { int count = 0; int *p = arr; for(int i=0;i<n;i++) { if(*(p+i) == 1) { count++; } } return count; } // 精简参数:删arr2,保留核心指针操作 void Jud(int arr1[],int n) { int flag2 = 0; int killer_idx = -1; for(int i=0;i<n;i++) { int* p = arr1; *(p+0) = (i != 0); *(p+1) = (i == 2); *(p+2) = (i == 3); *(p+3) = (i != 3); flag2 = sofe(p, n); if (flag2 == 3) { killer_idx = i; break; } } print(killer_idx); // 调用新的指针版print } int main() { int arr1[4] = { 1,1,1,1 }; int n = sizeof(arr1) / sizeof(arr1[0]); Jud(arr1, n); // 删arr2参数 return 0; }4.3步骤 2 思考过程(指针 + 函数设计)
- 查资料学到:字符数组也能靠指针偏移访问元素(
*(p + killer_idx)),比多个 if 判断更简洁,还能练指针; - 函数参数精简:
Jud函数原本传arr1、arr2、n,现在只传arr1和n,指针传参的 “目的性” 更强 —— 新手练指针,要知道 “指针是工具,不是负担”; - 核心收获:指针传参的本质是 “传地址”,不管是 int 数组还是 char 数组,指针偏移的逻辑是通用的,这一步把数组指针的用法打通了。
五、简化步骤 3:指针进阶优化(保留指针,合并冗余函数)
最后一步,我查 “C 语言指针最佳实践” 的资料,知道了 “没必要为短逻辑单独写函数”,于是合并sofe函数,但全程保留指针核心用法(毕竟练指针是核心目的)。
5.1简化思路(指针终极优化,不丢练手目标)
- 合并
sofe函数:统计真话的逻辑只有几行,合并到Jud中,减少指针传参的次数,但保留指针统计的逻辑; - 变量名语义化:
flag2改true_count,查资料学到 “变量名要见名知意,指针代码更需要清晰命名”; - 保留所有核心指针操作:
*(p+i)赋值、指针遍历、字符数组指针偏移。
5.2步骤 3 代码(指针版极简化)
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> // 指针版打印函数(练手核心) void print(int killer_idx) { char suspects[] = {'A','B','C','D'}; char *p = suspects; // 字符数组指针练手 printf("凶手是:%c\n", *(p + killer_idx)); } // 核心推理函数(保留所有指针练手逻辑) void findKiller() { int killer_idx = -1; int arr1[4] = {0}; // 指针操作的载体 int *p = arr1; // 核心指针 // 遍历嫌疑人,练指针赋值 for(int i=0;i<4;i++) { // 指针偏移赋值:练*(p+i) *(p+0) = (i != 0); // A的供词 *(p+1) = (i == 2); // B的供词 *(p+2) = (i == 3); // C的供词 *(p+3) = (i != 3); // D的供词 // 指针统计真话数(合并sofe函数,保留指针逻辑) int true_count = 0; int *p_count = arr1; for(int j=0;j<4;j++) { if(*(p_count + j) == 1) true_count++; } if (true_count == 3) { killer_idx = i; break; } } print(killer_idx); } int main() { findKiller(); return 0; }5.3步骤 3 思考过程(指针练习总结)
- 合并函数但不丢指针:
sofe函数合并后,我仍用p_count指针统计,没改用数组下标 —— 练指针的目标要守住; - 查资料的关键收获:
arr[i]等价于*(arr+i),指针偏移和数组下标是同一逻辑的两种写法;- 指针传参的本质是 “共享地址”,所以函数内操作指针能修改原数组;
- 指针要 “按需使用”,冗余指针只会增加理解成本;
- 最终代码:既保留了所有练指针的核心用法(数组指针、字符指针、指针偏移、指针统计),又精简了冗余逻辑,解题 + 练指针的目标都达成了。
六、新手练指针的核心收获(附避坑指南)
1. 指针核心知识点(查资料 + 练手总结)
- 基础等价关系:
arr[i] = *(arr+i) = *(i+arr) = i[arr](后两种少用,但验证了指针偏移的本质); - 指针传参:数组名作为参数传递时,本质是传递首元素的地址,函数内修改指针指向的内容会影响原数组;
- 字符数组指针:
char *p = str中,*(p+i)可以访问字符串的第 i 个字符,和 int 数组指针逻辑一致。
2. 新手练指针的避坑点(我踩过的坑)
- 语法坑:把判断
==写成赋值=(指针条件判断尤其要注意); - 逻辑坑:为了用指针而加冗余指针(比如
p2),忘记 “指针是工具,不是目的”; - 命名坑:用
flag1/flag2这种无意义命名,指针代码本身抽象,命名不清晰更难理解。
3. 练指针的小技巧(新手向)
- 先 “刻意用”:哪怕代码绕弯,先把指针的核心用法(偏移、传参、遍历)用熟;
- 再 “查资料优化”:写完初始版本后,查指针优化的资料,区分 “有效指针” 和 “冗余指针”;
- 最后 “精简保留”:删冗余但保留核心练手逻辑,比如我最终版仍保留所有指针操作,只合并了冗余函数。
七、补充:最优写法(高效简洁版)
前面的所有版本,核心目的是练指针,所以刻意保留了指针操作,哪怕有更简洁的写法。但如果单纯追求 “最优解”(逻辑最简洁、代码最短、效率最高、可读性最好),可以去掉所有冗余,不用刻意用指针(指针是工具,无需为了用而用),以下是这道题的最优写法:
7.1最优写法代码
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { // 嫌疑人映射:0=A,1=B,2=C,3=D char suspects[] = {'A','B','C','D'}; // 遍历每个嫌疑人,假设i是凶手 for(int i=0;i<4;i++) { // 直接计算4人供词真假,统计真话数(核心逻辑一步到位) int true_count = (i!=0) + (i==2) + (i==3) + (i!=3); if(true_count == 3) // 满足3真1假,直接打印凶手 { printf("凶手是:%c\n", suspects[i]); break; } } return 0; }7.2最优写法解析
1.为什么最优?
- 效率最高:无多余函数调用、无冗余指针 / 数组,循环仅执行 4 次,每次循环仅 1 行计算,时间复杂度 O (1)(固定 4 个嫌疑人,无额外开销);
- 代码最简:仅 15 行代码,核心逻辑浓缩为 1 行(真话数统计);
- 可读性最好:去掉所有无关语法,只保留 “遍历嫌疑人→计算真话数→判断输出” 的核心逻辑,新手也能快速理解。
2.与练手版的区别:
- 练手版:核心是 “借解题练指针”,刻意用指针操作,哪怕冗余,重点在知识点练习;
- 最优版:核心是 “高效解题”,按需选择语法,不用多余工具,重点在逻辑简洁。
3.关键逻辑:
- 利用 C 语言 “逻辑表达式结果为 1(真)或 0(假)” 的特性,直接把 4 个供词的逻辑表达式相加,得到真话数;
- 无需额外数组 / 指针存储供词真假,直接计算、直接判断,一步到位。
八、最终总结
这次解题的核心目标不是 “写最简洁的代码”,而是 “借解题练指针”—— 从刻意写冗余指针代码,到查资料优化冗余、保留核心,我不仅找出了凶手(C),更吃透了数组指针、字符指针、指针传参这些新手必学的知识点。
对新手来说,练指针不用追求 “一步到位写最优解”,先 “能用指针实现功能”,再 “查资料优化指针用法”,最后 “精简并保留核心练手逻辑”,这个过程比直接抄最优解更有收获。
而最优写法则告诉我们:C 语言中,指针、数组都是工具,解题的核心是 “逻辑清晰”,按需选择工具,不堆砌语法,才是最优雅、最高效的编程方式。