运算符与表达式(Operators & Expressions)
"运算符是 C 语言的动词,决定了数据之间如何交互。" —— 我发现
开篇故事
修理工的工具箱里有扳手、螺丝刀、钳子、锤子。扳手拧螺母,螺丝刀拧螺丝,锤子敲钉子。每种工具负责一种操作,但它们的区别不在于外观,而在于「对物体做了什么」。选错工具,拧螺母用锤子只会把活搞砸。
C 语言的运算符就是这样的工具箱。+ 是加法扳手,/ 是除号螺丝刀,== 是比较钳子,&& 是逻辑锤子。它们看似简单,但优先级、结合性和短路求值这些特性就像工具的受力方向——用法不对,结果就可能出乎意料。比如把赋值 = 写成比较 ==,就像把扳手当锤子敲下去,程序不会报错,但干的是另一件事。
运算符是 C 语言的动词,它们决定了数据之间如何交互。理解了运算符,你才能真正让数据为你工作。
本章适合谁
- 学过基本数据类型和变量的人
- 写过程序,但对
++i和i++有什么区别说不清楚的人 - 在条件判断中踩过运算符坑的朋友
你会学到什么
- 算术运算符(Arithmetic):
+、-、*、/、% - 关系运算符(Relational):
==、!=、<、>、<=、>= - 逻辑运算符(Logical):
&&、||、! - 位运算符(Bitwise):
&、|、^、~、<<、>> - 赋值运算符(Assignment):
=,+=,-=,*=,/=,%= - 运算符优先级与结合性
- 短路求值(Short-circuit Evaluation)
前置要求
- 了解整数类型(
int、long)和浮点类型(float、double) - 了解二进制基础(对位运算章节有帮助)
- 会写简单的
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 + 5 | 8 |
- | 减法 | 10 - 4 | 6 |
* | 乘法 | 3 * 4 | 12 |
/ | 除法 | 7 / 2 | 3(整数除法) |
% | 取余(取模) | 7 % 2 | 1 |
注意:
/和%的行为取决于操作数类型。两个整数之间使用/是整数除法,只要有一个是浮点数就是浮点除法。
int a = 7 / 2; // a = 3
double b = 7.0 / 2; // b = 3.5
2. 关系运算符(Relational Operators)
返回 1(真)或 0(假):
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
== | 等于 | 5 == 5 | 1 |
!= | 不等于 | 5 != 3 | 1 |
< | 小于 | 3 < 5 | 1 |
> | 大于 | 5 > 10 | 0 |
<= | 小于等于 | 5 <= 5 | 1 |
>= | 大于等于 | 3 >= 7 | 0 |
3. 逻辑运算符(Logical Operators)
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
&& | 逻辑与 | 1 && 0 | 0:两个都为真才为真 |
| ` | ` | 逻辑或 | |
! | 逻辑非 | !1 | 0:取反 |
短路求值(Short-circuit evaluation):
int x = 0;
if (x != 0 && 10 / x > 2) {
// 永远不会执行到这里——不会除零
// 因为 x != 0 为假,&& 左边的条件失败后,右边不再求值
}
4. 位运算符(Bitwise Operators)
对二进制位进行操作:
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
& | 按位与 | 5 & 3 | 0101 & 0011 = 0001 → 1 |
| ` | ` | 按位或 | 5 \| 3 |
^ | 按位异或 | 5 ^ 3 | 0101 ^ 0011 = 0110 → 6 |
~ | 按位取反 | ~5 | ~00000101 = 11111010 |
<< | 左移 | 5 << 1 | 0101 → 1010 → 10 |
>> | 右移 | 5 >> 1 | 0101 → 0010 → 2 |
常见用法:
// 判断奇偶(比 % 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 = 5 | a = 5 |
+= | a += 3 | a = a + 3 |
-= | a -= 3 | a = a - 3 |
*= | a *= 3 | a = a * 3 |
/= | a /= 3 | a = a / 3 |
%= | a %= 3 | a = a % 3 |
&= | a &= 3 | a = a & 3 |
\|= | a \|= 3 | a = a \| 3 |
^= | a ^= 3 | a = a ^ 3 |
<<= | a <<= 3 | a = a << 3 |
>>= | a >>= 3 | a = a >> 3 |
6. 运算符优先级(Precedence)
部分优先级(从高到低):
| 优先级 | 运算符 | 方向 |
|---|---|---|
| 最高 | () [] . -> | 左→右 |
! ~ ++ -- (type) -(负号) | 右→左 | |
* / % | 左→右 | |
+ - | 左→右 | |
<< >> | 左→右 | |
< <= > >= | 左→右 | |
== != | 左→右 | |
& | 左→右 | |
^ | 左→右 | |
| ` | ` | |
&& | 左→右 | |
\|\| | 左→右 | |
| 最低 | = += -= 等 | 右→左 |
黄金法则:如果你不确定优先级,加括号。
a + b * c→a + (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: ++i 和 i++ 有什么区别?
++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) | ?::条件表达式的简写 |
延伸阅读
- cppreference: Operators in C
- C Operator Precedence Table
- Hacker's Delight(《算法心得》)— 位运算的经典之作
- K&R《C 程序设计语言》第 2 章
继续学习
掌握了运算符,你已经能写出丰富的表达式了!下一章我们将学习数组基础,了解如何在 C 语言中存储和处理一组相关数据。
💡 提示:试着用位运算符实现一个小的"标志寄存器",用一个
int管理多个开关状态。这在嵌入式开发中非常常见!