ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C程序设计第五版课后题全攻略:从做题思路到踩坑记录

C程序设计第五版课后题全攻略:从做题思路到踩坑记录 很多第一次翻开《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被写到了数组外面这就是越界。现象可能目前看不见程序也正常输出了但内存里那个位置可能存着别的重要数据一旦被破坏程序之后的行为就会变得诡异。正确做法是至少开n1的空间。数组越界的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位置至少开n1字节逻辑对但判错行尾多空格或组间空行不对按i0方式控制空格输出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分钟还无进展再去看答案。看的时候只看思路不看代码看完把书合上自己从头写一遍。这样既避免死磕浪费时间又能保证代码是你自己敲出来的。直接抄一遍或者把代码背下来除了自我安慰没有任何学习效果。还有一点网上流传的题目答案代码质量差距很大。有的写法虽然是“能跑”的但变量命名混乱、逻辑绕弯、完全不考虑边界条件。你看到一份答案时先想想它有没有处理n1这种情况再想想它有没有考虑输入负数。好的答案值得学劣质的答案反而会带偏你。5.3 给自己加一张“测试用例检查单”每次做完一道课后题不要急着下一题先花两分钟做一轮自检。我习惯用一个小清单正常用例比如输入5输出25这种最典型的用例确认主流程跑通边界用例输入0、1、负数、上限值确认边界不出错特殊用例空输入、多空格、连续换行确认程序不会死循环或崩溃格式检查输出行首行尾有没有多余空格多组数据之间的空行对不对这个检查单用起来很快每题多花两分钟却能提前排除掉八成以上的隐藏错误。很多同学做题时只拿题目给的样例测试一下样例过了就觉得自己已经完成了。问题是教材给的样例通常是最平和的普通情况把样例跑通只能说明你的程序没写歪不能说明它没有漏洞。我自己现在的习惯是把这道题当成一个交给用户的小项目来验收。如果连自己这关都过不了就不要急着去看下一道题。这种严谨的习惯一旦养成不只是课后题受益以后参加考试、做课设、参加工作都会跟着受益。如果现在正有一道课后题卡着你我的建议很简单——先按第二章那套方法把输入、输出、边界三件事写在纸上然后给自己三个用例关掉所有参考代码从int main开始写。写不出来就打断点一步步看变量变化。你会发现C程序设计这本书的课后题答案其实永远不该是你搜来的那份而应该是你亲手调试通过后自己写下的那一份。
返回列表