
《C程序设计第五版》的第八章习题应该是整本书里最劝退人的一道坎。前几章的循环、数组还能凭“背代码套模板”勉强过关一到指针这章满屏幕的星号和取地址符瞬间让许多人开始怀疑自己是不是不适合写程序。我带过不少初学C语言的学员几乎每个人都在这一章的习题上卡过壳而且卡的原因高度一致——不是不会写代码而是脑子里对“地址”“指针”“间接访问”这几个词没有建立起正确的画面感。这篇文章我不打算把第八章所有习题的答案罗列一遍而是想跟你聊聊这一章的题型分布、必做习题的破题思路以及我在辅导过程中反复看到的那些经典翻车点。如果你正卡在第八章或者刷完题总觉得自己理解得不够扎实这篇文章应该能帮你把思路理顺。1. 为什么第八套是整本书的分水岭很多人低估了“善于利用指针”这一章的难度跃升。前面你处理的是值a 10arr[2] 3变量里存的就是数据本身。到了这一章你要开始处理“数据放在哪里”这件事变量里存的不是数据本身而是一个地址数据还老老实实待在原来的地方只是你多了一张写着门牌号的纸条。1.1 第八章的核心考点其实没几个别看习题数量不少真正需要你建立认知的就五个方向第一指针变量的声明、赋值和间接访问也就是int *p、p a、*p这套最基本的三连操作。第二指针与一维数组、二维数组的关系。数组名在表达式中会退化成首元素地址arr和arr[0]在数值上一样但类型上不完全等价这是初学者最容易混的地方。第三指针作为函数参数。C语言的函数参数默认是值传递想在一个函数里修改外部变量的值就必须把变量的地址传进去用指针形参接收。swap函数就是这类问题的典型代表。第四指针与字符串。字符串常量本身是一块连续内存字符数组名和指向字符的指针都能操作字符串但两者在“能不能修改内容”上有本质区别。第五指针数组和二级指针。这一部分在习题里经常以“对若干字符串排序”的形式出现核心思路是交换指针而不是交换整个字符串。1.2 学完这章你应该能回答这些问题如果你做完第八章的习题闭上眼睛能立刻说出下面几个问题的答案说明这章基本过关了arr、arr[0]、*arr分别表示什么为什么int *p arr成立而p arr在多数情况下会报警告用指针遍历数组时p arr n和p arr n - 1在效果上有没有区别字符串char *s hello和char s[] hello两个s[0]的访问方式相同但为什么前者修改字符会出问题后者可以这些问题看起来都很基础但能完整答上来的人比想象中少。我在辅导时观察到凡是能亲手把这几个问题的内存图画出来的人后面的习题基本都能顺利消化。2. 第八章的习题类型地图一眼看穿出题套路刷题之前先把题型结构看清楚比盲目写代码高效得多。我习惯把第八章的习题分成五个类别每一类的考法和突破口都很固定。题型类别典型考法一句话突破口指针基础操作声明、赋值、间接访问分清p存的是地址*p取的是值指针与函数传参交换变量、函数内修改外部值要改谁就传谁的地址指针与数组遍历、求最值、逆序、排序数组名退化为地址指针移动的步长看类型指针与字符串复制、连接、比较、统计字符串以\0结尾这既是终点也是循环条件指针数组与二级指针对多个字符串排序、批量处理排序时交换指针比交换字符串高效这五种类别不是割裂的习题通常会把它们混在一起出。比如用指针实现字符串排序就同时涉及指针数组、字符串处理和排序算法三个知识点。你不需要把每一道题都做得尽善尽美但每一种类型至少要吃透一题然后触类旁通。我的建议是给每种类别准备一道“代表题”做完之后自己变三个花样改数据类型、改处理逻辑、改输入输出方式。变到第三个花样还能轻松写出来这个知识点就算真的焊死在你脑子里了。3. 五道必做的代表题拆解从易到难一次打通如果你想用一个晚上集中把第八章最关键的思维建立起来我推荐先做下面五道题。它们难度递增覆盖面广每一题都能带出一个重要的指针使用习惯。3.1 先搞定两数交换理解指针传参的唯一入口第一道必做题是用指针实现两个整数的交换。这道题看起来简单却是检验你是否真正理解“值传递”和“地址传递”区别的试金石。#include stdio.h void swap(int *a, int *b) { int temp *a; *a *b; *b temp; } int main() { int x 3, y 5; printf(交换前: x%d, y%d\n, x, y); swap(x, y); printf(交换后: x%d, y%d\n, x, y); return 0; }如果你写过void swap(int a, int b)的版本会发现函数内确实交换了但回到main里x和y纹丝不动。原因很简单参数是值传递函数拿到的是两个数字的副本交换副本不影响原件。改用指针之后x和y把x和y的地址传了进去函数通过地址找到原本的变量间接访问并修改了它们的值。我特别想强调代码里的temp *a这一步。初学者时常写成temp a这会把指针本身的值也就是x的地址赋给temp而不是x的值。这里一个星号的差别结果天差地别。这道题体现的原则在后面所有题目里都会反复用到要修改一个变量本身就传它的地址要用变量的值就用*解引用。3.2 用指针求数组的最大值和最小值遍历习惯的建立第二道题是用指针处理一维数组找出最大值和最小值。这道题的训练目标是让你习惯用指针的关系运算来控制循环。#include stdio.h int *find_max(int *arr, int n) { int *max arr; for (int *p arr 1; p arr n; p) { if (*p *max) { max p; } } return max; } int main() { int nums[] {12, 5, 23, 8, 19, 32, 7}; int n sizeof(nums) / sizeof(nums[0]); int *maxPos find_max(nums, n); printf(最大值为: %d, 下标为: %d\n, *maxPos, (int)(maxPos - nums)); return 0; }核心思路是维护一个指针max初始指向首元素然后让另一个指针p从第二个元素开始逐个比较。比较的不是指针本身而是指针指向的值所以判断条件要写*p *max。当p指向的值更大时把max更新为p。这里有两个细节值得说。第一循环条件p arr n表示指针p从当前元素到末尾之后这种写法比p arr n - 1更直观也更容易推广到其他场景。第二程序最后通过maxPos - nums求得了最大值的下标这是因为两个指针指向同一数组时它们相减的结果表示中间间隔的元素个数。这种指针运算的技巧在后续很多习题里都会用到值得单独记下来。3.3 实现数组逆序两个指针往中间夹的经典写法第三道题是将数组逆序排列。要求不借助额外数组用指针完成原地逆置。这也是我见过最能训练“指针移动”感觉的一道题。#include stdio.h void reverse(int *arr, int n) { int *left arr; int *right arr n - 1; while (left right) { int temp *left; *left *right; *right temp; left; right--; } } int main() { int arr[] {1, 2, 3, 4, 5, 6, 7}; int n sizeof(arr) / sizeof(arr[0]); reverse(arr, n); for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }思路是让左指针从头部出发右指针从尾部出发每轮交换两个指针指向的值然后左指针向右移动一位右指针向左移动一位直到两个指针相遇或者错开。循环条件一定要用left right不要写left ! right。当数组长度为奇数时两个指针最终会指向同一个中间元素此时交换自己是没问题的但当数组长度为偶数时left会越过right如果条件写成left ! right循环永远不会停下程序会继续访问越界地址导致未定义行为。这个细节就是指针题里常见的暗坑。用而不是!能同时覆盖奇数和偶数长度是最稳妥的选择。3.4 指针实现字符串复制把多位拆透的经典表达式第四道题是自己写一个字符串复制函数不用库函数strcpy。这道题的考点集中在*dest *src这个表达式上理解它比记住它更重要。#include stdio.h void strCopy(char *dest, const char *src) { while ((*dest *src) ! \0) { ; } } int main() { char source[] Hello, Pointer!; char target[50]; strCopy(target, source); printf(源字符串: %s\n, source); printf(目标字符串: %s\n, target); return 0; }先解释*src。这里涉及两个运算符*是解引用是后置自增。优先级上高于*但后置自增的特性是先返回当前值再让指针自增。所以*src的含义是取出src当前指向的字符然后让src指针向后移动一位。同理*dest ...是先拿到dest当前指向的存储位置把右边的字符放进去然后dest指针向后移动一位。整个循环就是把src当前字符复制到dest指向的位置复制完成后src和dest都各自后移一格然后判断复制过去的字符是不是\0。当复制到字符串结尾的\0时循环条件为假循环结束。这里有个巧妙之处\0已经被复制过去了所以函数不需要在循环结束后再补一个字符串结束符目标数组已经自动获得了正确的结束标记。我给初学者一个检验自己是否真懂的办法把while ((*dest *src) ! \0)拆成四步写出来每一步标明当时dest和src指向哪里复制了什么字符指针移动了几格。能完整写对这个过程的人对指针自增的理解就过关了。3.5 用指针数组对多个字符串排序交换指针比交换字符串高效第五道必做题是用指针数组对若干字符串按字母顺序排序这也是第八章习题里最综合的一道题同时考验字符串处理、指针数组和排序三块知识。#include stdio.h #include string.h void sortNames(char *names[], int n) { for (int i 0; i n - 1; i) { for (int j i 1; j n; j) { if (strcmp(names[i], names[j]) 0) { char *temp names[i]; names[i] names[j]; names[j] temp; } } } } int main() { char *names[] {peach, apple, cherry, banana, date}; int n sizeof(names) / sizeof(names[0]); sortNames(names, n); for (int i 0; i n; i) { printf(%s\n, names[i]); } return 0; }核心思路是names是一个指针数组每个元素是一个char *分别指向不同的字符串常量。排序时用strcmp比较两个字符串的内容如果顺序不对就交换两个指针的位置。交换的是指针不是字符串本身。为什么要交换指针而不是交换字符串因为字符串是连续的一段内存交换整个字符串需要逐字符搬运代价很大而指针只是一个存储地址的变量交换两个指针和交换两个整数一样快。这个思维今后你在处理大批量数据时还会遇到排序比较的是值调整的是索引或指针这是工程里非常常见的做法。有一个细节要注意字符串常量在多数编译器下是只读的你不能通过names[i][0]去修改字符。这道题只涉及排序和输出不修改内容所以没问题。把这一点讲清楚是避免有人后面写代码时莫名崩溃的关键。4. 刷题时最容易踩的坑每一条都是真实翻车现场刷完一轮习题后把自己踩过的坑整理一遍价值不低于再做一遍题。下面这些错误我几乎在每个人身上都见过有些甚至反复出现。4.1 未初始化的指针就敢直接解引用int *p; // p 没有初始化里面是随机值 *p 5; // 把 5 写进一个未知的地址这种代码在主流的编译器上运行大概率会直接崩溃报段错误或访问冲突。指针存放的是地址而地址必须是合法可写的内存位置。int *p;只创建了一个能存放地址的变量但还没给它赋值它里面可能是任何一个乱七八糟的数字。正确做法是指针声明后尽快初始化要么指向一个合法变量要么置为NULL。养成习惯之后很多难排查的运行时错误都能从根源上避免。4.2 把p、*p、p三者搞混这三个表达式代表三种完全不同的东西。p存放地址*p是p指向的那个变量本身p是p这个指针变量自身的地址。初学者最容易在函数传参时搞混想传x的地址写了传*p想在函数里修改外部变量结果改的却是形参。举例来说在swap习题里如果写成swap(*a, *b)实际上是把两个整数传了进去又回到值传递的老路。正确做法是swap(x, y)实参是地址形参用指针接收。4.3 忘记指针加减运算的单位是“元素大小”不是“字节”p 1到底移动了几个字节取决于p指向的类型。int *p的p加1地址值增加4在常见的32位int平台或8char *q的q加1地址只增加1。这是C语言自动按类型换算的结果。这个特性如果理解不透就容易出现两种典型错误。一种是把int *当char *来算地址导致取到的数据完全错位另一种是在二维数组里用arr 1想当然地以为它等同于arr[0][1]实际上arr 1移动了整整一行。回去重新做一遍3.2题的指针遍历会帮助你巩固这个知识点。4.4 以为数组名可以随意自增arr是数组名它在表达式中会退化为首元素地址但数组名本身是一个常量不能作为左值。arr是不合法的因为数组名不是一个变量。要实现遍历必须用一个指针变量来保存数组首地址然后让这个指针变量去自增。int arr[5] {1, 2, 3, 4, 5}; int *p arr; // 合法p获得首元素地址 p; // 合法p指向下一个元素 // arr; // 非法数组名不能自增这个区别看似简单但在写习题答案时出错率极高。凡是遇到“让数组名往后指”的想法都先问自己一句是不是应该另起一个指针变量4.5 函数返回了指向局部变量的指针int *badFunction() { int local 42; return local; // 警告返回局部变量的地址 }局部变量在函数结束后就失效了它的内存会被系统回收或复用。这种情况下返回的指针指向的是一块“已经不属于你”的空间后续访问它完全是未定义行为。这个坑在你做动态内存分配相关习题时尤其值得警惕正确做法是使用malloc分配堆内存并记得free。5. 从“会做题”到“真会用”的进阶思路做完习题不等于真正掌握了指针。以我辅导的经验来看以下几个动作能把“会做题”升级成“真会用”而且效果比把习题集刷三遍更明显。5.1 在纸上画内存图这是最便宜也最有效的训练不用开电脑随便拿一道做过的指针题在纸上画出每个变量的内存格子。把地址用具体数字标出来比如假设数组首地址是0x100一个int占4字节那么arr[1]的地址就是0x104。然后把指针变量也画成格子格子里面写它保存的地址值。最后手动模拟代码一行一行执行随时更新指针格子里的值。我曾经让一位学员用这个方法只练了五道题再回头做其余习题时明显不再畏惧。画内存图的过程会把抽象概念具象化远比盯着屏幕发呆有效。5.2 用调试器观察指针的实时变化现在的开发环境都自带断点调试功能。看书上的地址抽象讲解时不如自己跑代码按F10单步执行一边按下一边在变量窗口看p的值是多少*p的值是多少p移动一次后地址变化了多少。把调试器里的观察结果跟纸上画的内存图对照你的理解会非常扎实。你可以专门挑一道字符串复制题来调试单步看完整个循环你会对*dest *src这个表达式产生肌肉记忆。5.3 变式练习把问题往工程场景推一步学完第八章后尝试给自己出三道变式题第一题把求数组最大值的int版本改成double版本思考指针步长发生了什么变化代码里哪些地方需要调整。第二题把字符串排序题从固定数组改成手动输入任意数量字符串用动态内存分配的方式读入并排序。第三题把数组逆序改成链表逆序感受用指针串联节点和操作数组的差异。这三道变式题做完你会发现在学校习题里学到的不只是考试技巧更是未来写真实程序的基础能力。指针在后续的数据结构、操作系统、嵌入式开发里无处不在第八章习题就是为你打地基的。我自己带学员这么多年最深的体会是指针知识从来不是靠“看明白”的而是靠“画明白”和“调明白”的。如果你现在正卡在第八章的某道题上不妨放下代码先拿起笔把那个程序的每个变量、每个指针、每个地址变化从头到尾画一遍。画完那一瞬间很多纠结的问题都会豁然开朗。
拿不准这条消息跟你有没有关系?
工种不同、批次不同,要求可能差很多。打电话把你的情况说清楚,我们按信阳、平顶山本地的口径给你捋一遍。