写运算符和表达式的练习,这事听起来特别基础,但说句实话,我带过不少初学者,也看过很多工作两三年的开发者在代码评审里翻车,问题往往就出在这块。不管是C、C++、Java还是Python,运算符和表达式永远是语言的第一道门槛,而且是一道“看着会、一写就错”的门槛。你觉得自己懂了+ - * /,实际上连个自增运算符都能倒下一片人。这篇文章我就围绕“运算符与表达式题目综合练习”这个主题,把我实际刷题、出题、带人复盘过程中积攒的经验全部摊开来讲,从原理到易错点,再到一套可以直接拿来练手的题目和解析,一次说透。
1. 这类题目到底在练什么,为什么值得花大力气
1.1 几乎所有程序Bug的源头都藏在这里
很多人写代码出问题,第一反应是查逻辑、查框架、查网络,绕了一大圈,最后发现是表达式求值顺序跟想象的不一样。比如int x = 5; int y = x++ + ++x;这种代码,在不同语言里结果可能不一样,甚至在同一门语言的不同版本里都可能存在未定义行为。很多公司面试题特别喜欢出这种题,不是为了炫技,而是因为运算符和表达式直接决定了你对语言底层机制的理解程度。
从实际项目来看,运算符相关的Bug主要集中在几个场景:
- 复合赋值运算符用反了,比如
total *= price + tax到底先加还是先乘 - 位运算误用在逻辑判断上,把
&和&&搞混 - 浮点数直接用
==比较 - 短路求值带来的副作用忽略,比如
if (a != null && a.length() > 5)里顺序写反导致空指针 - 三目运算符嵌套过深,代码审查时没人看得懂
所以练运算符与表达式,本质上是在训练两种能力:第一是对语言规则的精确记忆,第二是对代码执行过程的脑内模拟能力。
1.2 这套练习适合谁,解决什么问题
如果你是刚学编程的学生,这套题能帮你把语法基础打牢,避免后续学数组、函数、指针时被表达式的各种隐含细节反复绊倒。如果你已经在工作中写业务代码,这套题能帮你快速定位那些藏在条件判断和计算逻辑里的隐蔽Bug,跟同事对代码时也能更有底气。如果你正准备面试或者刷算法题,那更是绕不开,很多算法题的第一步就是正确写出表达式,边界条件处理就是运算符基本功的直接体现。
我以前带过一个某高校的实习生A同学,写排序算法时比较器返回值理解和运算顺序搞错,导致排序结果时好时坏,排查了快一天。后来我把运算符和表达式的常见陷阱按类型整理成练习给他做,一周之后他写代码的老毛病明显减少。这种东西不是看一遍文档就能会,必须靠题量堆出敏感度。
2. 算数运算符:最基础但最阴险的五个陷阱
2.1 整数除法不是“除”,是“整除”
看这段代码:
int a = 7 / 2; // a = 3 double b = 7 / 2; // b = 3.0,不是3.5 double c = 7.0 / 2; // c = 3.5很多初学者摔在第一行,甚至摔在第二行。第二行是重灾区:右边两个整数相除,结果先按整数除法算成3,再转换成double赋给b,所以拿到的不是3.5而是3.0。正确的写法是7 / 2.0或者(double)7 / 2。这点在C/C++和Java里都一样,Python3好一点,直接出浮点数,但Python2又不一样,所以日常写代码前先确认语言版本没错。
我自己刷题时习惯把“整型变量参与除法”看成两个步骤:先做整数除法,再做类型转换。这个思路能避免九成以上的类型转换错误。
2.2 取模运算的符号问题,比你想的复杂
取模运算%在正整数下很简单,一旦涉及负数就容易出错。C和C++里,结果符号跟被除数一致:
int a = -7 % 3; // a = -1 int b = 7 % -3; // b = 1Python则不同,结果符号跟除数一致:
a = -7 % 3 # a = 2 b = 7 % -3 # b = -2如果你写代码时没有意识到这个差异,做哈希、分页、循环队列这类场景时,索引可能直接越界或者出现诡异行为。我建议做题的时候先问自己三个问题:被除数是正是负?除数是正是负?语言规定的结果符号规则是什么?
2.3 自增自减:全局最常见翻车点
i++和++i的区别,字面上是“先使用后自增”和“先自增后使用”,但真正做题时经常被复合表达式绕晕。我给你们出个经典题,猜输出:
int i = 1; int x = i++ + ++i;问x是多少?实话告诉你,在C和C++里,这个表达式的结果是未定义行为,编译器可能给出任何结果。因为同一个表达式里对i进行了多次有副作用的修改,并且没有序列点隔开。这种题拿去做,只会误导初学者。真正规范的教学题应该把副作用拆分开:
int i = 1; int x = i++; // x=1, i=2 int y = ++i; // i=3, y=3所以做题要会分辨哪些题是考察规则,哪些题本身就是垃圾题。有个简单的判断标准:一个变量在同一个表达式中被修改了两次以上,且没有隐含的顺序约束,这种题可以直接跳过,别浪费时间去猜。
2.4 溢出与类型提升,边界值踩死人
比如Java里这个常见题:
int x = Integer.MAX_VALUE; x = x + 1; // 变负数,溢出再比如混合运算的类型提升:
long result = 100000 * 100000; // 先按int运算,溢出后再提升为long,结果是负数 long correct = 100000L * 100000; // 正确写法我在做题时的习惯是:混合类型运算,先判断最大操作数的类型,再看结果能否准确放下。特别是涉及字面量时,别嫌麻烦,该加后缀就加后缀。1e9这种double字面量参与取整运算时也要注意。
2.5 浮点数比较,永远别用等号
double a = 0.1 + 0.2; if (a == 0.3) { ... } // 大概率不成立这是老生常谈,但每次练习还是要放进来。正确的比较方式是设定一个极小误差阈值:
double eps = 1e-9; if (fabs(a - 0.3) < eps) { ... }做题时涉及到浮点数相等判断,第一反应就该想到精度问题。有些练习题故意用a = 0.3; b = 0.1 + 0.2;让你判断是否相等,这就是在考察浮点存储原理。
3. 逻辑运算符与短路求值:隐藏的行为决定成败
3.1 短路求值到底是什么
逻辑与&&和逻辑或||有一个重要性质:如果左边的结果已经能决定整个表达式的结果,右边就不执行了。这就是短路求值。
int a = 5; boolean result = (a > 3) || (a++ > 0); // 左边为true,右边a++不执行,a仍然是5这类题看起来简单,但一旦换成函数调用或者赋值表达式,就很容易写错:
if (list != null && list.size() > 0) { // 顺序反了就会空指针 } if (list.size() > 0 && list != null) { // 危险写法 }第二行一旦list为空就抛异常。所以我们在做逻辑判断时,常量、判空条件必须写在左边,这是铁律。
3.2 逻辑运算的返回值陷阱
在C和C++里,&&和||返回的是整数1或0,而在Java里,&&只能用在boolean上,&还能用在整数上做位运算。有些题会故意混搭:
int x = 2; int y = 1; int z = x && y; // C语言中结果是1,不是2Python则不一样,and和or返回的是参与运算的对象本身,而不是布尔值:
a = 0 and 2 # a = 0 b = 1 and 2 # b = 2 c = 0 or 3 # c = 3这个特性常常被用来写“默认值”逻辑,比如name = input_name or "default",但前提是你清楚知道空字符串、0、None这些值会被当作假来处理,否则容易埋雷。
3.3 从题目里识别“副作用陷阱”
我在综合练习里专门设计了这样一道题:已知a=1, b=2, c=3,求(a++ > 0) && (b++ > 0) && (c++ > 0)之后 a、b、c 各自的值。答案不是简单的都加了1,而是要从左到右一步步模拟:先算a++ > 0,a变成2,结果是true;接着算b++ > 0,b变成3,true;接着算c++ > 0,c变成4,true。如果第一项是false,后面两个都不会执行。
这种题的做题方法就是一个词:逐步展开。把每个带副作用的子表达式拆开,标注前后值变化,千万别用“直觉”一口气判断。
4. 位运算:笔试和面试的最爱,也是实用技能
4.1 基础位运算要形成肌肉记忆
位运算总共就那么几种:左移<<、右移>>、无符号右移>>>(仅Java等)、按位与&、按位或|、按位异或^、取反~。
做题时必考的几个点:
- 判断奇偶:
x & 1 == 1比x % 2 == 1更高效 - 交换两个数不用临时变量:
a ^= b; b ^= a; a ^= b; - 清除最低位的1:
x & (x - 1) - 计算二进制中1的个数(Brian Kernighan算法源起处)
- 判断是不是2的幂:
x > 0 && (x & (x - 1)) == 0
比如 2 的幂判断,原理在于2的幂二进制只有一个1,减1之后借位让后面的位全变1,与原数相与必然为0。学了原理之后就不需要背结论。
4.2 右移的高频坑:算术右移还是逻辑右移
C/C++里,对有符号数>>是算术右移,最左边补符号位;对无符号数是逻辑右移,最左边补0。负数右移不等于除以2的向下取整,有些题会拿这个做文章。
int x = -8; int y = x >> 1; // -4,常见实现是算术右移Java里分两种:
int x = -8; int a = x >> 1; // -4,算术右移 int b = x >>> 1; // 2147483644,逻辑右移,最左边补0做题时看到>>>和>>混在一起,第一反应就是检查符号位处理。这个点我在实际面试中见过很多次,经常出现在“不用乘除法实现除以2”的问题里直接把人送走。
4.3 位运算综合题:状态压缩初体验
有一类经典题:用整型变量的若干位来表示多个布尔开关,比如一个字节的高4位表示4种权限,低4位表示另外4种状态。然后要求写代码去设置、清除、反转某一位。
// 按位设置第n位 x |= (1 << n); // 清除第n位 x &= ~(1 << n); // 取反第n位 x ^= (1 << n); // 读取第n位 (x >> n) & 1这类题考的是位掩码构造和“先清零再置位”的思路,非常贴近底层开发的日常。写业务代码的人可能接触不多,但做嵌入式、网络协议、高性能计算一定会用到。
4.4 位运算的优先级是所有人的公认噩梦
按位运算符&、^、|的优先级都低于关系运算符,比如x & 1 == 0会先算1 == 0,结果完全不是你想的那样。正确的写法是(x & 1) == 0。
C语言里这段代码是个经典反面教材:
int x = 5; if (x & 1 == 0) { // 等价于 x & (1==0),即 x & 0,恒为0,条件恒为假 }正确写法是if ((x & 1) == 0)。我在综合练习题里面专门放一道这种题,想让读者建立条件反射:位运算两侧一定要加括号。
5. 赋值运算符与复合赋值运算符:你以为你懂,其实你不懂
5.1 复合赋值自带强转属性
Java中x += 1并不完全等价于x = x + 1,区别在于复合赋值会自动进行一次窄化转换:
short s = 1; s = s + 1; // 编译错误:s+1是int,不能赋给short s += 1; // 编译通过,等效于 s = (short)(s + 1)这题很多人第一次看到都表示震惊。C语言类似但细节不同,所以做题必须先锁定语言。我的建议是:把所有复合赋值运算符的“类型转换行为”单独整理一遍,这比死记运算符优先级划算得多。
5.2 连等运算要注意结合方向
Java里a = b = c = 3;从右往左结合,最终a、b、c都是3。但C语言里连等不是总有合法类型,例如a = b = c = 3;虽然从右往左赋值,但链式赋值要求两侧类型匹配。实际项目里我几乎不写连等赋值,可读性太差,Debug时又没法在中间打断点观察。练习题里考这个,主要是让你理解赋值运算符是右结合这一点。
5.3 复合赋值中表达式只求值一次
有一个隐藏细节:a[i++] += 2等价于a[i++] = a[i++] + 2吗?答案是否定的。复合赋值中左边的变量或数组引用只求值一次,所以a[i++] += 2实际上只是先取出i计算位置,再在该位置上加2,i自增发生在哪一步需要看语言规范。
C标准里这个行为在某些场景下也涉及到未定义问题,像a[i++] += 2这种,C++里既有修改副作用又有读取,非常危险。我刷题看到这类代码,第一反应就是跑一遍看看不同编译器的结果,不建议死记答案。真正规范的教学练习里应该写成:
int idx = i++; a[idx] += 2;这样语义清晰、所有编译器结果一致。
6. 运算符优先级:从根源上理解,而不是背表格
6.1 一张表搞定95%的题目
优先级的表格网上到处都是,背下来难度其实不大,真正难的是在复合表达式里灵活应用。我的建议是记几组关键分层:
第一层:括号、数组下标、函数调用、成员访问 第二层:单目运算符(! ~ ++ -- 正负号) 第三层:算术运算符(* / %高于+ -) 第四层:移位运算符(<< >> >>>) 第五层:关系运算符(< > <= >=) 第六层:相等运算符(== !=) 第七层:位运算符(&高于^高于|) 第八层:逻辑运算符(&&高于||) 第九层:三目运算符 第十层:赋值运算符
有了这个分层,做题时先在脑内给表达式套括号,再逐步求值。
6.2 两个典型的高频坑
先来看第一个:
int x = 2; if (x = 3) { // 赋值而不是比较,条件恒为true }很多语言里赋值表达式的结果就是被赋的值,非零即为真。这种题在C/C++的老题里频繁出现,考察的是你对“赋值也是表达式”的理解。严谨的代码应该写成if (3 == x),避免手滑少写一个等号。
再看第二个:
int a = 2, b = 3; int result = a > b ? a : b + 1; // 先算b+1,再取两者较大值?错了,条件运算符优先级很低实际上这个表达式的解析是a > b ? a : (b + 1),因为条件运算符优先级低于关系运算符。做题时如果没有形成“低优先级运算符让整个表达式分段”的意识,很容易按从左到右的顺序晕掉。我的做题习惯是:找出表达式里优先级最低的运算符,先从那里切一刀,把原式拆成若干个子表达式,再逐步求值。
6.3 括号策略:代码是写给人看的,然后是机器
虽然规则背熟了可以不用括号,但是我在实际项目里强烈建议:只要混合用了两种以上运算符,就加括号。比如a & b == 0,就算你知道它的解析是a & (b == 0),阅读你代码的人不一定知道。练习题里那些“不加括号算结果”的题只是训练,真实工程里代码可读性优先。
7. 综合练习题库:直接上手刷起来
7.1 基础入门的10道题(先说结论再讲思路)
我设计了以下题组,覆盖算数、逻辑、位运算和优先级:
// 第一组:算数基础 1. int a = 7 / 2; a 的值? 2. int b = 7.0 / 2; b 的值? 3. int c = -7 % 3; c 的值? (C语言) 4. double d = 0.1 + 0.2; d == 0.3 是否成立? 5. int i = 2; i += i++ + ++i; i 是?第一题答案是3,第二题如果直接赋给int会编译报错,Java里直接编译不过,C里会有警告但结果还是3。第三题C里是-1,Python里是2。第四题不成立,浮点精度问题。第五题是典型垃圾题,存在未定义行为,不给出答案才是正确的答案。做题时碰到这种第五题,要能识别出来,比计算出某个具体值更重要。
7.2 中级挑战的10道题(结合真项目需求)
进入中级,我更倾向于包装成“小情景”,而不是干巴巴的算术题。比如这道题就来自一个分页功能:
int page = 0; int totalPages = ...; boolean hasNext = page++ < totalPages; // 问题是:page和hasNext分别变成多少?答案是:先比较page(0)是否小于totalPages,再把page变成1,hasNext取决于比较结果。这里若把page++ < totalPages改成++page < totalPages,行为就完全不同了。这类题在实际业务代码中出现频率奇高,你能在几秒内说出两个写法的区别,基本就能秒杀绝大多数面试场景。
再放一道经典的位运算升级题:
int x = 0b101100; int count = 0; while (x != 0) { x = x & (x - 1); count++; } // count等于几?先把0b101100转成十进制是44,二进制含3个1,循环每次清除最右边的1,所以count是3。这类题考察的就是x & (x - 1)到底干了什么,属于刷题高频考点。
7.3 进阶项目的10道题(一题多解才有意思)
进阶题我不追求偏难怪,而是追求一题多解和边界覆盖。比如这道:不用条件语句求x的绝对值:
int abs = (x ^ (x >> 31)) - (x >> 31);把x >> 31当成一个掩码,若x为正则右移31位得0,若为负则得-1(全1),通过异或和减法算出绝对值。能理解这行代码的人,对算术右移、异或、补码的理解就到位了。
再给一个经典题:判断某年是否是闰年,要求只能用一条表达式:
boolean leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;这题不是运算符技巧本身有多难,而是考察你把数学条件和逻辑表达式结合的能力,顺序、括号都不能错。初学者常常少写一个括号导致算错,题目设计得很值得练。
7.4 每道题的评分标准建议
做练习时建议给自己打分:
- 能一次写对,并且能解释为什么这样算,满分
- 能写对但说不出理由,算半对,说明还要回头啃基础
- 写错了,别急着看答案,先用断点调试或者手写步骤推导,再看解析
我强烈建议把“写出解题过程”作为训练核心,而不是只看最终结果。你每推导一次,就是在模拟一次CPU执行流程,这对后续学指针、递归、并发都有巨大帮助。
8. 刷题的高效方法:我踩过坑也总结出规律
8.1 分类整理远比盲目刷题重要
我见过不少新人上来就啃几百道“运算符题”,结果题型一变就懵。更合理的做法是按本节前面拆的类别——算数陷阱、短路求值、位运算、优先级——每种类型集中刷10到20道,直到一个类型做到90%以上正确率再换下一个。这样每类题型都形成了独立的知识组块。
我自己刷题时,每做完一道题会顺手贴到自己的错题本里,分成两类:一类是“规则不知道”,这类错题说明知识点有漏洞,需要回到文档里补背景;一类是“步骤跳步”,这类错题说明习惯不好,做题太依赖直觉。两类错误的复习频率不太一样,前者需要每周回顾,后者每天提醒自己放慢速度就好。
8.2 用“口述复现法”检验掌握程度
关于运算符表达式题,有一个我屡试不爽的检验方法:把题目遮住,向身边人或者镜子口述这道题的完整推演过程,包括每一步的运算符结合方向和值的变化。如果你能流畅讲出来,说明真正掌握了;如果讲到一半去抓笔算,说明还没形成内化记忆。
我当年在组织某模拟项目X的代码评审时,要求参与者轮流讲解自己写的表达式为什么这样写,结果半个小时的会议活活开成了两个小时。过程中暴露了大量“能跑但说不清”的代码,这在团队协作里是隐患,因为后续没人敢重构它。
8.3 实战项目里怎么继续保持敏感度
刷阶段题只是建立基础,真正让运算符与表达式的感觉上升一层,还得靠写实际项目。我建议在日常开发中刻意做三件事:
- 每个条件表达式的运算符两侧强制加括号,先不用管“多余不多余”
- 每次用完取模、位运算和复合赋值,在代码旁边写一行注释,说明这里在干什么数学变换
- 定期回看一个礼拜前自己写的表达式代码,能一眼看懂就说明可读性合格,需要仔细想才看懂说明当初的表达方式就有问题
这样持续一个月,你对运算符的敏感度会明显超过很多人。很多资深开发者所谓的“代码直觉”,其实就是大量规则内化之后的表达习惯。
最后再分享一个小技巧:做运算符与表达式练习时,建议自己在本地写一个很小的测试工程,把每道题用printf输出结果验证,然后用眼睛对着输出看一遍,再跟推导过程对照。有时候推导和实际输出不一致,不是规则记错了,而是没考虑编译器的具体实现,这种时候也要值得高兴,因为你正好抓到了语言规范和编译器实现之间的灰色地带。这个灰色地带,恰恰才是区分你和普通开发者的分水岭。