谭浩强C++第三章习题精讲:从基础语法到工程级代码的进阶指南
1. 为什么第三章“程序设计初步”值得反复刷1.1 这一章到底在讲什么谭浩强老师的《C程序设计》第三章标题叫“程序设计初步”很多人第一遍看的时候觉得内容不多——无非是赋值语句、输入输出、if判断、循环结构这些。但真正写过几年代码再回头看会发现这一章其实是整个C学习路径上最关键的“地基”。它不讲类、不讲模板、不讲STL它讲的是你每天写代码都在用的东西怎么让程序接收数据、怎么根据条件走不同分支、怎么把一段逻辑重复执行到满足条件为止。我见过太多人学完这一章就急着往后翻结果到了数组、函数、指针那几章写出来的代码逻辑混乱、边界条件不考虑、循环变量乱飞。根子就在第三章没吃透。所以这篇内容我打算把第三章的习题从头到尾捋一遍不只是给答案而是把每道题背后的设计思路、容易踩的坑、以及可以改进的地方都讲清楚。1.2 适合谁来参考如果你正在学C用的是谭浩强的教材第三章的习题做完之后不确定自己写得对不对那这篇内容可以直接对照。如果你已经学过一遍但觉得自己写代码总是“能跑就行”想回头把基础打扎实那这篇也适合你。甚至如果你是在准备考试或者面试第三章的习题里涉及的输入输出格式控制、条件判断的嵌套、循环的边界处理都是笔试和机试里反复出现的考点。我写这篇的原则是不只给答案要给思路不只给能跑的代码要给经得起推敲的代码。有些习题的原答案存在一些小问题比如变量命名不规范、边界条件没考虑、输入输出格式不够健壮我会在给出基础答案之后补充改进版本并说明为什么要这样改。1.3 第三章习题的整体分类翻一遍第三章的习题大致可以分成四类输入输出与格式控制类主要考察cin、cout的使用以及setw、setprecision等格式控制符。这类题看起来简单但格式细节很容易出错。条件判断类涉及if、if-else、if-else if、嵌套if以及switch语句。核心考察的是逻辑分支的完整性和边界条件的处理。循环结构类包括for、while、do-while三种循环以及循环嵌套。这类题最容易出现死循环、循环变量初值和终值搞错、累加变量没初始化等问题。综合应用类把输入输出、条件判断、循环结合起来比如求素数、求最大公约数、打印图形等。这类题最能检验你是否真正理解了前面的知识点。下面我按照这个分类逐类拆解典型习题给出答案和改进方案。2. 输入输出与格式控制类习题精讲2.1 基础输入输出题的常见陷阱第三章开头几道题通常是这样的从键盘输入两个整数输出它们的和、差、积、商。看起来简单到不需要思考但我批改过很多初学者的作业发现几个高频错误。第一个错误是变量类型选择不当。比如求两个整数的商如果直接用int类型的变量接收结果5/2会得到2而不是2.5。正确的做法是把被除数强制转换为double或者直接用double类型的变量来接收。原答案有时候会忽略这一点导致输出结果不符合预期。第二个错误是输入提示信息缺失。很多初学者写出来的程序是这样的int a, b; cin a b; cout a b endl;程序运行后光标闪烁用户完全不知道要输入什么。正确的做法是在cin之前加一句cout 请输入两个整数 endl;。这不是“多此一举”而是良好编程习惯的起点。第三个错误是没有处理输入失败的情况。如果用户输入的不是整数cin会进入失败状态后续所有输入操作都会失效。虽然第三章的习题通常不要求处理这种情况但我在改进版里会加上简单的输入检查让程序更健壮。2.2 格式控制符的实战用法第三章有一类题专门考察输出格式比如要求按指定宽度输出、保留指定小数位数、以十六进制或八进制输出等。这里涉及iomanip头文件里的几个关键控制符控制符作用使用示例注意事项setw(n)设置下一个输出项的字段宽度cout setw(8) 123;只对下一个输出项有效需要重复设置setprecision(n)设置浮点数有效位数或小数位数cout setprecision(3) 3.14159;默认是有效位数配合fixed才是小数位数fixed以定点表示法输出浮点数cout fixed setprecision(2) 3.14159;一旦设置后续所有浮点输出都受影响hex/oct/dec切换进制输出cout hex 255;同样会影响后续所有整数输出setfill(c)设置填充字符cout setfill(*) setw(6) 42;配合setw使用默认填充空格我见过一个典型的坑有同学写了cout setprecision(2) 3.14159;期望输出3.14结果输出了3.1。原因就是没有加fixedsetprecision(2)设置的是有效位数而不是小数位数。加上fixed之后才能得到期望的结果。另一个坑是setw的作用范围。它只影响紧接着的下一个输出项之后的输出项又恢复默认宽度。所以如果你要输出一个表格每一列都需要单独设置setw。2.3 改进版输入输出代码示例以“输入两个整数输出它们的和、差、积、商”为例我给出一个改进版#include iostream #include iomanip using namespace std; int main() { int a, b; cout 请输入两个整数用空格分隔 flush; if (!(cin a b)) { cout 输入无效请确保输入的是整数。 endl; return 1; } cout 和 a b endl; cout 差 a - b endl; cout 积 a * b endl; if (b ! 0) { cout fixed setprecision(4); cout 商 static_castdouble(a) / b endl; } else { cout 商除数不能为0 endl; } return 0; }这个版本相比原答案做了几处改进加了输入提示、加了输入有效性检查、处理了除数为零的情况、用static_cast做了类型转换、用fixed配合setprecision控制了小数位数。这些改进并不复杂但体现了“程序设计初步”阶段就应该养成的严谨习惯。注意static_castdouble(a)是C推荐的类型转换方式比C风格的(double)a更安全、更易读。在第三章阶段就可以开始使用。3. 条件判断类习题的深度拆解3.1 if-else链的完整性设计条件判断类习题里最典型的是“根据成绩输出等级”这类题。比如90分以上为A80-89为B70-79为C60-69为D60以下为E。很多初学者写出来的代码是这样的if (score 90) cout A; if (score 80) cout B; if (score 70) cout C; if (score 60) cout D; if (score 60) cout E;这段代码的逻辑是错误的。如果输入95前四个条件都成立会输出“ABCD”。正确的写法应该是用else ifif (score 90) cout A; else if (score 80) cout B; else if (score 70) cout C; else if (score 60) cout D; else cout E;else if的本质是“前面的条件不成立时才检查下一个条件”这样就保证了只有一个分支会被执行。这个知识点在第三章是重点但很多人第一次学的时候没有真正理解。还有一个细节最后的else不需要再写score 60因为前面所有条件都不成立时剩下的唯一可能就是小于60。这样写既简洁又不会遗漏。3.2 嵌套if的层次与缩进嵌套if是第三章的另一个重点。比如“判断一个年份是否为闰年”的经典题目能被4整除但不能被100整除或者能被400整除。写成代码if (year % 4 0) { if (year % 100 0) { if (year % 400 0) { cout year 是闰年 endl; } else { cout year 不是闰年 endl; } } else { cout year 是闰年 endl; } } else { cout year 不是闰年 endl; }这段代码逻辑是对的但嵌套层次太深可读性差。改进版可以合并条件if ((year % 4 0 year % 100 ! 0) || year % 400 0) { cout year 是闰年 endl; } else { cout year 不是闰年 endl; }改进版的逻辑更清晰一行条件表达式就把闰年的定义完整表达了。这里用到了逻辑运算符和||的短路特性左边为假时右边不再计算||左边为真时右边不再计算。理解短路特性对后续学习很重要。实操心得写嵌套if时每进入一层就增加一级缩进并且用大括号把每个分支的代码块括起来即使只有一条语句也建议加。这样以后要添加代码时不容易出错。3.3 switch语句的适用场景与陷阱switch语句适合处理“多分支且分支条件是同一个表达式的不同取值”的情况。比如“输入一个数字输出对应的星期几”switch (day) { case 1: cout 星期一; break; case 2: cout 星期二; break; case 3: cout 星期三; break; case 4: cout 星期四; break; case 5: cout 星期五; break; case 6: cout 星期六; break; case 7: cout 星期日; break; default: cout 输入无效; break; }这里最大的坑是忘记写break。如果漏掉了某个break程序会继续执行下一个case的代码这叫“穿透”。有时候穿透是有意为之的比如多个case共享同一段代码switch (grade) { case A: case B: cout 优秀; break; case C: cout 良好; break; default: cout 需努力; }但大多数情况下忘记break都是bug。我的建议是除非你明确知道自己在利用穿透否则每个case后面都写上break。另一个坑是switch的表达式类型。C中switch只能用于整型、字符型、枚举型不能用于浮点型或字符串。如果你要判断的是浮点数范围还是得用if-else。4. 循环结构类习题的完整实现4.1 for循环的边界控制循环类习题里for循环的边界控制是出错的重灾区。以“求1到100的和”为例int sum 0; for (int i 1; i 100; i) { sum i; } cout 1到100的和为 sum endl;这段代码看起来没问题但初学者常犯的错误包括i的初值写成0、条件写成i 100、忘记初始化sum、循环体后面多写分号等。每一个错误都会导致结果不对。我建议在写for循环之前先问自己三个问题循环变量从哪开始到哪结束每次变化多少把这三个问题的答案写清楚再动手写代码。比如上面的例子从1开始到100结束包含100每次加1。对应到代码就是i 1; i 100; i。还有一个常见的坑是循环变量的作用域。在C中for (int i 0; ...)里声明的i只在循环体内有效循环结束后就不能再用了。如果你需要在循环结束后访问i的值就要把i声明在循环外面。这一点在第三章可能不太明显但到了后面写复杂程序时会经常遇到。4.2 while与do-while的选择while和do-while的区别在于while先判断条件再执行循环体do-while先执行一次循环体再判断条件。也就是说do-while至少会执行一次循环体。以“输入密码直到输入正确为止”为例string password; do { cout 请输入密码; cin password; } while (password ! 123456); cout 密码正确欢迎进入 endl;这里用do-while是合适的因为用户至少要输入一次密码。如果用while就需要在循环之前先写一次输入语句代码会显得冗余。但do-while也有一个容易忽略的细节while后面的分号不能少。do { ... } while (条件);这个分号是语法要求漏掉会编译错误。4.3 循环嵌套与图形打印循环嵌套是第三章的难点典型题目是打印各种图形比如直角三角形、倒三角形、金字塔等。以打印一个n行的直角三角形为例int n; cout 请输入行数; cin n; for (int i 1; i n; i) { for (int j 1; j i; j) { cout *; } cout endl; }外层循环控制行数内层循环控制每行打印的星号数。关键是找到行号i和星号数之间的关系第i行有i个星号。如果要打印金字塔形状就需要在每行的星号前面打印空格。空格数和行号的关系是第i行有n - i个空格。代码for (int i 1; i n; i) { for (int j 1; j n - i; j) { cout ; } for (int k 1; k 2 * i - 1; k) { cout *; } cout endl; }这里星号数是2 * i - 1因为金字塔每行的星号数是奇数递增的。这个关系需要自己推导一下第1行1个第2行3个第3行5个……所以第i行是2i - 1个。避坑技巧写循环嵌套时先用小数据量手动模拟一遍。比如n3手动写出每一行应该输出什么然后对照代码看是否一致。这个方法能帮你快速定位边界错误。5. 综合应用类习题的完整实现5.1 求最大公约数与最小公倍数求两个正整数的最大公约数经典解法是辗转相除法欧几里得算法。原理是两个数的最大公约数等于其中较小的数和两数相除余数的最大公约数。代码实现int a, b; cout 请输入两个正整数; cin a b; int m a, n b; while (n ! 0) { int r m % n; m n; n r; } cout a 和 b 的最大公约数是 m endl; cout 最小公倍数是 a * b / m endl;这里有几个细节需要注意第一用m和n作为工作变量保留原始的a和b用于输出第二最小公倍数的计算公式是a * b / 最大公约数但要注意a * b可能会溢出更安全的写法是a / m * b第三辗转相除法的循环条件是n ! 0当n为0时m就是最大公约数。5.2 判断素数及其优化判断一个数是否为素数的基本思路是从2到sqrt(n)逐一试除如果都不能整除就是素数。代码int n; cout 请输入一个正整数; cin n; bool isPrime true; if (n 2) { isPrime false; } else { for (int i 2; i * i n; i) { if (n % i 0) { isPrime false; break; } } } if (isPrime) { cout n 是素数 endl; } else { cout n 不是素数 endl; }这里用i * i n代替i sqrt(n)避免了浮点数运算和头文件引入。另外一旦发现能整除就立即break不需要继续检查。还有一个优化是只检查到sqrt(n)因为如果n有一个大于sqrt(n)的因子那必然有一个小于sqrt(n)的对应因子。5.3 求水仙花数水仙花数是指一个三位数其各位数字的立方和等于该数本身。比如153 1³ 5³ 3³。代码for (int i 100; i 999; i) { int a i / 100; int b (i / 10) % 10; int c i % 10; if (a * a * a b * b * b c * c * c i) { cout i ; } } cout endl;这道题的关键是提取各位数字百位用i / 100十位用(i / 10) % 10个位用i % 10。这个技巧在后续处理数字问题时经常用到值得记住。6. 常见问题与排查技巧实录6.1 编译错误速查表错误信息常见原因解决方法expected ; before ...上一条语句漏了分号检查上一条语句末尾cout was not declared没有包含iostream或没有using namespace std;添加头文件和命名空间i was not declared循环变量作用域问题把变量声明移到循环外面cannot convert ...类型不匹配使用static_cast进行显式转换lvalue required赋值号左边不是变量检查赋值语句左边6.2 逻辑错误排查方法逻辑错误比编译错误更难发现因为程序能跑但结果不对。我的排查步骤通常是缩小范围把输入数据改小比如求1到100的和改成求1到5的和手动计算期望结果然后看程序输出是否一致。打印中间变量在关键位置加cout输出变量的值观察是否符合预期。比如循环里输出i和sum的当前值。检查边界重点检查循环的初值、终值、条件判断的边界。比如i n和i n的区别。检查初始化累加变量是否初始化为0累乘变量是否初始化为1标志变量是否初始化为正确的值。6.3 几个容易忽略的细节浮点数比较不要直接用比较两个浮点数因为浮点数在计算机中不能精确表示。比如0.1 0.2 0.3在C中可能是false。正确的做法是判断两个浮点数的差值是否小于一个很小的数比如fabs(a - b) 1e-6。整数除法两个整数相除结果还是整数小数部分会被截断。如果需要小数结果要把其中一个操作数转换为浮点数。输入缓冲区cin和getline混用时cin留下的换行符会被getline读取导致getline读到空行。解决方法是在cin之后加一句cin.ignore()。循环变量命名外层循环用i内层循环用j再内层用k这是约定俗成的做法。不要用a、b、c作为循环变量容易和普通变量混淆。6.4 改进版代码的通用原则回头看第三章的习题很多原答案虽然能得出正确结果但在健壮性、可读性、可维护性方面有提升空间。我总结了几条改进原则输入提示任何需要用户输入的地方都加上清晰的提示信息。输入检查对用户输入进行有效性验证比如除数不能为0、成绩不能为负数等。边界处理考虑所有可能的输入情况包括边界值和异常值。变量命名用有意义的变量名比如sum、average、maxValue而不是a、b、c。代码缩进保持一致的缩进风格通常用4个空格或一个Tab。注释对复杂的逻辑添加注释说明“为什么这样做”而不是“做了什么”。这些原则在第三章阶段可能显得“要求过高”但正是这些习惯决定了你后续能走多远。我见过太多人到了数据结构、算法阶段还在为变量命名和边界条件头疼根源就是基础阶段没有养成好习惯。7. 从第三章习题延伸出的学习路径第三章的习题做完之后如果你觉得还有余力可以尝试几个延伸方向。第一个方向是把习题中的程序改写成函数。比如把“判断素数”的逻辑封装成一个bool isPrime(int n)函数把“求最大公约数”封装成int gcd(int a, int b)函数。这样既能复习第三章的知识又能提前接触函数的概念。第二个方向是增加异常处理。比如在输入阶段检查用户输入是否合法在计算阶段检查是否会出现除以零、溢出等情况。虽然第三章还没有正式讲异常处理机制但用if判断来处理这些情况是完全可行的。第三个方向是优化算法效率。比如求素数时只检查到sqrt(n)求最大公约数时用辗转相除法而不是暴力枚举。这些优化在第三章阶段就可以开始思考对后续学习算法很有帮助。第四个方向是尝试用不同的循环结构实现同一个功能。比如用for、while、do-while分别实现“求1到100的和”比较三种写法的差异。这样能更深刻地理解三种循环的适用场景。我个人在实际操作中的体会是第三章的习题虽然基础但真正把每一道题都写出“生产级”的代码并不容易。它要求你不仅理解语法还要考虑各种边界情况、输入输出的友好性、代码的可读性。这些能力不是一蹴而就的需要反复练习和刻意改进。如果你能把第三章的习题都认真做一遍并且对照改进版思考“为什么要这样改”那后续的学习会顺畅很多。