运算符与表达式:C语言的“计算引擎”
各位同学,欢迎来到第一章。今天咱们聊聊C语言里最基础、也最核心的东西——运算符和表达式。
你可能觉得这玩意儿太简单了,不就是加减乘除吗?嗯,我当年刚学的时候也这么想。直到有一次在项目里,因为一个运算符优先级的问题,排查了整整两天bug……从那以后,我再也不敢小看这些“小符号”了。
说白了,运算符就是C语言里的“动词”,表达式就是“句子”。没有它们,程序就是一潭死水。咱们今天就把这潭水搅活。
一、算术运算符:最熟悉的陌生人
加减乘除,小学就会。但在C语言里,有几个坑你得小心。
先看个例子:
int a = 10, b = 3;
int c = a / b; // c = 3,不是3.333
int d = a % b; // d = 1,取余数
看到了吗?两个整数相除,结果还是整数——直接截断小数部分。我当年做嵌入式电机控制时,就因为这个特性,算出来的转速总是偏小。后来改成浮点数才解决。
关键点:
/对整数做除法,结果截断小数%只能用于整数,不能用于浮点数- 自增
++和自减--有前缀和后缀之分
说到自增自减,这里有个经典问题:
int x = 5;
int y = x++; // y = 5, x = 6
int z = ++x; // z = 7, x = 7
前缀先加再用,后缀先用再加。嗯,就这么简单。但嵌套在复杂表达式里就容易乱。我的建议是:别把 ++ 和 -- 塞进复杂表达式里,单独一行写,清晰明了。
二、关系运算符:真假判断
关系运算符用来比较两个值,结果要么是1(真),要么是0(假)。
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
| > | 大于 | 5 > 3 | 1 |
| < | 小于 | 5 < 3 | 0 |
| == | 等于 | 5 == 3 | 0 |
| != | 不等于 | 5 != 3 | 1 |
| >= | 大于等于 | 5 >= 3 | 1 |
| <= | 小于等于 | 5 <= 3 | 0 |
⚠️ 我曾经踩过的坑:把 == 写成 =。在 if 条件里,if (x = 5) 不会报错,它会先把5赋给x,然后判断x是否为真(非0即为真)。结果就是条件永远成立!
我的习惯:把常量写在左边,比如 if (5 == x),这样万一写成 if (5 = x),编译器会直接报错。
三、逻辑运算符:组合判断
逻辑运算符用来组合多个条件:
&&逻辑与:两边都为真才真||逻辑或:一边为真即为真!逻辑非:取反
这里有个重要特性——短路求值。你想想看:
int a = 0, b = 10;
if (a != 0 && b / a > 5) {
// 这段代码永远不会执行
}
为什么?因为 a != 0 已经是假了,&& 后面的表达式根本不会执行。这就避免了除以零的错误。反过来,|| 也是,左边为真,右边就不看了。
我在做传感器数据采集时,经常用这个特性:
if (ptr != NULL && ptr->value > 100) {
// 先检查指针是否有效,再访问数据
}
这样写既安全又高效。
四、位运算符:嵌入式工程师的利器
位运算直接操作二进制位,在嵌入式开发里用得特别多。说白了,就是跟寄存器打交道。
| 运算符 | 含义 | 示例 |
|---|---|---|
| & | 按位与 | 0x0F & 0xF0 = 0x00 |
| | | 按位或 | 0x0F | 0xF0 = 0xFF |
| ^ | 按位异或 | 0x0F ^ 0xF0 = 0xFF |
| ~ | 按位取反 | ~0x0F = 0xF0(假设8位) |
| << | 左移 | 0x01 << 3 = 0x08 |
| >> | 右移 | 0x08 >> 3 = 0x01 |
举个例子,你想设置某个寄存器的第3位为1:
REG |= (1 << 3); // 置位第3位
REG &= ~(1 << 3); // 清零第3位
这种写法在单片机开发里随处可见。我调试I2C驱动时,就靠位运算来操作控制寄存器,一个位一个位地配置。
小技巧:用 ^ 异或可以不用临时变量交换两个数:
a = a ^ b;
b = a ^ b;
a = a ^ b;
不过说实话,可读性差了点。我一般只在内存极度受限的场合才这么干。
五、赋值运算符:不只是等号
最基本的赋值就是 =。但C语言还提供了复合赋值运算符,把运算和赋值合二为一:
int x = 10;
x += 5; // 等价于 x = x + 5
x *= 2; // 等价于 x = x * 2
x &= 0x0F; // 等价于 x = x & 0x0F
我个人很喜欢这种写法,简洁,而且编译器通常能生成更高效的代码。但要注意,别写得太花哨,比如 x += y *= z 这种——嗯,虽然语法上合法,但读起来费劲。
六、运算符优先级:最容易翻车的地方
优先级决定了表达式里谁先算。我直接给你一张表,按优先级从高到低排列:
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1(最高) | () [] -> . | 左到右 |
| 2 | ! ~ ++ -- + - * & (类型) | 右到左 |
| 3 | * / % | 左到右 |
| 4 | + - | 左到右 |
| 5 | << >> | 左到右 |
| 6 | < <= > >= | 左到右 |
| 7 | == != | 左到右 |
| 8 | & | 左到右 |
| 9 | ^ | 左到右 |
| 10 | | | 左到右 |
| 11 | && | 左到右 |
| 12 | || | 左到右 |
| 13 | ?: | 右到左 |
| 14(最低) | = += -= 等 | 右到左 |
你可能会问:这么多优先级,怎么记得住?
我的回答是:别死记,多用括号。括号的优先级最高,而且能让代码意图一目了然。
举个例子:
// 这样写,鬼知道先算哪个
int result = a + b * c << 2 & 0xFF;
// 加个括号,清清楚楚
int result = ( (a + (b * c)) << 2 ) & 0xFF;
我在代码评审时,看到那种靠优先级炫技的写法,一般都会要求加括号。代码是写给人看的,不是写给编译器看的。
核心原则:
- 不确定优先级?加括号!
- 觉得别人可能看不懂?加括号!
- 宁可多写几个括号,也别让读代码的人猜
好了,运算符这块儿就聊到这儿。内容不少,但都是基本功。你写代码的时候多留意这些细节,慢慢就会形成肌肉记忆。下次咱们接着聊控制流程——if、switch这些,到时候这些运算符就派上大用场了。