☰
C程序设计第五版课后题全攻略:从做题思路到踩坑记录
2026/10/6 14:25:27 网站建设 项目流程

很多第一次翻开《C程序设计(第五版)》的同学,都会在第一章结束后产生一种错觉:C语言好像就这么回事,无非是printf加几个循环。等真正坐到电脑前动手做每章末尾的课后习题,才发现“写出一个能运行的程序”和“把一个程序写对”之间隔着很长一段路。这本书的课后习题数量不算夸张,但质量分层很明显,从语法默写型到综合设计型都有,恰好覆盖了从入门到能上手写小项目的大部分关键节点。这篇内容就是围绕这本书的课后题展开的,适合正在自学、跟着网课刷题、或者被学校作业逼着赶进度的读者。我会直接从做题思路、典型题型拆解、踩坑记录三个角度聊,全是实际操作里能直接用的东西。

1. 课后题的难度分布,决定了你不该平均用力

1.1 先分成三类:概念题、代码题、细节题

第五版的课后习题,表面上看每章后面都排着十几道题,但你把它们全部扫一遍就会发现,题目大致能分成三种完全不同的类型。

第一类是概念题。这类题基本不用写完整代码,考的是记忆和辨析。比如基本数据类型的取值范围、运算符优先级、sizeof的运算结果、字符常量与字符串常量的区别、实参与形参的关系等等。题目本身没难度,但容易在细节上翻车。比如sizeof("hello")到底是多少,char str[] = "hello"和char str[] = {'h','e','l','l','o'}两个数组长度一样不一样,这类问题不去实际验证还真容易记混。

第二类是代码题,这也是课后题的大头。从第三章的循环嵌套输出九九乘法表、求水仙花数,到第四章的数组插入删除、排序,再到第六章的字符串处理、第七章的函数递归,每一章都有一批需要你从头到尾独立写完的题目。这类题的要求很直接:拿到题目,分析需求,设计变量和流程,写出可运行、结果正确的程序。

第三类是细节题,通常藏在题目的最后一两问里。典型代表是printf的输出格式控制、转义字符、自增自减运算符的副作用、逗号表达式的值等。这类题单独看知识点很小,但特别适合用来检验你对语法细节的把握程度。很多人在考试时丢分,丢的往往不是大题的逻辑,而是这些细节题。

把题目分成这三类,最大的意义是避免平均用力。概念题和细节题用零散时间翻一翻书就能解决,真正需要整块时间坐下来敲代码的是代码题。如果你把每个晚上的时间花在反复背诵sizeof的结果上,反而忽略了最有含金量的排序、字符串处理,那就本末倒置了。

1.2 难度跨度很大,建议按档位分配时间

我用一个表格把这本书课后题的典型档次做个划分,大家可以对照自己的进度来安排时间:

难度档位典型题目建议用时掌握目标
基础档变量交换、数据类型转换、简单的输入输出10-15分钟不看书写出完整的main框架
逻辑档素数判断、最大公约数、水仙花数、数列求和20-30分钟能写出带边界检查的循环程序
综合档数组插入删除、矩阵转置、字符串统计、链表建立40-60分钟能画出内存图并解释每一步
思维档汉诺塔、递归转换、趣味数学题60分钟以上能讲清楚递归过程和终止条件

我见过不少学生把大量时间耗在基础档的题目上,反复敲“交换两个变量的值”,却迟迟不动手做“字符串单词统计”这种综合性强的题目。实际上,基础档做两三道确认自己会了就可以往后走,综合档才值得你画图、打断点、反复调试。

第五版相比更早的版本,增加了一些偏算法思维的题目,像是“猴子吃桃”“汉诺塔”“数字旋转方阵”这类。这些题刚看到时会觉得难,但它们才是真正拉开差距的地方。把这些题做一遍,再回头看前面的基础循环,会有一种“降维打击”的轻松感。建议按档位分配时间,至少要保证综合档和思维档的题目占到总时间的一半以上。

2. 动手前,先想清楚三件事:输入、输出、边界

2.1 读题时先画“输入输出模型”,而不是画重点线

很多同学做题有个习惯,读题时拿笔把题目里的关键词圈出来,比如“从大到小”“素数”“逆序输出”,然后就开始写代码。这个方法不能说错,但它容易漏掉一个最关键的信息:这个程序到底要接收什么样的输入,要输出什么样的结果。

