这本《C++程序设计教程》第二版是很多高校C++入门课拿来做主教材的,第二章讲常量、变量、基本数据类型和表达式,表面上内容不多,但课后练习题的质量相当高,也相当“咬人”。很多同学跟我反馈说,教材例子能看懂,一做习题就发懵;尤其是表达式求值和程序阅读题,看着简单,怎么写怎么错,一对答案哪哪都不一样。这篇东西想做的,就是把第二章练习题背后的知识点拆开,讲清楚做题的顺序、踩过的坑、以及真正能反复用的排查方法。适合正被这一章折磨的你,也适合学完很久但基础不牢、想回头补一补的人。
1. 第二章到底在练什么
1.1 把知识点从“看懂”变成“算出来”
第二章的核心内容可以压缩成四块:基本数据类型、常量与变量、运算符与表达式、最简单的输入输出。这四块单独拎出来,每一块都不难,难的是它们会在题目里组合出现。比如一道题让你写出某个表达式的值,它可能同时考你类型转换、运算符优先级和结合性,一个环节想错,整个答案就偏了。
我发现一个很普遍的现象:看书的时候,大家都觉得“哦,int是整数,float是小数,这个我知道了”;做练习的时候,题目把char和int混在一个表达式里,或者把3/2乘以一个double,很多人就不知道编译器到底按什么顺序算了。原因在于,看书是理解概念,做题是要模拟编译器的执行过程。这是两种完全不同的能力。第二章练习题训练的就是后者:你要学会“像编译器一样思考”。
所以在做题之前,建议你先别急着翻题目,花半小时在纸上列出几个关键点。第一,这几种基本类型各占多少字节、能表示什么范围;第二,不同类型变量互相赋值时会发生什么;第三,不同类型混合运算时,谁会自动转换成谁;第四,运算符优先级从高到低大概是怎样的;第五,结合方向是左到右还是右到左。这五条如果能不看资料写出来,再去做题,手感完全不一样。
1.2 第二章题型的分类逻辑
这一章的练习题大致可以分成三路:一路是“表达式求值题”,给你一个带着各种运算符的式子,让你写出输出结果;一路是“程序阅读题”,给你一段简短代码,让你分析变量在每个语句之后的值;还有一路是“编程题”,让你从输入、计算到输出,完整写出一个小程序。
大部分初学者会花大量时间在编程题上,觉得那才是“真本事”。但我的经验恰恰相反,前面两路题才是这一章的精髓。表达式求值题能检验你对运算符优先级、结合性和类型转换的敏感度;程序阅读题能检验你是否真的理解变量赋值的语义。编程题反而好办,因为它只要你会最基本的写法就能跑通。我印象里很多考试翻车,不是翻在编程大题上,而是翻在那些“小题”上——表达式算错一个,类型转换认知错误,后面全崩。
2. 做题前必须过关的几个基础细节
2.1 数据类型到底占多少字节,别凭感觉
学C++的人最容易在“类型大小”上栽跟头,尤其是习惯把“int就是4字节”当成铁律的。实际上,C++标准只规定了不同类型的最小范围,并没有规定每种类型必须占多少字节。在常见的64位桌面环境下,int通常是4字节、double是8字节、char是1字节、bool是1字节,但这并不是绝对的。在做题或者写代码的时候,最好用sizeof去确认,而不是凭经验猜。
这里有一个容易忽略的点:字符型虽然单独算一类,但它本质上是整数类型。char可以用来保存字符,也可以参与整数运算。例如'a'的实际数值在ASCII编码下就是97,所以'a' + 1的结果是98,如果你拿char类型接,得到的是ASCII码98对应的字符'b',如果你拿int类型接,得到的就是数字98。这种“字符就是数字”的思维,是这一章很多题目的隐藏考点。
还有一点,bool类型可以赋值给整型变量,true转成1,false转成0。反过来,整型转bool时,0变成false,非0变成true。题目里经常出现类似int x = 5; bool b = x > 0; cout << b;的写法,输出不是“true”一类的字符串,而是整数1。
2.2 运算符优先级表,重要的是“会查”而不是“背”
很多人一想到优先级,就准备把整张表背下来。其实优先级表不用死记,做题的时候会查、会加括号就够了。真正要熟练掌握的是那几条经常出题的分界线。
先说容易被逆袭的:算术运算符优先于关系运算符,关系运算符优先于逻辑运算符。比如a + b > c实际是(a + b) > c,不是a + (b > c)。逻辑与&&的优先级又高于逻辑或||,所以a || b && c是a || (b && c)。
再说一个容易错的地方:==和!=的优先级低于<、>、<=、>=。也就是说,a < b == c会被解释成(a < b) == c,先比较大小,再把比较结果拿来跟 c 做相等判断。这个看起来有点反直觉,但确实是常考的细节。
赋值运算符的优先级非常低,在C++里只高于逗号运算符。所以x = a + b没问题,因为加法先算完才赋值。但如果你写if (x = a),这可不是判断x是否等于a,而是先把a赋给x,再判断x转换成bool之后的结果。很多人会把=和==搞混,第二章的找错题特别喜欢考这个点。
2.3 类型转换:编译器替你做的“决定”
类型转换是这一章最强调、也最让人头疼的内容。在做题前,先建立一条基本认识:当不同类型的数据出现在同一个表达式里时,编译器会按一定规则把它们变成“同一个类型”再计算,这个过程叫隐式类型转换,或者叫“常见算术转换”。
常见的转换方向是:char和short在运算时会先提升为int,然后如果遇到更高等级的浮点类型,int会往float、double方向转。因此int和double混合运算时,结果是double。这一点很好理解,但也导致了一个经典错误:int除以int的结果仍然是int,小数部分会被直接截掉,不会四舍五入。5 / 2的结果是2,不是2.5。想得到2.5,必须让至少一个操作数是浮点类型,比如5 / 2.0。
还有一种转换很容易被忽略:赋值时的转换。当double值赋给int变量时,小数部分被截断;当int值赋给char变量时,超出范围的部分会被“截断”或按平台规则重新解释。题目里经常出现int i = 97; char c = i;这种写法,你不但要能说出c是什么,还要能解释为什么。
3. 三大高频题型的做题套路
3.1 表达式求值题:先加括号,再逐步算
拿到一道表达式求值题,我的建议永远是一句话:先把所有隐式的优先级用括号显式标出来,再一步一步算。这一步叫“括号化”。很多人嫌麻烦,直接在脑子里心算,结果优先级判断错误、结合方向错误,答案自然就废了。
举一个比较典型的例子:求3 + 4 * 2 > 10 - 5 && 5 % 3 == 2的值。先按优先级加括号,过程是这样的:
3 + (4 * 2)先算,因为乘法优先于加法;10 - 5两边都是算术;然后>比&&优先;5 % 3先算;==比&&优先。于是整个表达式括号化之后是:
((3 + (4 * 2)) > (10 - 5)) && ((5 % 3) == 2)
然后逐步算:
4 * 2 = 8,所以3 + 8 = 1110 - 5 = 511 > 5为真,也就是true5 % 3 = 2,2 == 2为真- 最后
true && true的结果是true
如果你直接用cout <<输出这个表达式,会得到1,因为C++里布尔值在输出时默认表现成整数。
这里面还有一个细节:%是取余运算,它只对整数有意义。面对负数时,C++的取余结果符号跟随被除数,比如-5 % 3结果是-2,而不是2。题目如果出了负数取余,很多人会栽在这里。
3.2 程序阅读题:做一张变量跟踪表
程序阅读题给你一段顺序执行的代码,让你写每个变量经过若干语句之后的值。这种题最忌凭感觉从头扫到尾。我自己的习惯是,在草稿纸上画一个简单的表,变量名放在表头,每执行一条语句就更新一行。
例如:
int a = 1; int b = 2; a = b; b = a + b;如果只看代码不看表,很多人会在脑中直接推出a = 2、b = 4。但如果你认真走一遍,就会发现关键在于赋值语句的“副作用顺序”。第一行a = 1、b = 2;执行a = b后,a变成了2,但b没变;执行b = a + b时,a已经是2,所以b变成2 + 2 = 4。画表能把这种“变量值随时间变化”的过程看得清清楚楚。
程序阅读题的另一个考点是自增自减运算。i++是先取i的当前值参与表达式,再让i加1;++i是先让i加1,再用新值参与表达式。这条规则单独背很容易,放在长表达式里就乱了。我的建议是,遇到带自增自减的表达式,先拆成“取值/自增/参与运算”三个步骤来模拟。比如int i = 5; int j = i++ + i;,这里i++先取5参与运算,然后i变成6,接着算5 + 6 = 11。如果换成++i,那就是6 + 6 = 12。
需要提醒一句:在同一个表达式里对一个变量多次自增自减,比如i++ + i++,属于未定义行为,不同编译器的结果可能不一样,做题时不要死抠这种题,更不要自己在代码里写。
3.3 编程题:把“输入-处理-输出”三段拆开写
第二章的编程题一般都很短,无非是输入几个数、做点运算、再输出结果。难不在算法,而在你能不能把程序完整地写出来。很多初学者写的代码有语法错误,或者找不到变量声明位置,或者干脆忘写头文件。
我的建议是,不管题目多简单,都用固定的三段式结构去写。第一段是输入区,负责读入数据;第二段是处理区,负责核心计算;第三段是输出区,负责把结果显示出来。这样哪怕题目再复杂,代码也不会乱。
举个例子,如果要求从键盘输入一个三位整数,然后输出它的个位、十位、百位,处理区可以这样写:
int n; cin >> n; int a = n % 10; int b = n / 10 % 10; int c = n / 100; cout << a << " " << b << " " << c << endl;这里考的就是整数除法和取余运算的组合。用% 10拿个位,用/ 10 % 10拿十位,用/ 100拿百位。这种手法在练习题里反复出现,值得你记下来。写完之后一定要自己代入几个数测一测,比如输入123,输出应该是3、2、1;输入980,应该是0、8、9。很多人写完不测试就交,其实代码可能在某些边界数据上出错。
4. 新手最容易踩的坑和排查方法
4.1 整型除法不是四舍五入
这个坑我已经见无数人踩过了。只要两个操作数都是整数,C++的除法结果就是整数,小数部分直接丢弃。比如7 / 2是3,不是3.5,也不是4。
所以当你写公式或者核对答案时,先问自己一句:这里有没有可能出现小数?如果会,操作数里至少得有一个是浮点类型。一种常见改法是:
double x = 7.0 / 2;另一种是用强制类型转换:
double x = (double)7 / 2;注意(double)(7 / 2)是另一个意思,它先算整数除法得到3,再转成3.0。括号位置不同,结果完全不同。第二章的题目经常用这种细节来“劝退”粗心的人。
4.2 自增运算符在长表达式里的表现
自增自减运算符的优先级很高,尤其是后缀++,它的优先级高于几乎所有双目运算符。但这不代表它在表达式里就“先执行”。后缀++的核心是:先返回原来的值,再完成自增。这个“返回原值”和“完成自增”之间有先后顺序,导致它在表达式里的作用常常被初学者误解。
排查这类问题时,不要直接背“先加后算还是先算后加”,而是把表达式的求值拆成三个时刻:取值时刻、自增时刻、表达式继续运算时刻。每次遇到自增,就追问一句:“现在这个表达式中,它用的是旧值还是新值?”这样一步步推下来,基本不会错。
4.3 赋值表达式的值也能参与运算
C++里赋值是一种运算,a = b这个表达式本身是有值的,值就是赋值完成后左边变量的新值。所以x = a = b是合法的,它先把b赋给a,再把a的新值赋给x,赋值运算符的结合方向是右到左。
这个特性平时写代码用得上,但也是很多“找错题”的题眼。如果看到if (a = b)这种写法,很多人以为它是判断相等,其实它是先赋值再判断。想要判断相等必须写a == b。我建议你在做第二章练习时,凡是遇到=和==混在条件里的题目,都停下来仔细看一遍,这是全章最高频的丢分点。
4.4 教材里的老式头文件和现代编译器
这本教材出版年代比较早,部分例题和习题答案里的头文件写法可能还是iostream.h,或者代码里没有using namespace std;直接写cin、cout。现在的主流编译器,比如Visual Studio和Linux下g++带的工具链,通常要求写成<iostream>,并且加using namespace std;或者使用std::cin、std::cout。
如果你照着教材敲代码,结果报了一堆“找不到cin、cout”的错误,大概率不是思路的问题,而是头文件或命名空间没写对。这一条算是我能送给你的最实用的避坑技巧之一,别被编译器的一堆报错吓到,先看头文件,再看命名空间。
平时做表达式求值题,如果你手边有电脑,完全可以把表达式原样丢到一个临时程序里,用cout输出结果来验证自己的手算过程。但注意,千万不要把编译器输出当作第一计算方式,否则上了考场,手一停,题就废了。正确姿势是:先手算,再上机验证,算错了就回头对照优先级表找原因。我第一次带学生做这一章时,发现这个“手算+上机对照+查表”的流程,比单纯刷题有效得多,因为它逼着你找出错误发生在哪一个优先级上,而不是笼统地归结为“算错了”。
第二章的练习看起来像是在抠基础,其实它练的是你从头到尾严谨推理的能力。把表达式逐层括号化、把变量的变化过程逐行记录、把运算符的优先级和结合性弄扎实,这些习惯一旦养成,后面学函数、学指针、学类的时候会轻松非常多。我个人带过不少初学者,凡是这一章认真做完、并且愿意对着编译器逐题核实的人,后面的课程基本没太吃力。便宜的代价就是在第二章多花几天,贵的效果是往后每一章都能跑得比预期顺畅。希望你也能在这一章把地基打好,而不是急着往后翻。