4. 运算符与表达式
运算符这东西,说白了就是C语言里的「动作指令」。你写个 a + b,那个加号就是运算符,告诉计算机把两个数加起来。我刚开始学的时候觉得这有什么好讲的,不就是加减乘除吗?结果写了几年代码才发现,坑全藏在优先级和结合性里。
这一章咱们把C语言里常用的运算符捋一遍。我会把每个运算符的用法、注意事项、以及我踩过的坑都讲清楚。
4.1 算术运算符:最基础的加减乘除
算术运算符有五个:+、-、*、/、%。前四个大家都熟,重点说两个特殊的。
除法 /:整数除法会截断小数部分。比如 5 / 2 结果是 2,不是 2.5。我记得刚入行时有个同事写了个计算平均值的函数,两个整数相除,结果永远不对。查了半天才发现是整数除法的问题。
int a = 5, b = 2;
float c = a / b; // c = 2.0,不是 2.5
float d = (float)a / b; // d = 2.5,正确做法
取模 %:只用于整数,结果是余数。比如 7 % 3 结果是 1。注意:负数取模的结果在不同C标准里可能不一样。我个人习惯尽量避免负数取模,容易出幺蛾子。
% 不能用于浮点数。如果你需要对浮点数取余,用 fmod() 函数。
4.2 关系运算符:比较两个值
关系运算符有六个:>、<、>=、<=、==、!=。结果要么是 1(真),要么是 0(假)。
这里有个经典坑:不要把 == 写成 =。我曾经在调试一个嵌入式程序时,发现某个条件判断永远为真。检查了半天,发现是 if (x = 5) 而不是 if (x == 5)。前者是赋值,表达式结果就是 5,非零即为真。
if (5 == x)。这样万一写成 if (5 = x),编译器会直接报错,因为不能给常量赋值。
4.3 逻辑运算符:组合条件判断
逻辑运算符有三个:&&(与)、||(或)、!(非)。
重点说一下短路求值。对于 &&,如果左边为假,右边就不执行了。对于 ||,如果左边为真,右边也不执行了。
int a = 0;
if (a != 0 && 10 / a > 2) {
// 这里永远不会执行到 10 / a,因为 a != 0 为假
// 所以不会发生除零错误
}
这个特性很有用。我写代码时经常利用短路求值来做「先检查再使用」的操作。比如检查指针不为空后再访问其成员。
f() && g() 能保证 f 先执行,但 f() + g() 不保证哪个先执行。不同编译器结果可能不同。
4.4 赋值运算符:不只是等号
基本赋值就是 =。但C语言提供了复合赋值运算符,比如 +=、-=、*=、/=、%= 等。
a += 5 等价于 a = a + 5。我个人很喜欢用复合赋值,代码更简洁,而且有些编译器对复合赋值的优化更好。
注意:赋值表达式本身也有值。比如 a = b = c = 0 是合法的,从右向左赋值,最终 a、b、c 都变成 0。
if ((x = get_value()) > 0) 虽然合法,但可读性差。我建议分开写:先赋值,再判断。
4.5 位运算符:直接操作二进制位
位运算符是C语言的特色之一,特别适合嵌入式开发和底层编程。有六个:&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)。
我在项目中常用位运算来操作寄存器。比如设置某个位:
// 设置第3位(从0开始计数)
reg |= (1 << 3);
// 清除第3位
reg &= ~(1 << 3);
// 检查第3位是否为1
if (reg & (1 << 3)) {
// 第3位为1
}
左移 << 相当于乘以 2 的幂,右移 >> 相当于除以 2 的幂。但注意:对于有符号整数的右移,行为是「实现定义」的——有的编译器做算术右移(补符号位),有的做逻辑右移(补0)。
4.6 运算符优先级:谁先谁后?
这是最容易出问题的地方。我见过太多因为优先级搞错而导致的bug。下面这张表列出了常用运算符的优先级,从上到下递减。
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1(最高) | () [] -> . |
左到右 |
| 2 | ! ~ ++ -- -(负号) *(解引用) &(取地址) |
右到左 |
| 3 | * / % |
左到右 |
| 4 | + - |
左到右 |
| 5 | << >> |
左到右 |
| 6 | < <= > >= |
左到右 |
| 7 | == != |
左到右 |
| 8 | &(按位与) |
左到右 |
| 9 | ^ |
左到右 |
| 10 | | |
左到右 |
| 11 | && |
左到右 |
| 12 | || |
左到右 |
| 13 | ?:(三目运算符) |
右到左 |
| 14 | = += -= 等赋值运算符 |
右到左 |
| 15(最低) | ,(逗号运算符) |
左到右 |
我的建议是:拿不准就加括号。括号不仅保证正确性,还能让读代码的人一眼看懂你的意图。别为了省几个字符而埋下bug。
// 不加括号,容易误解
if (a & b == c) // 实际是 a & (b == c),因为 == 优先级高于 &
// 加括号,意图清晰
if ((a & b) == c)
4.7 知识体系总览
下面这张图把本章的运算符体系串起来了。你可以把它当作一个快速参考。
4.8 避坑指南
最后总结几个我踩过的坑,你写代码时多留个心眼:
- 优先级搞混:比如
a < b && c < d没问题,但a & b == c就出事了。记住:比较运算符优先级高于位运算符和逻辑运算符。 - 赋值当比较:
if (x = 5)永远为真。用我前面说的「常量放左边」技巧可以避免。 - 整数除法:两个整数相除结果还是整数。需要浮点结果时记得做类型转换。
- 负数取模:不同平台结果可能不同。能不用就不用。
- 有符号数右移:行为是实现定义的。如果你需要逻辑右移,用无符号类型。
运算符这东西,多用多练就熟了。刚开始拿不准就加括号,写多了自然就记住了。下一章咱们聊聊控制流——if、switch、循环这些,到时候你会发现,运算符是控制流的基石。
- 算术运算符注意整数除法和取模的坑
- 关系运算符结果只有0或1,小心 == 和 = 混淆
- 逻辑运算符有短路求值特性,善加利用
- 位运算符适合底层操作,注意有符号右移的问题
- 优先级拿不准就加括号,别装高手
可可精品资料库