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)。

技巧:如果你需要高效地乘以或除以 2 的幂,用移位运算比乘除法快得多。编译器通常也会做这个优化,但显式写出来能让代码意图更清晰。

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 知识体系总览

下面这张图把本章的运算符体系串起来了。你可以把它当作一个快速参考。

C语言运算符体系 运算符 算术运算符 + - * / % 关系运算符 > < >= <= == != 逻辑运算符 && || ! 赋值运算符 = += -= *= /= 位运算符 & | ^ ~ << >> 优先级决定运算顺序,结合性决定同优先级时的计算方向 优先级记忆口诀 括号成员排第一,单目运算第二位 乘除取模第三位,加减第四别忘记 移位关系排五六,等于不等第七位 按位与或异或,逻辑与或最后跟

4.8 避坑指南

最后总结几个我踩过的坑,你写代码时多留个心眼:

  • 优先级搞混:比如 a < b && c < d 没问题,但 a & b == c 就出事了。记住:比较运算符优先级高于位运算符和逻辑运算符。
  • 赋值当比较:if (x = 5) 永远为真。用我前面说的「常量放左边」技巧可以避免。
  • 整数除法:两个整数相除结果还是整数。需要浮点结果时记得做类型转换。
  • 负数取模:不同平台结果可能不同。能不用就不用。
  • 有符号数右移:行为是实现定义的。如果你需要逻辑右移,用无符号类型。

运算符这东西,多用多练就熟了。刚开始拿不准就加括号,写多了自然就记住了。下一章咱们聊聊控制流——if、switch、循环这些,到时候你会发现,运算符是控制流的基石。

核心要点:
  • 算术运算符注意整数除法和取模的坑
  • 关系运算符结果只有0或1,小心 == 和 = 混淆
  • 逻辑运算符有短路求值特性,善加利用
  • 位运算符适合底层操作,注意有符号右移的问题
  • 优先级拿不准就加括号,别装高手

可可精品资料库