我建议大家拿到任何一道课后题,先在草稿纸上写两行字:

  • 输入:什么类型?一个数还是多个数?单组数据还是多组数据?
  • 输出:一行还是多行?有没有空格间隔?有没有特殊换行要求?

举个例子,教材里常见的“统计一行字符串中的单词个数”这道题。假如题目只写了“输入一行字符,统计其中单词的个数”,你一定得先问自己几个问题:字符串开头如果有空格怎么算?两个单词之间如果有多个空格怎么算?换行符算不算一个字符?这些如果不提前想清楚,你写出来的代码只能处理“恰好每个单词之间只有一个空格”的理想情况,稍微一变就崩。

把输入输出模型写下来的过程中,你其实是在把题目从自然语言翻译成程序语言。一旦翻译完成,后面的代码就是在实现一个你自己已经理解清楚的逻辑。大多数卡壳,卡的根本不是语法,而是你根本没想清楚程序要对什么数据做什么事。

2.2 边界值提前写在草稿上,而不是错一次才想起来

边界值这个东西,是初学者最容易忽略、老师又最喜欢考的。最简单的例子:输入n个数求最大值,很多人会把max的初始值设成0。题目正常给一组正数,程序运行没问题。但要是输入的全是负数呢?

如果max初始化为0,那么这一组负数都和0比较,最终输出结果就是0,而正确答案应该是最大的那个负数。这就是典型的边界条件没考虑清楚。

正确的做法是把max初始化为数组的第一个元素,或者输入的第一个数,然后从第二个元素开始遍历。这样不管输入是正数还是负数,结果都对。你把max初始化为0的那一瞬间,其实已经默认了“输入里一定存在比0大的数”,这个隐含假设一旦建立,就埋下了一颗雷。

其他常见的边界值还包括:输入0、输入负整数、输入上限值(比如n为100时数组长度够不够)、输入字符串为空、输入多个空格连续出现。这些情况不一定会出现在教材样例里,但判题系统或者老师的隐藏测试数据里大概率会有。我自己的习惯是把这些边界值写在草稿纸最上面,每写完一个程序就对照着跑一遍,跑完再交。这个习惯帮我在各类考试和比赛里少丢了无数冤枉分。

2.3 单组数据还是多组数据,决定了循环长什么样

课后题里有一类题,光看题目看不出数据有几组。比如“输入若干行字符串,统计每行有多少个单词”,这就意味着你的程序要能处理多组输入,直到用户停止输入为止。

处理多组数据,最常用的写法是去检查scanf的返回值:

