C/C++第41-50题精讲:数组指针链表与文件操作的高频考点 这批练习题做到第41-50题说明你已经把前面基础部分啃得差不多了。到了这个阶段题目考察的东西会逐渐“变重”——不再是单纯让你写一个循环、判断一个语法而是开始把数组、指针、结构体、链表、文件操作这些知识点揉在一起很多题看着不复杂丢分点却不少。这篇文章我按题型把后十道题重新整理了一遍每道题都会讲清楚思路、给出参考代码再把容易踩的坑单独拎出来说。适合正在准备期末考试、考研复试机试或者校招笔试的读者也适合自学C/C想检验一下自己水平的人。1. 第41-43题从数组逆序到递归优化先把基本功压实这三道题放在41-50的开头难度其实不算高但出题老师很喜欢在细节上动手脚。你如果这三道题都能一遍写对后面的题才值得往下做。1.1 第41题数组逆序两种写法都要熟题目描述一般很简单给定一个长度为 n 的数组把数组元素原地逆序不能额外开数组。核心解法是用双指针一头一尾往中间走边换边收敛void reverseArray(int arr[], int n) { int left 0, right n - 1; while (left right) { int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; right--; } }易错点集中在两个地方。第一right 的初始值必须是 n-1不是 n很多人用 for 循环习惯性写了for (i 0, j n; i j; i, j--)在数组边界外访问了一次虽然有时候程序还能跑但已经属于未定义行为了考试想查这种错很难查。第二left right 的循环条件保证了奇数长度数组中间那个元素不用跟自己交换逻辑更干净。我建议你顺手也练一下指针版本用两个指针int *p arr, *q arr n - 1操作思路完全一样。为什么练这个因为很多综合题里逆序操作不是单独考的而是作为链表逆置、字符串反转的底层思路指针熟了之后后面那道链表题会轻松很多。1.2 第42题删掉字符串中的指定字符这题的要求是删除字符串中所有等于某个指定字符的字符要求原地操作不能开新数组。原地删除的经典手法是“双下标覆盖”一个指针负责遍历一个指针负责记录可写入位置void deleteChar(char s[], char c) { int j 0; for (int i 0; s[i] ! \0; i) { if (s[i] ! c) { s[j] s[i]; } } s[j] \0; }逻辑上最关键的一行是循环结束后手动补s[j] \0。很多人遍历完就以为结束了忘了收尾导致字符串结尾位置停着一个残留字符输出的时候就会出现莫名其妙的尾巴。这题的进阶版本经常变成“删除字符串中所有的空格”“删除连续的重复字符”“把字符串中的大写字母全转成小写”等等本质都是用同一个模型双下标 条件判断 结束符。你只要把这一题吃透那几个变种都能顺带解决。还有一个小细节函数形参写成char s[]还是char *s在功能上是等价的但如果你在主函数里调用deleteChar(hello, l)字符串字面量是不能修改的程序会崩。考试时如果题目没有明确说明最好把主函数里的字符串定义成字符数组不要用字符串常量。1.3 第43题递归数列先会写再会优化斐波那契数列几乎是递归教学第一题能衍生出的考法也很多直接写递归、改成记忆化递归、改成迭代、甚至让你算某一个较大的 n 并考察溢出陷阱。最常见的标准递归写法long long fib(int n) { if (n 1) return n; return fib(n - 1) fib(n - 2); }但你要清楚这个写法的时间复杂度是 O(2^n)n 稍微大到 40 以上就会明显变慢n 到 45 左右普通电脑就已经要等好几秒了。如果题目真的让你算比较大的 n单纯写递归是拿不到分数的。应对方法有两种。第一种是加上记忆化数组把递归过程中算过的值存下来避免重复计算long long memo[100] {0}; long long fibMemo(int n) { if (n 1) return n; if (memo[n] ! 0) return memo[n]; memo[n] fibMemo(n - 1) fibMemo(n - 2); return memo[n]; }第二种是直接用迭代连递归调用栈的消耗都省了long long fibIter(int n) { if (n 1) return n; long long a 0, b 1, c; for (int i 2; i n; i) { c a b; a b; b c; } return b; }做题的时候我的建议是第一遍递归第二遍追问递归的问题第三遍给出迭代或记忆化方案。很多机试不会只让你写一个能跑的版本它考察的是你能不能在递归解法的基础上看出来效率问题并顺利完成优化。2. 第44-46题结构体排序、链表逆置与矩阵转置必考模型一网打尽这三道题是后十道里性价比最高的因为它们几乎代表了三类常考模型排序、链表、二维数组。三道题难度适中但每道题里都有高频陷阱值得认真刷两遍。2.1 第44题结构体数组排序qsort和sort的用法差异题目一般给你一个学生结构体包含学号、姓名、成绩要求按成绩从高到低排序成绩相同按学号从小到大排。C语言的做法是用 qsort这题的关键不是排序本身而是比较函数能不能写对typedef struct { int id; char name[32]; double score; } Student; int cmp(const void *a, const void *b) { const Student *sa (const Student *)a; const Student *sb (const Student *)b; if (sa-score sb-score) return -1; if (sa-score sb-score) return 1; return sa-id - sb-id; }主排序调用qsort(students, n, sizeof(Student), cmp);三个特别容易丢分的地方要重点说。第一个比较函数不能写成return sa-score - sb-score因为 score 是 double 类型相减得到的结果会截断成 int0.5 和 -0.5 都会被当成 0排序直接失效。第二个如果题目要求按姓名字典序排序比较要用strcmp(sa-name, sb-name)不能手写字符相减因为中文姓名的字节序和字典序之间的关系没那么简单。第三个qsort的参数顺序经常有人记错首地址、元素个数、单个元素大小、比较函数缺一个都编译不过。如果你写的是 C用sort(students, students n, cmp)会更方便但这里的 cmp 签名和 C 语言不一样C 版本一般写成bool cmp(const Student a, const Student b) { if (a.score ! b.score) return a.score b.score; return a.id b.id; }考试时看清楚科目要求用 C 还是 C别把两套函数写混。2.2 第45题链表逆置断链和重连的顺序不能乱链表的操作题一定要动手画图光靠脑子想很容易出错。这题要求把单链表就地逆置返回新的头指针。迭代版本是标准解法typedef struct Node { int data; struct Node *next; } Node; Node* reverseList(Node *head) { Node *prev NULL; Node *cur head; while (cur) { Node *next cur-next; cur-next prev; prev cur; cur next; } return prev; }这段代码的要点在于循环体里那三行顺序先保存下一个节点再改当前节点的 next然后整体往后移动。如果你先改了cur-next prev再取cur-next就会拿到已经指向 prev 的指针后面的节点全部丢失链表从中间断掉。我见过不少满分代码在测试链表长度为 0 或 1 的时候没问题但一跑长链表就挂在cur-next为 NULL 时取空指针。刚才的写法用Node *next cur-next在循环开头保存就是天然规避这种问题的方式。链表逆置的递归写法也值得掌握不过它需要额外理解调用栈的返回时机如果笔试时间紧张优先保证迭代写法。2.3 第46题二维数组转置内层循环的起点有讲究对一个 n×n 的矩阵做转置即把a[i][j]和a[j][i]互换很多人一拍脑袋就写两个嵌套 for 循环写完之后发现矩阵没变问题就出在交换了两次。正确写法void transpose(int a[][N], int n) { for (int i 0; i n; i) { for (int j i 1; j n; j) { int temp a[i][j]; a[i][j] a[j][i]; a[j][i] temp; } } }关键就是内层循环从i1开始保证只遍历右上三角区域。如果从 0 开始前面已经换好的元素会在后面再次被换回去转置就变成了恒等操作。二维数组作为函数参数时的声明也有坑。void transpose(int a[][N], int n)里必须要写明第二维大小 N不能写成int a[][]因为编译器需要知道行与行之间的内存间距才能计算下标地址。这个细节在填空题和改错题里经常出现。如果题目改成 m×n 的非方矩阵转置那就要开一个新矩阵来存放转置结果不能原地操作做题的时候注意分清“方阵原地转置”和“任意矩阵转置”的区别。3. 第47-49题查找、单词统计与文件操作上机实操最容易翻车的地方这三道题上机练习含金量很高表面上是考算法实际上还考你的代码健壮性。很多同学在纸上写得头头是道一上机跑起来就各种段错误和死循环。3.1 第47题二分查找循环边界与mid的计算方式二分查找的前提是数组有序题目通常会保证这一点。标准写法如下int binarySearch(int arr[], int n, int target) { int left 0, right n - 1; while (left right) { int mid left (right - left) / 2; if (arr[mid] target) return mid; else if (arr[mid] target) left mid 1; else right mid - 1; } return -1; }最容易出错的地方是循环条件到底用left right还是left right。这题里用前者因为当区间只有一个元素时这个元素可能是要找的目标如果是left right就会跳过最后这一个位置返回错误结果。另一个细节是mid的计算。很多人写(left right) / 2在极端大数组上可能发生整数溢出标准规避方式是left (right - left) / 2。考试时数据量不大也许看不出区别但面试和竞赛里这是默认要求。二分查找的变种题很多比如查找第一个大于等于 target 的位置、最后一个小于等于 target 的位置核心思路都是调整边界时决定要不要保留 mid。建议把标准二分写的滚瓜烂熟再去动变种。3.2 第48题统计单词个数状态标志比数空格更稳妥题目要求统计一个字符串中的单词个数单词之间用空格隔开。最朴素的想法是“每遇到一个空格就 count”但字符串开头就是空格、结尾也是空格、多个连续空格这些情况都能把这种思路搞崩。更稳妥的写法是用一个状态标志判断当前是否处于单词内部int countWords(const char *s) { int count 0; int inWord 0; while (*s) { if (*s ! *s ! \t *s ! \n) { if (!inWord) { inWord 1; count; } } else { inWord 0; } s; } return count; }这个写法的核心思想是“只在从非单词状态进入单词状态的瞬间计数一次”连续空格不会导致重复计数开头和结尾的空格也不影响结果。如果把题目条件扩展一下例如把标点符号也当作分隔符只需要在判断非空格条件里加上!ispunct(*s)函数整体的状态机框架不需要改。这种“换条件不换结构”的能力正是大题考察的重点。3.3 第49题文件读写统计字符行数时EOF的处理要谨慎题目往往是打开一个文本文件统计里面的字符总数和行数然后输出。参考代码FILE *fp fopen(test.txt, r); if (fp NULL) { perror(open file failed); return 1; } int ch; int chars 0, lines 0; while ((ch fgetc(fp)) ! EOF) { chars; if (ch \n) lines; } fclose(fp);这里有一个老生常谈但依然有人犯的错ch 必须声明为 int不能声明为 char。因为 EOF 通常是一个负数而 char 类型可能无法保存负值用 char 接收 fgetc 的返回值会导致判断失败最常见的现象是程序陷入死循环一直读不到文件末尾。还要注意逻辑细节文件最后一行如果没有换行符那么行数的统计结果会比“实际文本行数”少 1。很多较真的题目会在说明里提到这一点如果没提你可以把实现写成“文件内容非空且最后一字符不是 \n 时行数加 1”这样更严谨。另外一个实操习惯fclose(fp)一定要写。有人觉得程序结束系统会自动回收文件资源就不关但在循环里多次打开文件的程序里不关闭文件会导致资源泄漏最终影响后面的读写操作。其实只要写文件操作一律先判断是否打开成功用完立刻关闭这是正规的工程习惯不只在考试里有用。4. 第50题学生成绩管理系统综合题的模块化思路与实现第50题通常是整套练习卷的压轴题体裁就是让写一个“学生成绩管理系统”。这类题的特点是功能多、代码长时间紧的情况下特别容易写着写着就乱成一团。但只要把模块拆分清楚了它反而是最容易拿高分的一道题。4.1 题目要求与模块拆分常见的功能要求有六个录入学生信息、按成绩排序并打印、按学号查找学生、修改成绩、计算平均分、把数据保存到文件以及从文件读取数据。如果你从头到尾写一个 main 函数所有逻辑堆在一起调试起来会非常痛苦。正确做法是先定义全局结构和数组再把每个功能封装成独立函数#define MAXN 100 typedef struct { int id; char name[32]; double score; } Student; Student stu[MAXN]; int n 0;后面每个功能对应一个函数函数之间通过全局变量n和stu共享数据。对于考试来说这种写法最直观也方便阅卷看到你每个子功能是否完整。4.2 核心代码实现与关键设计录入模块要处理“重复学号”的情况简单起见可以先把学号读进来再看有没有相同值有就提示重新输入。排序模块之间直接复用第44题写好的比较函数cmp调用 qsort 一行搞定。查找模块用线性查找就够学号是整数遍历比较即可int searchById(int id) { for (int i 0; i n; i) { if (stu[i].id id) return i; } return -1; }修改成绩模块调用searchById拿到下标直接改stu[index].score如果返回 -1 就提示“学生不存在”。平均分函数累加所有成绩后除以 n注意n为 0 时会除零崩溃加一个保护判断。文件保存部分建议把每个字段按行写入比如学号、姓名、成绩各占一行因为姓名里可能有空格用空格分隔容易解析出错void saveToFile(const char *filename) { FILE *fp fopen(filename, w); if (!fp) return; for (int i 0; i n; i) { fprintf(fp, %d\n%s\n%.2f\n, stu[i].id, stu[i].name, stu[i].score); } fclose(fp); } void loadFromFile(const char *filename) { FILE *fp fopen(filename, r); if (!fp) return; n 0; while (fscanf(fp, %d, stu[n].id) 1) { fscanf(fp, %s, stu[n].name); fscanf(fp, %lf, stu[n].score); n; } fclose(fp); }主函数用 while switch 做菜单循环用户输入对应数字执行相应功能退出循环前自动保存一次。整体结构清晰之后就算某个模块写错你也能快速定位到具体函数去改不会牵扯到其他部分。4.3 综合题拿到高分的三个关键习惯第一数组容量不要拍脑袋定太小一般题目没说就设 100稳妥一点设 256 也完全可以。第二所有函数返回值要想清楚比如查找函数返回下标找不到返回 -1不要用 0 表示“找不到”因为 0 号学生是合法的。第三在草稿纸上先画出菜单和功能对照再动手写代码能省掉后期大量返工时间。实际阅卷过程中老师更看重代码的完整度和逻辑清晰度而不是某个花哨算法。这道题你用最朴素的线性查找 qsort 文件读写就能解决关键是每一块都写对、写完。5. 复盘10道题最容易丢分的三个环节做完练习回看这10道题你会发现有些分数不是“不会做”丢的而是“细节没注意”丢的。复盘的价值就在找出这些规律性的错误来源。5.1 数组边界不是 n 就是 n-1永远先画下标区间第41题的 right、第47题的 right 初始值都是 n-1第46题内层循环起点是 i1这三个地方如果错程序行为都很隐蔽要么越界读垃圾值要么死循环。我自己的习惯是每写一个涉及下标的循环先问自己一句这个区间是闭区间还是开区间中点 split 在哪想清楚了再敲代码。数组题还有一个常见但每次都会出现的疏忽for (int i 0; i n; i)写成闭区间莫名多访问一个位置。这一类错误在刷题复盘时要单独标记下次一看到循环边界就条件反射地检查一遍。5.2 比较函数误差、截断、字符串比较三大坑第44题 qsort 的比较函数是最典型的重灾区。double 不能直接相减返回姓名比较必须用 strcmp结构体排序的最终依据还要注意第二关键字怎么写。一个比较函数锤炼到位很多排序类题目处理起来就顺手了。你自己可以试着把第44题改成“先按成绩降序再按学号升序最后按姓名首字母升序”看看比较函数会变成什么样。这个练习对理解多关键字排序很有帮助。5.3 文件操作打开失败、EOF类型、关闭遗漏第49题和第50题都涉及文件操作。打开成功与否不判断的人不在少数程序跑起来没有任何提示就结束你根本不知道是文件不存在还是路径写错。把if (fp NULL)这个守卫养成习惯能节省大量调试时间。EOF 接收变量用 int 这个知识点考试填选题常考上机也是实实在在的坑。最后不要忘记 fclose尤其在多次打开同一个文件的循环结构里不关闭下一次就写不进去。6. 复习节奏安排与后面冲刺建议这批41-50练完整套50题的复习主线就全部过了一遍。下一步我建议别再按题号顺序刷了而是按题型重新洗牌把数组和指针类放一起字符串类放一起结构体和链表类放一起文件和综合类放一起。每类挑两三道有代表性的题掐时间重写比从头到尾盲目再过一遍效率高很多。具体时间上如果距离考试还有两周前一周每天做两组题型后一周直接进入整套模拟和错题重写。每道题给自己限制15到20分钟超出时间就得停下来看哪里卡壳了。我见过太多人一道题抠一个小时最后看起来“做出来了”但考试根本不可能给你那么多时间。最后分享一个我自己的刷题习惯每道题跑通初版之后再逼自己写第二种写法。第41题的数组逆序可以再用指针写一遍第43题的递归可以再补一版记忆化第45题的链表逆置再推演一遍递归过程。不要嫌麻烦这一步就是把“我好像会了”变成“我真的会了”的关键。等到你做第50题那种综合题时会明显感到每个模块都是在调用之前练熟的能力而不是临时凑。