运算符与表达式(Operators & Expressions)

"运算符是 C 语言的动词,决定了数据之间如何交互。" —— 我发现

开篇故事

修理工的工具箱里有扳手、螺丝刀、钳子、锤子。扳手拧螺母,螺丝刀拧螺丝,锤子敲钉子。每种工具负责一种操作,但它们的区别不在于外观,而在于「对物体做了什么」。选错工具,拧螺母用锤子只会把活搞砸。

C 语言的运算符就是这样的工具箱。+ 是加法扳手,/ 是除号螺丝刀,== 是比较钳子,&& 是逻辑锤子。它们看似简单,但优先级、结合性和短路求值这些特性就像工具的受力方向——用法不对,结果就可能出乎意料。比如把赋值 = 写成比较 ==,就像把扳手当锤子敲下去,程序不会报错,但干的是另一件事。

运算符是 C 语言的动词,它们决定了数据之间如何交互。理解了运算符,你才能真正让数据为你工作。

本章适合谁

  • 学过基本数据类型和变量的人
  • 写过程序,但对 ++ii++ 有什么区别说不清楚的人
  • 在条件判断中踩过运算符坑的朋友

你会学到什么

  • 算术运算符(Arithmetic):+-*/%
  • 关系运算符(Relational):==!=<><=>=
  • 逻辑运算符(Logical):&&||!
  • 位运算符(Bitwise):&|^~<<>>
  • 赋值运算符(Assignment):=, +=, -=, *=, /=, %=
  • 运算符优先级与结合性
  • 短路求值(Short-circuit Evaluation)