int n; while (scanf("%d", &n) == 1) { // 对每组数据做处理 }

这里要特别注意:scanf的返回值表示成功读取到的变量个数,而不是是否读到文件末尾。用while (scanf("%d", &n) != EOF)当然也能处理正常情况,但如果输入了非数字字符,scanf会返回0而不是EOF,程序就陷入死循环。相比之下,== 1的写法更严谨,因为它同时保证了“读到了”和“读进去的是数字”。

如果你用的是字符串处理函数,常见的是while (fgets(buf, sizeof(buf), stdin) != NULL)。fgets读到文件末尾会返回NULL,用这个判断多组字符串输入很可靠。注意fgets会把换行符一起读进字符串里,统计字符时如果不想把换行算进去,要自己处理掉。

多组数据的题目还有一个隐藏要求:输出格式。有的题目要求每组结果占一行,有的要求每组结果之间空一行。这个细节在后面的输出格式坑里会详细说,但你在设计输入输出模型时就要先看明白题目到底要哪种。

3. 四道高频题型拆解:从会抄答案到能讲出为什么

3.1 素数判断:边界条件与break的位置

素数判断几乎是所有教材第三章循环题的标配。题目很简单:输入一个正整数,判断它是否为素数。最直接的思路是枚举从2到n-1的所有整数,看能不能整除n。这个思路正确,但效率低,而且代码也不够干净。

通常优化的写法是只需要遍历到sqrt(n):

#include <stdio.h> #include <math.h> int is_prime(int n) { int i; if (n <= 1) return 0; for (i = 2; i <= (int)sqrt(n); i++) { if (n % i == 0) return 0; } return 1; } int main(void) { int n; scanf("%d", &n); if (is_prime(n)) printf("yes\n"); else printf("no\n"); return 0; }

为什么只需要到sqrt(n)?因为如果n有一个大于sqrt(n)的因子a,那就必然存在一个小于sqrt(n)的因子b = n / a。换句话说,如果从2到sqrt(n)之间找不到因子,更大的范围也不可能找到。这是很多初学者理解不了的数学结论,但你可以用具体数字验证:24的因子是2、3、4、6、8、12,其中必然有一个因子小于等于sqrt(24)≈4.9。

还有两个细节容易出错。第一是n <= 1要先排除掉,因为1既不是素数也不是合数。第二是如果用for (i = 2; i * i <= n; i++)这种写法,当n接近int上限时,i * i可能溢出变成负数,导致死循环。稳妥的方案是使用sqrt(n),或者把i声明为long long再计算i * i。

3.2 统计单词个数:用状态机代替一长串if

“输入一行字符,统计其中单词的个数”是第六章很经典的一道题。很多人的第一反应是数空格的数量,然后用“空格数加1”作为单词数。这个方案在“单词之间只有一个空格”时成立,可一旦出现开头空格、连续多个空格、结尾空格,就全乱了。

更稳的做法是维护一个标志变量in_word,表示当前是否处于一个单词内部:

#include <stdio.h> #include <ctype.h> int main(void) { char s[1024]; int i, cnt, in_word; while (fgets(s, sizeof(s), stdin) != NULL) { cnt = 0; in_word = 0; for (i = 0; s[i] != '\0'; i++) { if (isalpha(s[i])) { if (!in_word) { cnt++; in_word = 1; } } else { in_word = 0; } } printf("%d\n", cnt); } return 0; }

这段代码的核心逻辑是:遇到字母时判断“是否刚刚从一个非字母字符走过来”,如果是,说明新单词开始了,计数加1。遇到非字母字符(空格、标点、换行符),就把状态置为0,表示当前不在单词里。

这个思路其实就是计算机领域里的“状态机”概念,用一个状态变量跟踪程序当前处于什么阶段。你不需要学过状态机这个名字,只需要理解“用一个变量记住上一个字符的类型”这个思想,就能优雅地处理这类问题。如果你只用strlen加空格计数的方式,输入“ hello world ”(开头和结尾都有空格)时,得到的结果会多出1或2个,而状态机写法可以完全免疫这些情况。

注意fgets会把换行符读进来,换行符不是字母,所以走else分支把in_word置0,对计数没有影响,这个设计很自然地处理掉了行尾问题。

3.3 有序数组插入:从后往前搬,难度一下就低了

数组插入是第四章经常出现的题目:有一个已经排好序的数组,输入一个数,要求按原来的规律把它插入到正确位置。这道题的核心操作是“搬元素”,而搬元素的顺序决定了代码的复杂度。

正确的做法是从最后一个元素开始,从后往前依次比较并后移:

#include <stdio.h> int main(void) { int a[11] = {1, 4, 7, 10, 13, 16, 19, 22, 25, 28}; int n = 10; int x, i; scanf("%d", &x); for (i = n - 1; i >= 0 && a[i] > x; i--) { a[i + 1] = a[i]; } a[i + 1] = x; n++; for (i = 0; i < n; i++) { printf("%d ", a[i]); } printf("\n"); return 0; }

为什么必须从后往前?假设从前往后搬,第一步就会把a[0]的值覆盖到a[1]上,此时a[0]原来的值已经丢了,想要把它搬到后面的位置就需要额外变量保存。而从后往前搬时,每次都是把较大的元素搬到它后面的空位里,后面的位置已经被搬走了,不会再被用到,天然安全。

这个循环结束后,i停下来的位置就是插入点前一个位置。所以用a[i + 1] = x把新元素放进去就刚刚好。很多人在这一步会晕,解题技巧是手动模拟一遍:假设数组是{1, 4, 7},插入5,你用纸写几轮循环,很快就能理解i的终止值为什么就是插入位置减1。

3.4 两数交换与函数封装:指针参数为什么非得画出图

第七章讲函数时,有一道几乎必做的题:写一个函数交换两个整数的值。很多人在这个题上第一次感受到指针的威力,也第一次感受到C语言的凶险。

void swap(int *x, int *y) { int temp; temp = *x; *x = *y; *y = temp; }

为什么不能写void swap(int x, int y)?因为C语言的函数参数默认是值传递,传入的只是实参的一份拷贝。在函数内部交换x和y,只会改变拷贝的值,函数结束后拷贝销毁,实参纹丝不动。

理解这个问题的关键是画出内存图。你在草稿纸上画两个格子,代表main里的a和b,再画两个格子,代表swap函数参数里的x和y。值传递时,x和y是独立的格子,怎么交换都影响不到a和b。指针传递时,x和y里装的是a和b的地址,通过地址找到原来的格子,改动就能生效。

很多同学卡在指针这章,就是跳过了画图这个步骤,硬背语法。指针的语法本身不复杂,难的是建立“地址”这个思维模型。凡是遇到指针相关的课后题,我都建议花三分钟画一张图,画完再写代码,准确率会明显上升。

4. 那些“本地能跑、上交就错”的细节,全在这里

4.1 void main是第一坑

在学校机房或者自己电脑上,void main()在部分编译器里能跑出来,甚至很多老师上课也这么写。但严格来说,C标准规定main函数的返回类型应该是int,写成void main()在标准C里是不合法的。

第五版教材里通常给的示例是int main(void)。如果你养成了写void main的习惯,在某些判题系统或新版GCC编译器下,可能能编译,但可能会收到一个警告,甚至直接判编译错误。最稳的做法就是老老实实写:

int main(void) { return 0; }

另外还有一个容易被忽略的环境坑:很多学生用老版本的Dev-C++写作业,这个IDE默认可能按C++语法来编译C文件,导致一些在C语言里合法的写法在编译时报错。最典型的例子是用//注释没问题,但变量声明位置、for循环里声明变量这类C99标准才支持的特性,在老编译器上可能出问题。如果你发现“明明书上这么写,我的编译器就报错”,先看一眼编译标准设置,而不是怀疑书错了。

4.2 输入缓冲区里的“隐形人”:换行符

C语言里用scanf读入数据时,缓冲区里残留的换行符经常制造让人抓狂的bug。最典型的场景是:先读一个整数,再读一个字符。

int n; char ch; scanf("%d", &n); scanf("%c", &ch); // 这里读到的可能是换行符

第一次scanf("%d")读取整数后,会留下一个换行符在缓冲区里。第二次scanf("%c")读到的就是这个换行符,而不是你真正想读的那个字符。解决办法很简单,在%c前面加一个空格:

scanf(" %c", &ch);

scanf格式串里的空格会匹配任意数量的空白字符,包括换行符、空格和制表符,这样就能跳过那个残留的换行符。这个坑在教材里不太显眼,但作业和考试里出现频率极高。另一个相关坑是gets函数在C11标准中已经被移除,继续使用会编译失败。建议统一使用fgets(buf, sizeof(buf), stdin)来读字符串。

4.3 数组越界是非常隐蔽的炸弹

C语言的数组没有边界检查,访问越界时不会在运行时报“数组越界”错误,而是直接访问了内存里别的位置。这个行为在C标准里叫未定义行为,意思是程序什么事都可能发生。

举一个常见场景:定义一个长度为n的字符数组,用来存放n个字符的字符串。很多人会这么写:

char s[5]; scanf("%s", s); // 输入 "hello",长度5

字符串"hello"除了5个字母外,末尾还有一个字符串结束符'\0',总共需要6个字节。数组只有5个字节,'\0'被写到了数组外面,这就是越界。现象可能目前看不见,程序也正常输出了,但内存里那个位置可能存着别的重要数据,一旦被破坏,程序之后的行为就会变得诡异。

正确做法是至少开n+1的空间。数组越界的bug最讨厌的地方在于,它不一定会立刻崩溃,可能跑了很多次之后才随机出错,非常难排查。我自己的习惯是,凡是处理字符串,先算清楚最大长度,多开几个字节,绝不顶着上限用。这个习惯帮我避免了很多线上问题。

4.4 输出格式:差一个空格都不算对

程序逻辑完全正确,OJ上却判错,最常见的元凶是输出格式。课后题和考试题对格式的要求各不相同,有的要求每行末尾不能有多余空格,有的要求每组数据之间空一行,有的要求“从小到大输出,元素之间用空格隔开”。

以输出数组为例,很多人会这么写:

for (i = 0; i < n; i++) { printf("%d ", a[i]); } printf("\n");

这个写法在显示器上看没问题,但行尾会多出一个空格。如果判题系统对输出做了严格比对,这个多出来的空格会被判定为格式错误。更稳妥的写法是先输出第一个元素,后续元素前面加空格:

for (i = 0; i < n; i++) { if (i > 0) printf(" "); printf("%d", a[i]); } printf("\n");

还有一种多组数据之间的空行要求。题目写“每组输出之间空一行”,意味着除了最后一组外,每组结束后要额外输出一个换行。用计数器或flag控制就行。这类细节看着不起眼,但我见过太多人卡在这里反复提交十几次,最后发现就是少处理了一个换行。

我把这些常见环境坑整理成一个表,方便对照:

现象常见原因解决办法
void main编译警告main返回类型不符合标准改成int main
读字符结果不对缓冲区残留换行符scanf(" %c", &ch)
gets编译失败C11标准移除了gets用fgets
字符串处理全对但结果错字符数组长度没留'\0'位置至少开n+1字节
逻辑对但判错行尾多空格或组间空行不对按i>0方式控制空格输出

5. 把课后题当成科目二练习:一题多解和验收方法

5.1 同一道题,用三种思路各做一遍

科目二练车的时候,教练会让你反复练习同一个动作直到形成肌肉记忆。课后题也应该这样练,尤其是逻辑档和综合档的题目,一道题至少写三遍,每遍用不同的思路。

以最大公约数为例,第一遍你可能会用最简单的枚举法:

int gcd = 1; for (int i = 1; i <= a && i <= b; i++) { if (a % i == 0 && b % i == 0) { gcd = i; } }

第二遍用辗转相除法:

int t; while (b) { t = a % b; a = b; b = t; }

第三遍尝试用递归:

int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }

