后台隔三差五就有人拿着《C++程序设计教程(第二版)》来问,第二章课后题到底该怎么刷。这一章讲的是数据类型、运算符和表达式,看着全是概念,实际上是很多人第一次被C++按在地上摩擦的地方。我的看法很直接:这章题目练的不是背语法,而是练一种“看见一段代码,就能在脑子里像编译器一样把它一步步算出来”的底子。C++数据类型、运算符优先级、表达式求值,这三样东西如果在这一章没有理顺,后面的分支、循环、函数、指针全部会像盖在沙子上的楼,越往后越心虚。
这篇文章适合三类人看:正在被课后题折磨的初学者、想回头补基础的项目党、以及要辅导别人做题的人。我会把第二章最常见的题型归纳一遍,挑出有代表性的题目逐步拆解,再把做题时最容易翻车的几个细节单独拎出来讲。答案本身不是重点,每一步为什么这么算,才是这章真正的价值。
1. 第二章到底在讲什么:从语法规则到“程序员的直觉”
很多初学者把第二章当成背定义的一章,我觉得这是最大的误解。这一章其实是在帮你建立两种能力:一是看到类型就知道它在内存里占多大地方、能存什么范围的值;二是看到表达式就能判断运算顺序和结果的类型。这两种能力不是靠背,而是靠练。
1.1 数据类型:每种类型不是标签,而是一块内存
先打个比方。声明一个变量,本质上就是向系统申请一块内存,然后约定这块内存怎么解读。比如int a;是在内存里划出4个字节(在常见平台上),告诉编译器“后面凡是读写这块区域,都按有符号整数来理解”。char c;则是划1个字节,按字符或小整数理解。类型不同,内存大小不同,能表达的范围不同,能参与的运算也不同——题目的陷阱往往就藏在这三个“不同”里。
如果你手头有编译器,建议做一个小实验,把sizeof(char)、sizeof(int)、sizeof(long long)、sizeof(float)、sizeof(double)逐个打印出来看看。第二章的题目可能直接考“某种类型在内存中占多少字节”,但更常考的是间接的:比如计算表达式时中间结果是int还是double,直接决定了最终答案是 0 还是 0.75。所以别把类型的字节数当成死知识,它会影响你对每一行代码结果的判断。
还有一个容易忽略的点:char到底是“字符”还是“整数”?在 C++ 里,字符类型本质上是小范围整数,'A'就是 65,'a'就是 97,ASCII 码表就藏在里面。很多表达式题、字符处理题,本质上都是在考这一点。
1.2 表达式计算顺序:优先级、结合性、求值顺序是三件事
我见过不少初学者把“优先级”“结合性”“求值顺序”混成一锅粥,做题时全凭感觉。实际上这是三个不同层面的东西。
优先级解决的是“谁先跟谁结合”的问题,比如a + b * c中乘法先结合,等价于a + (b * c)。结合性解决的是“同一优先级的运算符从哪边开始算”的问题,比如加减法是左结合,a - b - c从左往右算,等价于(a - b) - c;赋值是右结合,a = b = c等价于a = (b = c)。求值顺序则解决“先算左边还是先算右边”的问题,典型就是&&和||的短路规则——左边能决定整个结果时,右边根本不会执行。
这三个维度合起来,才是你判断一个表达式该怎么算的完整依据。第二章很多练习题就是故意把这个边界弄模糊,让你误以为优先级高的就一定先算。后面我会用真题演示如何把这三层拆开。
2. 课后题的五种常见面孔与通用解题框架
刷题之前先摸清题型,效率会高很多。结合我接触过的情况,第二章的课后题基本跳不出这四类:概念辨析题、表达式求值题、程序输出预测题、小编程题。每一类的解题套路都不一样。
2.1 第一类:概念辨析题
这类题常见问法是“判断下列哪些标识符是合法的”“下列哪个是关键字”“下列字面量写法正确吗”。主打对规则的记忆:标识符必须以字母或下划线开头,后面只能跟字母、数字或下划线,不能是关键字,大小写敏感。题目里容易出现的陷阱是:Int合法但int不合法;_a合法;3abc非法;a#b非法;a_b合法。
这类题没什么捷径,把规则记牢,看到每个选项都在心里过一遍,就能拿满分。我的经验是:不要在“关键字列表”上花太多时间,考试一般只挑常见的几个,比如if、int、while、for,反而更爱考大小写和特殊符号这种一眼容易忽略的点。
2.2 第二类:表达式求值题
这是第二章的绝对主力题型,也是区分“背过书”和“真会了”的分水岭。我总结过一个“三步法”,照着做基本不会错:
第一步,把所有运算符按优先级分组,心里给表达式加上“隐形括号”。比如a + b / c * 2,先拆成a + ((b / c) * 2)。
第二步,按结合性决定同一组里的顺序。比如b / c * 2是乘除同级、左结合,所以先b / c,再乘 2。
第三步,每算完一步,都记录下“当前结果的类型”。这一步最关键,因为整数除法和浮点数除法结果完全不同,而类型转换会让一些结果变成你意想不到的值。
很多学员做错,不是优先级没搞对,而是中间结果的类型搞错了。3 / 4算成 0.75 就是典型的“结果类型”错误,整数除法直接截断,结果是 0。
2.3 第三类:程序输出预测题
这类题会给你一段程序片段,问你cout到底输出什么。本质上是“人肉编译器”,把代码的执行过程在脑子里或者纸上跑一遍。我最推荐的训练方法是:不要口算,拿张纸画一个“变量变化表”,每执行一行,把每个变量的值更新一下。
比如考i++和++i的题,只要在纸上把i的旧值、新值、表达式返回值分开记录,就不会被绕晕。很多人栽在这类题上,是因为图快,在脑子里飞快地算,结果把副作用的发生时机弄错了。
2.4 第四类:小编程题
这种题表面上是“写代码”,实际上是在验证你对运算符和数据类型的综合理解。常见的有:交换两个变量、摄氏温度转华氏温度、判断闰年、求三个数的最大值。
这类题最容易暴露一个毛病:只写出代码,不推演数据流。比如温度转换的公式,如果没注意整数除法会把9 / 5算成 1,写出来的程序在C为 23 时就会输出错误结果。我的建议是:每写一段代码,自己手动代入一组数据,跟着代码走一遍,确认每一步都符合预期,再上机。这个习惯在第二章养成,后面写函数和算法时会受益无穷。
3. 典型课后题逐题拆解:过程比答案值钱
这一节我不按教材页数逐题列,而是挑出最有代表性、最容易错的几道,把完整思考过程写出来。你在看的时候,建议先自己算一遍,再对照我的拆解,这样印象最深。
3.1 标识符合法性判断:规则就那么几条,陷阱就那么几个
题目:判断下列标识符是否合法:_flag、int、3abc、abc#、a_b、A_b。
拆解:_flag以下划线开头,合法。int是关键字,不能做变量名,非法;但如果写成Int,因为 C++ 区分大小写,又变成合法了,这是常考的细节。3abc以数字开头,非法。abc#含#,不是字母、数字或下划线,非法。a_b和A_b都合法,而且a_b和A_b是两个不同的名字,这也是大小写敏感的表现。
这类题有时候还会延伸到一个问题:中文能不能做变量名?C++11 之后一些编译器确实支持 Unicode 标识符,但教材和考试一般还是按 ASCII 的规则来,建议不要在这种边缘问题上浪费精力,规则范围内的题目才是重点。做题时只要记住“字母或下划线开头,后面只能接字母、数字、下划线,避开关键字,大小写敏感”这四句话,基本不会丢分。
3.2 表达式求值:先分优先级,再记中间类型
题目:已知int a = 3, b = 4, c = 5;,求下面表达式的值:
| 表达式 | 计算过程 | 结果 |
|---|---|---|
a + b % c | 3 + (4 % 5) = 3 + 4 | 7 |
a / b * c | (3 / 4) * 5 = 0 * 5 | 0 |
a % b / c | (3 % 4) / 5 = 3 / 5 | 0 |
a + b / c * 2 | 3 + (4 / 5) * 2 = 3 + 0 * 2 | 3 |
double(a + b) / c | double(7) / 5 = 7.0 / 5 | 1.4 |
这张表里,最有价值的是中间两行。a / b * c如果按数学直觉,会觉得是 0.75 乘以 5 得 3.75,但在 C++ 里a / b是整数除法,3 除以 4 直接得 0,再乘 5 还是 0。a + b / c * 2同理,b / c是 0,整个表达式结果就是 3。这种题考的就是“中间结果类型是 int 还是 double”,只要每一步都盯着类型看,就不会算错。
这里还有一个小区别值得注意:double(a + b) / c是先把 7 转成double,再除以 5,得到 1.4;而double(a + b / c)是先算整数除法b / c得 0,再转成double,结果就是 3.0。表达式的写法差一点,意义差很多,这也是第二章喜欢设陷阱的地方。
3.3 类型转换与自增自减:拆开副作用,答案自然就出来了
题目:下面这段程序输出什么?
int i = 5, j; j = i++; cout << i << " " << j << endl; j = ++i; cout << i << " " << j << endl;拆解:第一行j = i++;,i++的值是自增前的 5,赋给j,然后i自增变为 6。所以第一个输出是6 5。第二行j = ++i;,++i的值是自增后的,先让i变成 7,再用 7 给j。第二个输出是7 7。
这个题目把i++的“先用后加”和++i的“先加后用”展示得明明白白。做题时最容易错的地方,是把“表达式的值”和“副作用”混在一起。我的建议是拆成两列写:一列记表达式返回的值,一列记变量在当前步骤之后变成多少。
再说个额外提醒:如果在考试或例题里看到cout << i++ << i;这种写法,要留个心眼。在同一个表达式中,又读i又改i,而且两者之间没有明确的顺序规则,这在某些标准版本下属于未定义行为,编译器不同结果可能不同。出这种题本身就是不严谨的,你按“最容易想到的输出结果”去答就好了,但心里要知道:真实项目里不要这么写。
3.4 短路求值:&& 和 || 的右边,真的可能不执行
题目:
int a = 0, b = 1; bool r = (a++ == 1) && (b++ == 2); cout << r << " " << a << " " << b << endl;拆解:先看(a++ == 1),a当前值是 0,判断0 == 1,结果为false,然后a自增变成 1。因为&&左边已经为false,整个&&的结果已经确定,右边(b++ == 2)根本不会执行,b保持 1。所以r为false,输出是0 1 1。
这里有两个考点叠加在一起:一是&&短路,右边不执行;二是a++的副作用即使发生在比较表达式里,也会在判断后生效。很多初学者会算出b变成 2,就是因为忽略了短路。||同理,左边为true时右边不会执行。平时判断边界条件时,short_circuit是特别实用的能力,比如写if (p != nullptr && *p == 0),当p为空时,右边根本不会访问到空指针,程序就不会崩。
3.5 编程题:交换两个变量,最稳妥的写法是什么
题目:输入两个整数,交换后输出。
最稳的写法:
int a, b, t; cin >> a >> b; t = a; a = b; b = t; cout << a << " " << b << endl;思路:两个杯子互相倒水,没有第三个杯子是倒不匀的。t就是那个临时杯子。这个“临时变量交换法”虽然朴素,但它是排序算法里元素交换的基本功,后面写冒泡排序、选择排序都会用到。所以别嫌简单,把它练成条件反射。
有些人会炫技,写a = a + b; b = a - b; a = a - b;或异或交换a ^= b; b ^= a; a ^= b;。我建议在初学阶段不要用。加减法交换在整数溢出时会产生未定义行为,异或交换可读性差,除了让你觉得自己很厉害之外,没有任何实际收益。工程的审美是清晰优先,这点越早建立越好。
3.6 编程题:温度转换里的整数除法陷阱
题目:输入摄氏温度C,输出华氏温度F,公式F = C * 9 / 5 + 32。
如果C声明为int,这个公式其实有一个非常隐蔽的坑:9 / 5如果先算,结果是 1,不是 1.8。但题目里的式子是先乘后除,C * 9在大多数情况下会先得到一个较大的整数,再除以 5,精度损失较小。比如C = 23,23 * 9 = 207,207 / 5 = 41(整数除法,丢掉 0.4),再加 32 得 73,而真实值应该是73.4,差 0.4。如果你把式子写成F = C / 5 * 9 + 32,23 / 5 = 4,4 * 9 = 36,加 32 得 68,误差就离谱了。
更稳妥的写法是让计算过程中出现浮点数:
double c, f; cin >> c; f = c * 9.0 / 5 + 32; cout << f << endl;把9写成9.0,整个除法变成浮点除法,结果就是 73.4。这也是第二章反复强调的一个点:整数除法直接截断小数,想要精确计算,至少让一个操作数是浮点数。
如果你还想控制输出的格式,可以加fixed << setprecision(2),这需要包含<iomanip>头文件。初学阶段不用管太多,但知道有这么个东西,后面做报表输出时会用上。
3.7 综合应用:闰年判断,把零散知识点串起来
题目:输入年份,判断是否为闰年。闰年规则:能被 400 整除,或者能被 4 整除但不能被 100 整除。
这道题把第二章的逻辑运算符、关系运算符、三目运算符全串起来了,很适合做章节总结。完整写法:
int year; cin >> year; bool leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); cout << (leap ? "Leap Year" : "Common Year") << endl;拆解:year % 4 == 0 && year % 100 != 0是“能被 4 整除但不能被 100 整除”,year % 400 == 0是“能被 400 整除”。两者用||连接,因为满足任一条件就是闰年。注意&&的优先级高于||,所以这里的括号即使不加,逻辑也是对的,但加上括号可读性更好,推荐保持。
三目运算符leap ? "Leap Year" : "Common Year"是第二章的新知识点,等价于一个简单的if-else。它在后面的代码里很常见,属于“一次学会、终身受用”的语法。这道题如果能独立写出来,说明第二章的核心知识点基本过关了。
4. 做题时最容易翻车的四个细节
前面拆题目的时候,其实已经把很多坑点出来了。这里我再把它们集中整理一遍,因为这些不是个别题目的问题,而是整个第二章的高频错误来源。
4.1 误区一:拿优先级当求值顺序
优先级决定“谁先结合”,不决定“谁先求值”。最典型的例子是短路求值:(a = 1) || (b = 2)中,=的优先级低于||,但运行时是先给a赋值,发现左边为真,整个表达式提前结束,b = 2根本不执行。如果你用“优先级高的先算”来解释,就会得出完全相反的结论。
我见过最经典的考题是:
int a = 0, b = 0; bool flag = (a = 1) || (b = 2); cout << a << " " << b << " " << flag << endl;输出是1 0 1。因为a = 1的返回值是 1,转为bool为真,||短路,b = 2没执行。这题就是把“优先级”和“求值顺序”放在一起考,你如果分不清这两个概念,必错。
4.2 误区二:忘记整数除法的截断规则
这是新手最常见的“我以为算对了,但程序输出不对”的原因。7 / 2结果是 3,不是 3.5;-7 / 2的结果是 -3(向零取整)。只要两个操作数都是整数,除法就是整数除法,小数部分直接丢弃,不四舍五入。
想要浮点数结果,至少要有一个操作数是浮点数。7.0 / 2、7 / 2.0、double(7) / 2都能得到 3.5。这个差别太小了,以至于很多人写了多年代码还会时不时踩到。第二章的题就是提前帮你把这个坑填上。
4.3 误区三:低估类型转换的影响
C++ 有一套隐式类型转换规则:在算术表达式里,char和short会先提升成int,然后按“能容纳所有值的最小到最大方向”转换,常见顺序大致是int→unsigned int→long→long long→float→double→long double。两个不同类型的操作数运算时,低级别的一方会自动转换成高级别的一方。
比如char ch = 'A'; cout << ch + 1;输出的是 66,而不是'B'。因为ch在参与算术运算时被提升为int,加 1 结果是整数 66。想得到字符'B',要写char(ch + 1)或static_cast<char>(ch + 1)。这种“字符和整数互相转换”的题,在第二章里出现频率极高,别只看结果,要看类型。
4.4 误区四:做题只看结果,不看过程
这条看起来像鸡汤,其实是最实用的建议。表达式求值题如果只看答案对不对,那你算错的那一步永远不知道自己错在哪。我辅导别人的时候,习惯要求他们把每一个中间步骤都写出来,并且标注中间结果的类型。
你自己刷题时也可以这样:拿张白纸,把表达式拆成一步步的计算,每步写下结果和类型。比如a / b * c,就写“a / b = 0 (int),0 * c = 0 (int)”。写过十几道之后,你对表达式的“手感”会明显好起来,后面看到任何复杂表达式都不慌。这个习惯一旦养成,受益的不只是第二章,后面的数组下标计算、指针运算、结构体偏移量,全都用得上。
5. 学完第二章,你其实已经能做点小东西了
很多人觉得第二章学完只能做卷子上的题,没什么实际产出。其实不然,这一章的工具已经足够你做出几个能跑的小程序了。
5.1 三个可以立刻上手的小练习
第一个是“简易整数计算器”:输入两个整数和一个运算符,输出结果。虽然还不能用switch优雅地处理多个分支,但用多层三目运算或嵌套判断也能实现,过程中会逼你反复练习优先级和类型转换。
第二个是“奇偶与倍数判断器”:输入一个整数,判断它是否为偶数、是否能被 3 或 5 整除。这看起来简单,但可以帮你把%运算符、&&、||的优先级和短路行为摸透。
第三个就是前面提到的“闰年判断器”,它可以改造成“月份天数查询”一类的工具。比如输入年份和月份,输出该月有多少天,二月还要根据闰年规则动态变化。逻辑上比单纯判断闰年多一层,但正好帮你把第二章的知识组合起来用。
5.2 给第三章的预习建议
第二章的知识属于“地基”,第三章通常开始讲流程控制,也就是if、for、while。如果第二章的表达式和类型转换没有练扎实,写循环条件时很容易出错,比如循环里误用了=而不是==,或者在while条件里写出了意料之外的整数除法结果。
我的建议是:别急着往下翻书,先把第二章的课后题做到“不看答案能独立写出过程和结果”的程度。哪怕进度慢一点,这个投入非常划算。我见过太多人第三章循环明明会写,但程序就是输出不对,回头一查,问题全出在第二章的表达式和类型转换上。基础这东西,偷不得懒。
我自己当年刷这章的时候,最不爽的就是有些解析只给一个答案,不给过程。后来我换了个方法,每道题都在编译器里验证一遍,把输出结果和纸上的推导对照,错了就回看是优先级拆错了,还是中间类型记错了。连续练了十几道之后,再看到表达式就习惯性地在脑子里拆层、标注类型。这个习惯帮我后面写指针、写模板时少踩了很多坑。第二章的题就是这样,看似不起眼,却值得你花时间认真对待。