04_运算符表达式与类型转换 运算符、表达式与类型转换一、本篇文章要解决什么问题你已经知道怎么定义变量、怎么输入输出了。但程序光有数据不行还得对数据做运算——加减乘除、比较大小、逻辑判断。这篇文章就帮你搞定三件事C 语言里有哪些运算符算术的、赋值的、比较的、逻辑的分别怎么用自增和自减--放在变量前面和后面到底有什么区别不同类型的数据混在一起运算时会发生什么什么叫自动类型转换和强制类型转换学完这篇你就能写出真正有计算能力的程序而不仅仅是输入什么就输出什么。二、先用一个简单例子理解2.1 超市结账的故事你去超市买东西购物车里放了一盒牛奶8 元× 2 16 元一袋面包12 元× 1 12 元一个苹果3.5 元× 3 10.5 元收银员的计算过程就是总价 8 × 2 12 × 1 3.5 × 3 16 12 10.5 38.5 元这个过程在 C 语言里就是各种运算符的组合乘法*、加法、赋值。更关键的是整数8、2、12、1、3和小数3.5一起参与计算——最终结果自动变成了小数。这就是 C 语言的类型转换在起作用。2.2 运算符优先级和先乘除后加减你小学学过的先乘除后加减在 C 语言里同样适用。但 C 语言有更多运算符它们的优先级规则更复杂。如果搞不清楚优先级最简单的办法就是——加括号。括号里的先算清晰又安全。三、核心知识点讲解3.1 算术运算符C 语言提供了 5 个基本算术运算符运算符含义示例结果加法5 38-减法5 - 32*乘法5 * 315/除法5 / 22注意不是 2.5%取余模5 % 21两个重点重点一整数除法会截断小数部分。5 / 2的结果是2不是2.5。因为两个操作数都是整数C 语言按整数除法执行小数部分直接丢弃。要想得到2.5必须让至少一个操作数是浮点数5.0 / 2或(double)5 / 2。重点二%取余只能用于整数。5 % 2得到 15 除以 2 的余数。5.0 % 2.0会报编译错误。取余运算在判断奇偶性n % 2 0表示偶数、循环截断等场景非常常用。#includestdio.hintmain(void){printf(5 / 2 %d\n,5/2);// 整数除法结果 2printf(5.0 / 2 %.1f\n,5.0/2);// 浮点除法结果 2.5printf(5 %% 2 %d\n,5%2);// 取余结果 1printf(10 %% 3 %d\n,10%3);// 取余结果 1return0;}运行结果5 / 2 2 5.0 / 2 2.5 5 % 2 1 10 % 3 13.2 赋值运算符与复合赋值最基本的赋值运算符是。注意一个等号是赋值两个等号才是判等下一节讲。这也是初学者最容易犯的错误。inta10;// 把 10 赋值给 aaa5;// a 现在是 15C 语言还提供了复合赋值运算符来简化写法写法等价于a 5a a 5a - 5a a - 5a * 5a a * 5a / 5a a / 5a % 5a a % 5#includestdio.hintmain(void){intx10;x5;// x 15x*2;// x 30printf(x %d\n,x);// 输出 30return0;}复合赋值的好处不仅是少打字更重要的是表达意图更清晰x 5直接传达在 x 的基础上增加 5比x x 5更直观。3.3 自增和自减–在编程中把一个变量加 1或减 1是非常高频的操作。C 语言为此设计了专门的运算符。前缀a和后缀a的区别——这是初学者最容易混淆的地方#includestdio.hintmain(void){inta5,b5;intresult1,result2;result1a;// 前缀a 先加 1 变成 6再把 6 赋给 result1result2b;// 后缀先把 b 的值 5 赋给 result2b 再加 1 变成 6printf(a: result1 %d, a %d\n,result1,a);printf(b: result2 %d, b %d\n,result2,b);return0;}运行结果a: result1 6, a 6 b: result2 5, b 6口诀a先加后用——先把自己加 1再把新值参与表达式a先用后加——先把旧值参与表达式再把自己加 1这两个在for循环的i里几乎没区别因为没有赋值给别的变量但在赋值和计算中差别很大。图4-1 前缀与后缀自增对比图一图讲清 a 和 a 的区别这是初学者最容易混淆的概念之一。3.4 关系运算符关系运算符用来比较两个值的大小关系运算结果是1真或0假。运算符含义示例结果大于5 31小于5 30大于等于5 51小于等于5 30等于5 30!不等于5 ! 31特别注意判等和赋值不要搞混// 经典错误——if(a5)// 把 5 赋值给 a然后判断 a 是否为 0。5 不是 0条件永远为真// 正确写法——if(a5)// 判断 a 是否等于 5这个错误编译器通常不会报错只是给一个警告因为a 5本身是合法的表达式。C 语言里赋值表达式的结果就是被赋的那个值这里是 55 不等于 0 所以条件永远成立。这是 C 语言里最有名的陷阱之一。3.5 逻辑运算符与短路求值逻辑运算符用来组合多个条件运算符含义示例结果逻辑与并且(5 3) (2 4)1两边都为真才为真||逻辑或或者(5 3) || (2 4)1有一边为真就为真!逻辑非取反!(5 3)0真变假假变真短路求值——这是 C 语言的一个重要特性// 短路左边为假时右边不再计算inta0;if(a!010/a5)// 左边 a!0 为假右边的 10/a 不会被计算{// 如果右边被执行了10/0 会导致除零错误}// || 短路左边为真时右边不再计算if(a0||10/a5)// 左边 a0 为真右边的 10/a 不会被计算{// 同样避免了除零错误}短路求值不仅是一种优化手段更是一种安全机制——你可以把安全条件放在前面避免后面的表达式在危险情况下被执行。图4-2 短路求值示意图解释短路求值不仅是优化更是安全机制。3.6 运算符优先级C 语言的运算符很多优先级各不相同。下表按从高到低列出常用运算符优先级运算符结合方向1最高()[]左→右2!--正-负(type)右→左3*/%左→右4-左→右5左→右6!左→右7左→右8||左→右9最低-等赋值运算符右→左实用建议与其背这张表不如记住两条规则不确定优先级就加括号。括号最优先而且让代码更可读赋值运算符的优先级很低所以a b c会先算b c再赋值这很自然图4-3 运算符分类图帮助读者建立运算符的全局分类认知。3.7 类型转换不同类型的数据混在一起运算时C 语言会自动做类型转换。3.7.1 自动类型转换隐式转换inta10;doubleb3.5;doubleresultab;// a 先被自动转成 double然后 10.0 3.5 13.5自动转换的规则是向更宽的类型转char → short → int → long → long long → float → double。这样可以避免精度丢失。但要小心——赋值时的自动转换可能丢数据doublepi3.14159;intxpi;// x 3小数部分被截掉不四舍五入printf(%d\n,x);// 输出 33.7.2 强制类型转换如果你明确需要改变类型可以在变量或表达式前面加(目标类型)inta5,b2;doubleresult1a/b;// 结果 2.0——整数除法doubleresult2(double)a/b;// 结果 2.5——强制转换doubleresult3a/(double)b;// 结果 2.5——一样的效果doubleresult4(double)(a/b);// 结果 2.0——注意先算了整数除法#includestdio.hintmain(void){inta5,b2;printf(a / b %d\n,a/b);printf((double)a / b %.1f\n,(double)a/b);printf(a / (double)b %.1f\n,a/(double)b);printf((double)(a / b) %.1f\n,(double)(a/b));// 陷阱return0;}运行结果a / b 2 (double)a / b 2.5 a / (double)b 2.5 (double)(a / b) 2.0注意(double)(a / b)的结果是 2.0不是 2.5。因为括号里的a / b先被计算整数除法得 2然后才把 2 转成 2.0。强制转换只影响紧跟在后面的那个量加了括号就是把整个表达式的结果再转换——但此时整数除法已经发生过了。图4-4 类型转换方向图帮读者理解自动类型转换方向和强制转换的精度风险。四、完整代码示例下面这个程序模拟一个超市收银计算器把本节所有知识点串在一起#define_CRT_SECURE_NO_WARNINGS#includestdio.hintmain(void){// 商品信息 intmilkCount2;doublemilkPrice8.0;intbreadCount1;doublebreadPrice12.0;intappleCount3;doubleapplePrice3.5;// 计算各项金额 doublemilkTotalmilkCount*milkPrice;// int * double → doubledoublebreadTotalbreadCount*breadPrice;doubleappleTotalappleCount*applePrice;// 总价doubletotalmilkTotalbreadTotalappleTotal;// 用户支付 doublepayment;printf( 超市收银 \n);printf(牛奶 x %d %.1f 元\n,milkCount,milkTotal);printf(面包 x %d %.1f 元\n,breadCount,breadTotal);printf(苹果 x %d %.1f 元\n,appleCount,appleTotal);printf(-------------------\n);printf(合计%.1f 元\n,total);printf(\n请输入付款金额);scanf(%lf,payment);// 找零计算 doublechangepayment-total;if(change0){printf(找零%.1f 元\n,change);}else{printf(金额不足还差 %.1f 元\n,-change);}// 演示自增自减分步写法清晰安全 printf(\n 演示 和 -- \n);inta5;intresult1,result2;result1a;// 前缀a 先加 1 变成 6再把 6 赋给 result1printf(前缀result1 a → result1 %d, a %d\n,result1,a);a5;// 重置 a 为 5result2a;// 后缀先把 a 的值 5 赋给 result2a 再加 1 变成 6printf(后缀result2 a → result2 %d, a %d\n,result2,a);// 演示除法和取余 printf(\n 除法与取余 \n);intitems7;intperBox3;printf(%d 件商品每盒装 %d 件\n,items,perBox);printf(需要 %d 盒整除剩余 %d 件取余\n,items/perBox,items%perBox);return0;}五、运行结果 超市收银 牛奶 x 2 16.0 元 面包 x 1 12.0 元 苹果 x 3 10.5 元 ------------------- 合计38.5 元 请输入付款金额50 找零11.5 元 演示 和 -- 前缀result1 a → result1 6, a 6 后缀result2 a → result2 5, a 6 除法与取余 7 件商品每盒装 3 件 需要 2 盒整除剩余 1 件取余六、代码逐行解析第一部分商品金额计算——自动类型转换intmilkCount2;doublemilkPrice8.0;doublemilkTotalmilkCount*milkPrice;milkCount是intmilkPrice是double。两者相乘时milkCount被自动转换为double2 → 2.0然后 2.0 × 8.0 16.0如果两边的类型不匹配C 语言会自动向更宽的类型转换避免精度丢失第二部分找零逻辑——关系运算符和条件判断if(change0)printf(找零%.1f 元\n,change);elseprintf(金额不足还差 %.1f 元\n,-change);change 0用到了关系运算符如果change是负数付款不够我们用-change把它转成正数来输出还差多少第三部分 和 – 分步演示inta5;intresult1,result2;result1a;// 前缀a 先加 1 变成 6再把 6 赋给 result1printf(前缀result1 a → result1 %d, a %d\n,result1,a);a5;// 重置 a 为 5result2a;// 后缀先把 a 的值 5 赋给 result2a 再加 1 变成 6printf(后缀result2 a → result2 %d, a %d\n,result2,a);前缀自增a先加后用。a 先加 15 → 6再把新值 6 赋给 result1后缀自增a先用后加。先把 a 的旧值 5 赋给 result2然后 a 再加 15 → 6这里采用分步写法把自增和赋值拆成独立语句每个printf只输出确定的值不依赖参数的求值顺序。这样不同编译器上的结果一致也是实际开发中推荐的方式第四部分除法与取余的实际应用intitems7,perBox3;printf(需要 %d 盒剩余 %d 件\n,items/perBox,items%perBox);items / perBox7 / 32整数除法得到装满的盒数items % perBox7 % 31取余得到装不满的那盒里的件数这种整数除法 取余的组合在实际编程中非常常见分页计算、进制转换、日期换算等七、初学者常见错误错误1把 写成 // 错误写法if(a5)// 把 5 赋值给 a条件永远为真{printf(a 等于 5\n);}// 正确写法if(a5)// 判断 a 是否等于 5{printf(a 等于 5\n);}预防方法养成习惯把常量写在左边if (5 a)。如果误写成if (5 a)编译器会直接报错因为不能给常量赋值帮你提前发现 bug。错误2整数除法截断导致意外结果// 错误写法doubleavg(908576)/3;// avg 83.0不是 83.666...// 原因右边全是整数先做整数除法得 83再转 double// 正确写法doubleavg(908576)/3.0;// avg 83.666...doubleavg(double)(908576)/3;错误3在复杂表达式中多次使用 或 –// 混乱写法——绝对不要这样写inti5;intresultii;// 未定义行为不同编译器结果不同上面这行代码的问题在于C 语言标准没有规定i和i在同一个表达式中的计算顺序也没有规定副作用变量值何时更新发生的时间点。因此不同编译器会给出不同的结果。核心理念如果一个表达式里对同一个变量用了两次以上的自增/自减运算符并且该变量的值还以其他方式被读取这就是未定义行为。正确做法是拆成多条清晰语句——一条语句只干一件事先改、再算、再输出顺序明确、结果确定。错误4用 % 对浮点数取余doublex5.5;intrx%2;// 编译错误% 只能用于整数// 浮点数取余需要用 fmod 函数math.h 中后续讲错误5逻辑运算符短路被忽略导致空指针解引用// 如果 p 是 NULL 左侧为假右侧的 p-value 不会被执行// 这个写法是安全的if(p!NULLp-value0){...}// 但如果写成if(p-value0p!NULL){...}// 危险先解引用了 NULL八、练习题练习题1数字拆分用户输入一个三位整数如 358程序分别输出它的百位、十位和个位数字。输入358 输出百位3十位5个位8提示用除法和取余。百位 n / 100十位 (n / 10) % 10个位 n % 10。练习题2挑战题计算器程序写一个简单的两个数的计算器让用户输入两个数和一个运算符、-、*、/程序输出计算结果。要求两个操作数用double存储因为可能有除法得到小数运算符用char存储用scanf( %c, op)读取用if判断运算符类别执行对应的运算if 的用法可以参考本篇 3.4 节的简单示例更完整的讲解见第 5 篇如果运算符是/且第二个操作数为 0输出除数不能为零挑战题说明本题需要用到分支结构if/else if属于第 5 篇的内容。如果你还没学到可以先尝试用简单的if完成学完第 5 篇后再回来用switch改写。练习题3位数反转用户输入一个三位整数如 123程序输出反转后的数字即 321。提示个位×100 十位×10 百位。结合取余和除法来提取每位数字。九、本篇总结算术运算符-*/%。整数除法截断小数%只能用于整数自增自减a先加后用a先用后加不要在复杂表达式中对同一变量多次使用关系与逻辑判等不是与、||或、!非。和||会短路求值类型转换不同类型运算时自动向宽类型转换。需要明确转换时用(类型)表达式优先级不确定就加括号比背优先级表更可靠