3. 运算符与表达式:C语言的“动作指令”

各位同学,欢迎来到第三章。如果说变量和数据类型是C语言的“名词”,那运算符和表达式就是“动词”。没有它们,程序就是一潭死水。

这一章内容不少,但别怕。我当年刚学的时候也觉得眼花缭乱,后来发现,其实就几大类。咱们一个一个啃,啃完你就知道,C语言的运算能力有多强。

核心观点:运算符是C语言对数据进行加工的工具。表达式是由运算符和操作数组成的式子,每个表达式都有一个值。

C语言运算符分类体系 运算符 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 逗号运算符 类型转换(隐式/显式) 每个运算符类别都有对应的优先级和结合性规则 表达式 = 运算符 + 操作数,每个表达式都有一个确定的值

3.1 算术运算符:最基础的数学工具

算术运算符就是咱们小学就认识的加减乘除。但在C语言里,有几个细节你得注意。

运算符含义示例结果
+5 + 38
-5 - 32
*5 * 315
/5 / 31(整数除法)
%取模(求余)5 % 32

注意:整数除法会截断小数部分。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 & 30101 & 0011 = 0001 (1)
|按位或5 | 30101 | 0011 = 0111 (7)
^按位异或5 ^ 30101 ^ 0011 = 0110 (6)
~按位取反~5~0101 = 1010 (补码表示)
<<左移5 << 10101 << 1 = 1010 (10)
>>右移5 >> 10101 >> 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 += 5a = a + 5
-=a -= 5a = a - 5
*=a *= 5a = a * 5
/=a /= 5a = a / 5
%=a %= 5a = a % 5
&=a &= 5a = a & 5
|=a |= 5a = a | 5
<<=a <<= 1a = a << 1
>>=a >>= 1a = 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