前置要求

  • 了解整数类型(intlong)和浮点类型(floatdouble
  • 了解二进制基础(对位运算章节有帮助)
  • 会写简单的 if 判断

第一个例子:基础算术运算

#include <stdio.h>

int main(void) {
    int a = 17, b = 5;

    printf("a = %d, b = %d\n", a, b);
    printf("a + b = %d\n", a + b);   // 加法:  22
    printf("a - b = %d\n", a - b);   // 减法:  12
    printf("a * b = %d\n", a * b);   // 乘法:  85
    printf("a / b = %d\n", a / b);   // 除法:  3  (整数除法!)
    printf("a %% b = %d\n", a % b);   // 取余:  2

    return 0;
}

运行结果:

a = 17, b = 5
a + b = 22
a - b = 12
a * b = 85
a / b = 3
a % b = 2

关键发现a / b = 3 不是 3.4!因为这是整数除法,小数部分被截断了。

原理解析

1. 算术运算符(Arithmetic Operators)

运算符名称示例结果
+加法3 + 58
-减法10 - 46
*乘法3 * 412
/除法7 / 23(整数除法)
%取余(取模)7 % 21

注意/% 的行为取决于操作数类型。两个整数之间使用 / 是整数除法,只要有一个是浮点数就是浮点除法。

int a = 7 / 2;        // a = 3
double b = 7.0 / 2;   // b = 3.5

2. 关系运算符(Relational Operators)

返回 1(真)或 0(假):

运算符含义示例结果
==等于5 == 51
!=不等于5 != 31
<小于3 < 51
>大于5 > 100
<=小于等于5 <= 51
>=大于等于3 >= 70

3. 逻辑运算符(Logical Operators)

运算符名称示例说明
&&逻辑与1 && 00:两个都为真才为真
``逻辑或
!逻辑非!10:取反

短路求值(Short-circuit evaluation)

int x = 0;
if (x != 0 && 10 / x > 2) {
    // 永远不会执行到这里——不会除零
    // 因为 x != 0 为假,&& 左边的条件失败后,右边不再求值
}

4. 位运算符(Bitwise Operators)

对二进制位进行操作:

运算符名称示例说明
&按位与5 & 30101 & 0011 = 00011
``按位或5 \| 3
^按位异或5 ^ 30101 ^ 0011 = 01106
~按位取反~5~00000101 = 11111010
<<左移5 << 10101 → 101010
>>右移5 >> 10101 → 00102

常见用法

// 判断奇偶(比 % 2 更快)
int is_odd = (n & 1);

// 乘以 2 的幂(左移)
int x = 5;
int y = x << 3;  // y = 5 * 8 = 40

// 设置/清除特定位
unsigned char flags = 0x00;
flags |= (1 << 3);   // 设置第 3 位: 0x08
flags &= ~(1 << 3);  // 清除第 3 位: 0x00

5. 赋值运算符(Assignment Operators)

运算符示例等价于
=a = 5a = 5
+=a += 3a = a + 3
-=a -= 3a = a - 3
*=a *= 3a = a * 3
/=a /= 3a = a / 3
%=a %= 3a = a % 3
&=a &= 3a = a & 3
\|=a \|= 3a = a \| 3
^=a ^= 3a = a ^ 3
<<=a <<= 3a = a << 3
>>=a >>= 3a = a >> 3

6. 运算符优先级(Precedence)

部分优先级(从高到低):

优先级运算符方向
最高() [] . ->左→右
! ~ ++ -- (type) -(负号)右→左
* / %左→右
+ -左→右
<< >>左→右
< <= > >=左→右
== !=左→右
&左→右
^左→右
``
&&左→右
\|\|左→右
最低= += -=右→左

黄金法则:如果你不确定优先级,加括号a + b * ca + (b * c) 更清晰。

常见错误

❌ 错误 1:= vs ==

int x = 5;
if (x = 10) {  // ❌ 这是赋值,不是比较!
    printf("x is 10\n");  // 总是会打印
}

修正:使用 == 进行比较。

int x = 5;
if (x == 10) {  // ✅ 正确比较
    printf("x is 10\n");  // 不会打印
}

防御性编程技巧:把常量写在左边——if (10 == x)。如果误写成 = 编译器会报错,因为不能给字面量赋值。

❌ 错误 2:整数除法陷阱

double result;
result = 1 / 2;      // ❌ result = 0.0(整数除法先算,再转 double)
result = 1.0 / 2;    // ✅ result = 0.5(浮点除法)
result = (double)1 / 2;  // ✅ 也正确,强制类型转换

❌ 错误 3:&&& 混淆

int a = 5, b = 3;
if (a & b) {  // ❌ 这是按位与: 5 & 3 = 1(真),但不是逻辑意图
    printf("true\n");
}

if (a && b) {  // ✅ 逻辑与: 两个非零值都为真
    printf("true\n");
}

两者都可能返回"真",但在复杂条件下行为不同:

  • &&短路(左边为假不计算右边)
  • & 不会短路(两边始终计算)

动手练习

🟢 练习 1:判断年份是否为闰年

// 闰年规则:
// 能被 4 整除但不能被 100 整除,或者能被 400 整除
// is_leap_year(2024) = 1
// is_leap_year(1900) = 0
// is_leap_year(2000) = 1
点击查看答案
int is_leap_year(int year) {
    return ((year % 4 == 0) && (year % 100 != 0))
        || (year % 400 == 0);
}

🟡 练习 2:不用第三个变量交换两个整数

// 使用异或运算符 ^ 来交换两个变量的值
// 不能创建第三个变量
void swap_xor(int *a, int *b);
点击查看答案
void swap_xor(int *a, int *b) {
    if (a == b) {
        return;  // 防止自己跟自己异或结果为 0
    }
    *a = *a ^ *b;
    *b = *a ^ *b;  // *b = (*a ^ *b) ^ *b = *a
    *a = *a ^ *b;  // *a = (*a ^ *b) ^ *a = *b
}

原理a ^ b ^ b = a,异或同一个数两次等于不变。

🔴 练习 3:实现位域(Bitfield)操作函数

// 给定一个 unsigned int flags:
// 1. set_bit(flags, pos)    — 设置第 pos 位为 1
// 2. clear_bit(flags, pos)  — 清除第 pos 位为 0
// 3. toggle_bit(flags, pos) — 翻转第 pos 位
// 4. get_bit(flags, pos)    — 获取第 pos 位的值(0 或 1)
点击查看答案
unsigned int set_bit(unsigned int flags, int pos) {
    return flags | (1u << pos);
}

unsigned int clear_bit(unsigned int flags, int pos) {
    return flags & ~(1u << pos);
}

unsigned int toggle_bit(unsigned int flags, int pos) {
    return flags ^ (1u << pos);
}

int get_bit(unsigned int flags, int pos) {
    return (flags >> pos) & 1;
}

故障排查(FAQ)

Q: ++ii++ 有什么区别?

++i(前缀)先加后用,i++(后缀)先用后加:

int i = 5;
int a = ++i;  // a = 6, i = 6(先增后赋值)
int b = i++;  // b = 6, i = 7(先赋值后增)

Q: % 运算符可以对负数使用吗?

可以,但结果符号取决于被除数(C 语言标准定义):

printf("%d\n",  7 % -3);   // 输出:  1
printf("%d\n", -7 %  3);   // 输出: -1
printf("%d\n",  7 %  3);   // 输出:  1

Q: 位运算符可以操作浮点数吗?

不能。位运算符只适用于整数类型。

Q: 为什么 a += b * c 的结果不是 (a + b) * c

因为赋值运算符的优先级几乎最低。a += b * c 等价于 a += (b * c),而不是 (a + b) * c

知识扩展(选学)

三元运算符(Ternary Operator)

int max = (a > b) ? a : b;
// 等价于:
// if (a > b) { max = a; } else { max = b; }

可以嵌套(但不推荐):

// 三个数中找最大值
int max3 = (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c);

逗号运算符

逗号运算符 , 从左到右依次求值,返回最右边的值:

int result = (a = 5, b = 10, a + b);  // result = 15

注意:这里的逗号与函数参数分隔符不同。int func(a, b) 中的逗号不是逗号运算符。

sizeof 运算符

sizeof 返回类型或变量的字节大小:

printf("int 的大小: %zu 字节\n", sizeof(int));
printf("double 的大小: %zu 字节\n", sizeof(double));
// sizeof 是一个编译期运算符,不是函数!

小结

这一章我们走过了 C 语言的运算符大家族——

  • 算术运算符:+-*/%—注意整数除法与浮点除法的区别
  • 关系运算符:==!=<><=>=—返回 1 或 0
  • 逻辑运算符:&&||!—理解短路求值
  • 位运算符:&|^~<<>>—二进制位操作的基础
  • 赋值运算符:=, +=, -= 等—复合赋值的简洁写法
  • 优先级很重要,但不确定时加括号是最安全的做法

术语表

术语(中 → 英)说明
运算符(Operator)对操作数执行操作的符号
表达式(Expression)由运算符和操作数组成的代码片段,会产生一个值
算术运算符(Arithmetic Operator)数学运算:+ - * / %
关系运算符(Relational Operator)比较大小关系:== != < > <= >=
逻辑运算符(Logical Operator)布尔运算:`&&
位运算符(Bitwise Operator)二进制位操作:`&
赋值运算符(Assignment Operator)赋值:= += -=
优先级(Precedence)决定运算顺序的规则
结合性(Associativity)同级运算符的求值方向
短路求值(Short-circuit Evaluation)&& 和 `
整数除法(Integer Division)两个整数相除,截断小数部分
前缀递增(Prefix Increment)++i:先加 1,再使用
后缀递增(Postfix Increment)i++:先使用,再加 1
三元运算符(Ternary Operator)?::条件表达式的简写

延伸阅读

继续学习

掌握了运算符,你已经能写出丰富的表达式了!下一章我们将学习数组基础,了解如何在 C 语言中存储和处理一组相关数据。

💡 提示:试着用位运算符实现一个小的"标志寄存器",用一个 int 管理多个开关状态。这在嵌入式开发中非常常见!