三个版本做完,你不只是会了“求最大公约数”这一道题,你还理解了枚举、循环迭代、递归三种思想,后面再遇到类似的问题就能灵活迁移。很多同学一套代码写完就交,做完就忘,第二天碰到稍微变形的问题又不会了。原因不是不聪明,而是做得太少、太浅。一题多解是性价比最高的练习方式。

5.2 怎么看待网上流传的题解答案

现在网上随便一搜“C程序设计第五版答案”,能找到一堆资源。打开之后你会发现一个尴尬的事实:很多资源号称是第五版答案,实际整理得参差不齐,有的题号对不上,有的代码风格混乱,有的干脆是旧版答案套了个新封面。还有一些搜索结果是其他教材的答案,比如《C语言程序设计》某个版本,或者是《C++程序设计》的内容,跟谭浩强这本书完全不搭。

正确使用答案的方式,不是照着抄,也不是完全不看。我的建议是:一道题自己先写,卡住15分钟还无进展,再去看答案。看的时候只看思路,不看代码,看完把书合上,自己从头写一遍。这样既避免死磕浪费时间,又能保证代码是你自己敲出来的。直接抄一遍或者把代码背下来,除了自我安慰没有任何学习效果。

还有一点,网上流传的题目答案,代码质量差距很大。有的写法虽然是“能跑”的,但变量命名混乱、逻辑绕弯、完全不考虑边界条件。你看到一份答案时,先想想它有没有处理n<=1这种情况,再想想它有没有考虑输入负数。好的答案值得学,劣质的答案反而会带偏你。

