1. 从第三章习题说起:为什么“会写”和“写对”是两码事
很多人学C++,教材翻到第三章“程序设计初步”,觉得内容无非是变量、运算符、输入输出、顺序结构这些“小儿科”。我当年也这么想,直到带过几批刚入门的同学之后才发现,这一章的习题才是真正拉开差距的地方。谭浩强老师的教材第三章,表面上看是让你熟悉cin、cout、printf、scanf这些基本工具,实际上它在训练一种更底层的能力——把数学表达式准确翻译成计算机能执行的语句,并且保证在各种边界条件下都不出错。
这一章的核心关键词其实就几个:顺序结构、数据类型、运算符优先级、输入输出格式控制。看起来简单,但习题里埋的坑一点都不少。比如“求两个整数之和”这种题,初学者写出来往往是这个样子的:
int a, b; cin >> a >> b; cout << a + b;能跑,但稍微改一下要求——比如要求输出格式为“a + b = sum”——很多人就开始手忙脚乱,忘了字符串拼接和变量输出的顺序。再比如“已知半径求圆面积”这类题,有人直接用3.14当π,结果精度不够;有人用了3.1415926,但没考虑double和float的区别,输出结果末尾一堆零。这些细节,教材正文里不会逐条强调,但习题答案里如果也不讲清楚,那刷题的意义就丢了一大半。
我写这篇东西的出发点很简单:把第三章习题里那些“看起来会做、实际上容易做错”的地方掰开揉碎讲一遍。不是单纯贴代码,而是把每道题背后的设计意图、常见错误、改进思路都说清楚。适合正在学C++基础的同学对照检查,也适合已经工作但想回头夯实基本功的人快速过一遍。下面我会按题型分类,把顺序结构、输入输出、运算符、类型转换这几块分别拆解,每道题都给出“原始解法”和“改进解法”的对比,并说明为什么改进版更靠谱。
2. 顺序结构习题的隐藏考点:不只是从上到下执行
2.1 变量声明的位置与初始化时机
第三章最基础的习题就是“输入两个数,输出它们的和”。教材给的参考代码通常是在main函数开头声明变量,然后cin读入,最后cout输出。这个流程没问题,但习题答案里很少提一个关键点:变量声明时最好直接初始化。比如:
int a = 0, b = 0; cin >> a >> b; int sum = a + b; cout << sum << endl;为什么要初始化?因为如果输入流因为某种原因失败(比如用户输入了字母),a和b的值就是未定义的,后续计算全是垃圾值。虽然基础习题不要求处理输入失败,但养成初始化的习惯,后面写复杂程序时能省掉大量调试时间。我见过太多人到了指针和数组章节还在被未初始化变量坑,根源就是第三章没打好底子。
另一个容易被忽略的点是变量声明尽量靠近第一次使用的位置。教材习惯把所有变量堆在函数开头,这是C语言遗留风格。C++里更推荐用到再声明,比如:
int a = 0, b = 0; cin >> a >> b; int sum = a + b; cout << "sum = " << sum << endl;sum在需要的时候才声明,代码可读性更好,也减少了“这个变量到底在哪用的”这种困惑。习题答案如果只给一种写法,初学者很容易以为必须把所有变量写在最上面,其实完全没必要。
2.2 输出格式的精确控制:空格、换行与提示信息
第三章习题里有一类题专门练输出格式,比如“按指定格式输出计算结果”。教材例题通常用cout直接输出,但习题里会要求“输出时保留两位小数”或者“每个结果占一行”。这时候就涉及iomanip头文件里的setprecision和fixed:
#include <iostream> #include <iomanip> using namespace std; int main() { double r = 0.0; cin >> r; double area = 3.1415926 * r * r; cout << fixed << setprecision(2); cout << "area = " << area << endl; return 0; }这里有个细节:fixed和setprecision(2)的顺序可以互换,但fixed一旦设置,后续所有浮点输出都会固定小数位。如果后面还有别的输出不想固定,需要用defaultfloat恢复。很多习题答案只给setprecision(2),不加fixed,结果输出的是两位有效数字而不是两位小数,比如3.1而不是3.14。这个坑我在批改作业时见过无数次,学生自己跑程序觉得“差不多”,实际上格式要求根本没满足。
提示:
setprecision(n)单独使用时控制的是有效数字位数,加上fixed之后才控制小数点后的位数。这两个行为完全不同,习题里要求“保留两位小数”时必须用fixed。
2.3 输入提示信息的取舍
教材习题经常要求“输入半径,计算圆面积”,但没说要不要输出提示语。有些答案会写:
cout << "请输入半径:"; cin >> r;这在交互式练习里没问题,但在在线判题系统里就是灾难——判题系统比对输出时,你的提示语会变成多余字符导致答案错误。我的建议是:如果题目没有明确要求输出提示信息,就不要加。如果是在本地练习想看得清楚,可以用注释或者条件编译隔开:
#ifdef LOCAL cout << "请输入半径:"; #endif cin >> r;这样提交到判题系统时去掉LOCAL宏定义就行。这个技巧教材不会讲,但实际刷题时非常实用。
3. 运算符与表达式:优先级和类型转换的连环坑
3.1 整数除法的陷阱与浮点提升
第三章习题里有一道经典题:“输入两个整数,输出它们的商”。很多人直接写:
int a, b; cin >> a >> b; cout << a / b << endl;如果输入5和2,输出是2而不是2.5。原因就是整数除法会截断小数部分。正确的做法是把其中一个操作数转成浮点:
cout << static_cast<double>(a) / b << endl;或者:
cout << a * 1.0 / b << endl;static_cast<double>(a)是C++推荐的显式转换写法,比C风格的(double)a更安全,编译器能帮你检查更多问题。a * 1.0这种写法虽然简洁,但可读性稍差,团队协作时容易被人忽略。我一般建议初学者老老实实用static_cast,把类型转换的意图写清楚。
这里还有一个延伸问题:如果题目要求“输出精确到小数点后两位”,那就要配合fixed和setprecision:
cout << fixed << setprecision(2); cout << static_cast<double>(a) / b << endl;注意fixed和setprecision要放在输出之前,而且一旦设置会影响后续所有浮点输出。如果后面还有整数输出,整数不受影响,但浮点数都会固定小数位。
3.2 自增自减的前后差异
第三章会介绍++和--,习题里经常出现类似:
int i = 5; cout << i++ << endl; cout << ++i << endl;这种题考的就是“先取值再自增”和“先自增再取值”的区别。i++输出5,然后i变成6;++i先把i变成7,再输出7。教材讲得很清楚,但习题里如果混合了其他运算符,优先级就容易乱。比如:
int i = 5; int j = i++ + ++i;这种表达式在不同编译器下结果可能不同,属于未定义行为。习题答案里如果出现这种写法,直接跳过,不要学。实际编程中永远不要在一个表达式里对同一个变量多次自增自减,这是铁律。
我改进后的写法是拆成多行:
int i = 5; int j = i; i++; ++i; j += i;虽然啰嗦,但结果确定,可读性也好。初学者刷题时如果遇到这种题,理解概念就行,别在代码里模仿。
3.3 复合赋值与逗号表达式的可读性权衡
第三章还会讲+=、-=、*=这些复合赋值运算符,以及逗号表达式。习题里可能有:
int a = 1, b = 2; a += b *= 2; cout << a << " " << b << endl;这里b *= 2先执行,b变成4,然后a += 4,a变成5。输出5 4。这种嵌套赋值可读性极差,实际项目中绝对禁止。习题里出现是为了考优先级,但答案里应该注明“仅用于理解运算符优先级,实际编程请拆开写”。
逗号表达式也是类似:
int a = (1, 2, 3);a的值是3,因为逗号表达式取最后一个值。这种写法除了在for循环里偶尔用,其他地方基本是炫技,不建议在习题答案里推广。
4. 输入输出进阶:cin/cout与scanf/printf的取舍
4.1 格式化输出的两种流派
第三章习题有的用cin/cout,有的用scanf/printf。教材两种都介绍,但没明确说什么时候用哪种。我的经验是:纯C++环境优先用cin/cout,需要精细格式控制时printf更方便。比如要求“输出宽度为8,左对齐,保留两位小数”,printf一行搞定:
printf("%-8.2f\n", area);cout要实现同样效果得写:
cout << left << setw(8) << fixed << setprecision(2) << area << endl;而且setw只对下一个输出有效,后面再输出又得重新设置。所以习题里如果格式要求复杂,用printf反而更省事。但要注意printf的格式说明符必须和变量类型严格匹配,double用%f,int用%d,写错了不会报错但输出是乱的。
4.2 输入缓冲区的换行符残留问题
混合使用cin >>和getline时,换行符残留是经典坑。比如:
int n; cin >> n; string s; getline(cin, s);cin >> n读完数字后,换行符还留在缓冲区里,getline直接读到空字符串。解决办法是在getline之前加一句cin.ignore():
cin >> n; cin.ignore(); getline(cin, s);第三章习题里可能不涉及getline,但提前知道这个坑没坏处。如果习题要求“输入一个整数和一个字符串”,用cin >> n >> s就行,>>会自动跳过空白字符,不会遇到这个问题。
4.3 输入失败的处理:习题答案里很少提但很重要
教材习题默认输入都是合法的,但实际写程序时用户可能输入字母。比如:
int a; cin >> a;如果用户输入abc,cin会进入失败状态,a的值不变(C++11之后会被置为0),后续所有输入操作都会失败。处理方法是:
if (!(cin >> a)) { cerr << "输入错误" << endl; return 1; }第三章习题不要求这个,但养成检查输入的习惯,后面做项目时能少很多莫名其妙的bug。我改进的习题答案里会加上简单的输入检查,让代码更健壮。
5. 那些习题答案里不会写的调试与验证技巧
5.1 用边界值测试代替“跑一遍看看”
很多同学做完习题,输入一组数据,输出对了就完事。但边界值才是最容易暴露问题的地方。比如“求两个数的最大值”,输入5和5,输出5,没问题;输入-3和-7,输出-3,也没问题。但如果用int存很大的数,比如2147483647和1,相加就溢出了。第三章习题虽然简单,但养成边界测试的习惯很重要。
我一般会准备几组测试数据:
| 测试类型 | 输入示例 | 预期行为 |
|---|---|---|
| 普通值 | 3, 5 | 正常计算 |
| 相等值 | 4, 4 | 结果正确 |
| 负数值 | -2, -8 | 结果正确 |
| 零值 | 0, 0 | 不崩溃 |
| 极值 | 2147483647, 1 | 检查溢出 |
5.2 用assert做快速验证
C++的<cassert>头文件提供assert宏,可以在调试时检查条件:
#include <cassert> int main() { int a = 3, b = 5; int sum = a + b; assert(sum == 8); return 0; }如果sum不等于8,程序会中止并打印出错位置。习题答案里加几行assert,能帮你快速确认逻辑对不对。发布时定义NDEBUG宏就能禁用所有assert,不影响性能。
5.3 输出中间变量:最笨但最有效的方法
程序结果不对时,最直接的排查方式是在关键步骤后输出中间变量:
int a = 5, b = 2; cout << "a = " << a << ", b = " << b << endl; double result = static_cast<double>(a) / b; cout << "result = " << result << endl;这样能清楚看到每一步的值是否符合预期。第三章习题逻辑简单,但养成这个习惯,后面写循环和函数时调试效率会高很多。
6. 从习题到实战:第三章知识在真实项目里的影子
6.1 一个简单的成绩统计程序
把第三章的知识串起来,可以写一个成绩统计的小程序:输入若干门课的成绩,计算总分和平均分,按指定格式输出。这个程序用到了变量声明、输入输出、算术运算、类型转换、格式控制,几乎覆盖第三章所有知识点。
#include <iostream> #include <iomanip> using namespace std; int main() { double score1 = 0.0, score2 = 0.0, score3 = 0.0; cout << "请输入三门课成绩:"; cin >> score1 >> score2 >> score3; double total = score1 + score2 + score3; double average = total / 3.0; cout << fixed << setprecision(2); cout << "总分:" << total << endl; cout << "平均分:" << average << endl; return 0; }这里total / 3.0用了浮点除法,如果写成total / 3,结果会被截断成整数。fixed和setprecision(2)保证输出两位小数。输入提示信息在本地练习时有用,提交判题系统时记得去掉。
6.2 单位换算工具的核心逻辑
再比如一个“英寸转厘米”的小工具:
#include <iostream> #include <iomanip> using namespace std; int main() { double inches = 0.0; cin >> inches; double centimeters = inches * 2.54; cout << fixed << setprecision(2); cout << centimeters << endl; return 0; }核心就是乘法,但2.54是浮点常量,inches是double,结果自动提升为double。如果inches声明成int,结果就会丢掉小数部分。第三章强调数据类型不是没道理的,类型选错,后面全错。
6.3 习题里没写但工作中常用的输入校验
实际项目中,输入校验是必不可少的一环。第三章习题虽然不要求,但可以提前练手:
#include <iostream> #include <limits> using namespace std; int main() { int age = 0; cout << "请输入年龄:"; while (!(cin >> age) || age < 0 || age > 150) { cout << "输入无效,请重新输入:"; cin.clear(); cin.ignore(numeric_limits<streamsize>::max(), '\n'); } cout << "年龄:" << age << endl; return 0; }cin.clear()清除失败状态,cin.ignore跳过缓冲区里的无效字符。这个模式在处理用户输入时非常常用,第三章学完就可以开始练。
7. 我踩过的坑与给后来者的建议
第三章习题里有一道“输入三角形三边长,求面积”,我当年用海伦公式写:
double p = (a + b + c) / 2; double area = sqrt(p * (p - a) * (p - b) * (p - c));结果输入3 4 5输出6,没问题。但输入1 2 3时,p - c等于0,面积0,程序不报错但结果没意义。后来才知道要判断“两边之和大于第三边”。教材习题没要求判断,但实际写程序时不做校验就是埋雷。
另一个坑是sqrt函数需要包含<cmath>头文件,而且参数是double。如果传int,会隐式转换,但返回值是double。这些细节习题答案里往往一笔带过,自己写的时候容易漏。
我的建议是:第三章习题不要只求“跑通”,每道题都问自己三个问题——输入非法怎么办?边界值对不对?输出格式符合要求吗?把这三个问题回答清楚,基础才算真正打牢。后面学循环、函数、指针时,你会发现第三章的功夫全用得上。