3. 运算符与表达式:C语言的“动作指令”
各位同学,欢迎来到第三章。如果说变量和数据类型是C语言的“名词”,那运算符和表达式就是“动词”。没有它们,程序就是一潭死水。
这一章内容不少,但别怕。我当年刚学的时候也觉得眼花缭乱,后来发现,其实就几大类。咱们一个一个啃,啃完你就知道,C语言的运算能力有多强。
核心观点:运算符是C语言对数据进行加工的工具。表达式是由运算符和操作数组成的式子,每个表达式都有一个值。
3.1 算术运算符:最基础的数学工具
算术运算符就是咱们小学就认识的加减乘除。但在C语言里,有几个细节你得注意。
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
| + | 加 | 5 + 3 | 8 |
| - | 减 | 5 - 3 | 2 |
| * | 乘 | 5 * 3 | 15 |
| / | 除 | 5 / 3 | 1(整数除法) |
| % | 取模(求余) | 5 % 3 | 2 |
注意:整数除法会截断小数部分。5/3 结果是1,不是1.666。我见过不少新手在这里栽跟头,写了个除法结果发现小数没了。
取模运算符 % 只能用于整数。浮点数不能用。这个限制很多人一开始不知道,编译报错才反应过来。
int a = 10, b = 3;
int c = a / b; // c = 3
int d = a % b; // d = 1
float x = 10.0, y = 3.0;
float z = x / y; // z = 3.333333
3.2 关系运算符:比较大小,判断真假
关系运算符用来比较两个值。结果只有两种:真(1)或假(0)。
| 运算符 | 含义 | 示例 |
|---|---|---|
| > | 大于 | 5 > 3 → 1 |
| < | 小于 | 5 < 3 → 0 |
| >= | 大于等于 | 5 >= 5 → 1 |
| <= | 小于等于 | 5 <= 3 → 0 |
| == | 等于 | 5 == 3 → 0 |
| != | 不等于 | 5 != 3 → 1 |
避坑指南:我曾经在代码里把 == 写成了 =,结果条件判断永远为真,排查了半天。记住:== 是比较,= 是赋值。这是C语言新手最容易犯的错误,没有之一。
3.3 逻辑运算符:组合条件判断
逻辑运算符用来连接多个条件。C语言用逻辑值0表示假,非0表示真。
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
| && | 逻辑与(AND) | (5>3) && (5<10) | 1 |
| || | 逻辑或(OR) | (5>3) || (5>10) | 1 |
| ! | 逻辑非(NOT) | !(5>3) | 0 |
这里有个重要特性叫“短路求值”。什么意思呢?
对于 &&,如果左边为假,右边就不执行了。对于 ||,如果左边为真,右边也不执行了。这不是优化,这是语言标准规定的行为。
int a = 0, b = 5;
if (a != 0 && b / a > 1) {
// 这行不会执行,因为 a != 0 为假,右边 b/a 根本没算
// 避免了除零错误
}
实战经验:我在做嵌入式驱动开发时,经常利用短路求值来先检查指针是否为空,再访问指针指向的内容。这样既安全又简洁。
3.4 位运算符:直接操作二进制位
位运算符直接操作整数的二进制位。这在嵌入式开发中极其常用——控制寄存器、设置标志位、快速乘除2的幂。
| 运算符 | 含义 | 示例 | 二进制说明 |
|---|---|---|---|
| & | 按位与 | 5 & 3 | 0101 & 0011 = 0001 (1) |
| | | 按位或 | 5 | 3 | 0101 | 0011 = 0111 (7) |
| ^ | 按位异或 | 5 ^ 3 | 0101 ^ 0011 = 0110 (6) |
| ~ | 按位取反 | ~5 | ~0101 = 1010 (补码表示) |
| << | 左移 | 5 << 1 | 0101 << 1 = 1010 (10) |
| >> | 右移 | 5 >> 1 | 0101 >> 1 = 0010 (2) |
// 位运算经典用法:用宏来设置和清除某一位
#define SET_BIT(x, n) ((x) |= (1 << (n)))
#define CLR_BIT(x, n) ((x) &= ~(1 << (n)))
#define GET_BIT(x, n) (((x) >> (n)) & 1)
unsigned char reg = 0x00;
SET_BIT(reg, 3); // reg = 0x08 (二进制 00001000)
CLR_BIT(reg, 3); // reg = 0x00
个人习惯:我写位操作时,一定会加括号。因为位运算符的优先级比加减法低,很容易出问题。比如 x << 2 + 1 实际是 x << (2+1),不是 (x<<2)+1。
3.5 赋值运算符:给变量“喂”值
最基本的赋值就是 =。但C语言还提供了一组复合赋值运算符,让代码更简洁。
| 运算符 | 示例 | 等价于 |
|---|---|---|
| += | a += 5 | a = a + 5 |
| -= | a -= 5 | a = a - 5 |
| *= | a *= 5 | a = a * 5 |
| /= | a /= 5 | a = a / 5 |
| %= | a %= 5 | a = a % 5 |
| &= | a &= 5 | a = a & 5 |
| |= | a |= 5 | a = a | 5 |
| <<= | a <<= 1 | a = a << 1 |
| >>= | a >>= 1 | a = a >> 1 |
赋值表达式本身也有值,就是赋完后的结果。所以可以连续赋值:a = b = c = 5; 从右往左依次赋值。
3.6 逗号运算符:把多个表达式串起来
逗号运算符用逗号连接多个表达式,整个表达式的值等于最后一个表达式的值。很多人觉得它没用,其实在for循环里特别方便。
int a = (1, 2, 3, 4, 5); // a = 5,逗号表达式取最后一个值
// for循环中同时初始化多个变量
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}
注意:逗号运算符的优先级是所有运算符中最低的。如果你在别的地方用逗号,记得加括号。否则编译器可能理解成函数参数列表里的逗号。
3.7 类型转换:隐式与显式
C语言中,不同类型的数据在一起运算时,会发生类型转换。转换分两种:隐式(自动)和显式(强制)。
3.7.1 隐式类型转换
编译器自动完成,不需要你写任何代码。规则是:向精度更高的类型转换。比如 int 和 float 运算,int 自动转成 float。
int i = 5;
float f = 3.14;
float result = i + f; // i 先转成 float,再相加,result = 8.14
还有一个常见的隐式转换发生在赋值时。把高精度类型赋给低精度类型,会截断或丢失精度。编译器通常会给出警告。
double d = 3.14159;
int i = d; // i = 3,小数部分被截断,编译器会警告
3.7.2 显式类型转换(强制类型转换)
用 (类型名) 表达式 的语法,明确告诉编译器你要转成什么类型。
int a = 5, b = 2;
float c = (float)a / b; // c = 2.5,如果不强制转换,结果是 2.0
实战经验:我在做传感器数据采集时,经常需要把ADC读到的整数值转换成实际的电压值。这时候强制类型转换就派上用场了。记住:转换只是临时改变值的解释方式,不会改变变量本身的类型。
3.8 运算符优先级与结合性
最后,咱们聊聊优先级。说白了就是:当一个表达式里有多个运算符时,谁先算?
我不建议你死记硬背优先级表。我的做法是:拿不准的地方就加括号。括号的优先级最高,而且让代码意图一目了然。
// 不加括号,你能一眼看出结果吗?
int x = 2 + 3 * 4 >> 1 & 0x0F;
// 加括号,清清楚楚
int y = ((2 + (3 * 4)) >> 1) & 0x0F;
我的建议:写代码是给人看的,不是写给编译器看的。多用括号,少炫技。你想想看,三个月后你自己回来看代码,加括号的版本肯定比不加的容易理解。
好了,运算符和表达式的内容就讲到这里。这一章的知识点比较多,但每个都很实用。尤其是位运算符和类型转换,在嵌入式开发中几乎天天用。多写代码,多练习,慢慢就熟练了。
公众号:蓝海资料掘金营,微信deep3321