5.3 给自己加一张“测试用例检查单”

每次做完一道课后题,不要急着下一题,先花两分钟做一轮自检。我习惯用一个小清单:

  • 正常用例:比如输入5输出25这种最典型的用例,确认主流程跑通
  • 边界用例:输入0、1、负数、上限值,确认边界不出错
  • 特殊用例:空输入、多空格、连续换行,确认程序不会死循环或崩溃
  • 格式检查:输出行首行尾有没有多余空格,多组数据之间的空行对不对

这个检查单用起来很快,每题多花两分钟,却能提前排除掉八成以上的隐藏错误。很多同学做题时只拿题目给的样例测试一下,样例过了就觉得自己已经完成了。问题是教材给的样例通常是最平和的普通情况,把样例跑通只能说明你的程序没写歪,不能说明它没有漏洞。

我自己现在的习惯是,把这道题当成一个交给用户的小项目来验收。如果连自己这关都过不了,就不要急着去看下一道题。这种严谨的习惯一旦养成,不只是课后题受益,以后参加考试、做课设、参加工作,都会跟着受益。

如果现在正有一道课后题卡着你,我的建议很简单——先按第二章那套方法,把输入、输出、边界三件事写在纸上,然后给自己三个用例,关掉所有参考代码,从int main开始写。写不出来就打断点,一步步看变量变化。你会发现,C程序设计这本书的课后题答案,其实永远不该是你搜来的那份,而应该是你亲手调试通过后,自己写下的那一份。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询