算术、关系与逻辑运算符
程序光会存数据和输入输出还不够,真正让程序“活”起来的是运算。这篇文章我们来讲 C 语言里最常见的一类工具:运算符。理解了它们,你才能写出能判断、能计算的程序。
学习目标
读完这篇文章,你会掌握:
- 算术运算符的用法,尤其是整数除法的陷阱
- 自增自减运算符的前后置区别
- 关系运算符和逻辑运算符的作用
- 运算符优先级和括号的重要性
- 赋值、复合赋值和条件运算符
算术运算符
C 语言里的算术运算符和数学课上学的差不多:
| 运算符 | 含义 | 示例 |
|---|---|---|
+ | 加法 | a + b |
- | 减法 | a - b |
* | 乘法 | a * b |
/ | 除法 | a / b |
% | 取余 | a % b |
前面四个都好理解,重点讲讲除法和取余。
整数除法会“截断”
如果 / 两边都是整数,C 语言会做整数除法,结果只保留整数部分,直接把小数部分扔掉,而不是四舍五入。
#include <stdio.h>
int main() {
printf("5 / 2 = %d\n", 5 / 2); // 输出 2,不是 2.5
printf("7 / 3 = %d\n", 7 / 3); // 输出 2
printf("-5 / 2 = %d\n", -5 / 2); // 输出 -2
return 0;
}
如果你想要小数结果,至少要让其中一个操作数变成浮点数:
printf("5.0 / 2 = %f\n", 5.0 / 2); // 输出 2.5
提示
整数除法是初学者特别容易踩的坑。以后如果你发现两个整数相除结果总是“不对”,先检查一下是不是掉进了这个陷阱。
取余运算符 %
% 用来求两个整数相除后的余数:
printf("5 %% 2 = %d\n", 5 % 2); // 输出 1
printf("10 %% 3 = %d\n", 10 % 3); // 输出 1
printf("8 %% 4 = %d\n", 8 % 4); // 输出 0
注意:% 只能用于整数,不能用于浮点数。
自增与自减
++ 和 -- 是 C 语言里很特别的运算符,分别表示“加 1”和“减 1”。
int a = 5;
a++; // a 变成 6
a--; // a 变回 5
它们既可以写在变量前面,也可以写在变量后面,但含义有区别:
- 前置:先自增/自减,再使用值
- 后置:先使用值,再自增/自减
#include <stdio.h>
int main() {
int a = 5;
int b = ++a; // a 先变成 6,再把 6 赋给 b
printf("a = %d, b = %d\n", a, b); // a = 6, b = 6
int c = 5;
int d = c++; // 先把 5 赋给 d,然后 c 变成 6
printf("c = %d, d = %d\n", c, d); // c = 6, d = 5
return 0;
}
提示
自增自减虽然方便,但不建议在一个复杂表达式里塞入多个副作用。a = b++ + ++c 在这里修改的是不同对象,含义有规则可循,但可读性仍然很差;真正危险的是在求值顺序没有保证的情况下,多次修改同一个对象,或一边修改一边读取它。复杂操作应拆成多条语句,让每一步的先后关系明确。
关系运算符
关系运算符用来比较两个值,结果是“真”或“假”。在 C 语言里,真用 1 表示,假用 0 表示。
| 运算符 | 含义 |
|---|---|
== | 等于 |
!= | 不等于 |
< | 小于 |
> | 大于 |
<= | 小于等于 |
>= | 大于等于 |
#include <stdio.h>
int main() {
int a = 5, b = 10;
printf("a == b: %d\n", a == b); // 0
printf("a != b: %d\n", a != b); // 1
printf("a < b: %d\n", a < b); // 1
printf("a >= b: %d\n", a >= b); // 0
return 0;
}
注意:判断相等是 ==,不是一个等号 =。= 是赋值运算符。后面在讲分支结构时,我们会再强调这个坑。
逻辑运算符
关系运算符比较单个条件,逻辑运算符用来组合多个条件。
| 运算符 | 含义 | 示例 |
|---|---|---|
&& | 逻辑与 | a > 0 && a < 10 |
|| | 逻辑或 | a < 0 || a > 100 |
! | 逻辑非 | !(a == 0) |
&&:两边都为真,结果才为真||:只要一边为真,结果就为真!:真变假,假变真
#include <stdio.h>
int main() {
int score = 85;
printf("及格且优秀:%d\n", score >= 60 && score >= 80); // 1
printf("不及格或满分:%d\n", score < 60 || score == 100); // 0
printf("不是满分:%d\n", !(score == 100)); // 1
return 0;
}
提示
&& 和 || 都有“短路”特性。如果 && 左边为假,右边就不会执行;如果 || 左边为真,右边也不会执行。这个特性后面会很有用。
位运算符
位运算符直接操作整数的二进制位。虽然初学阶段用得不多,但在底层编程(嵌入式、图形、加密)中非常常见。
| 运算符 | 含义 | 示例 |
|---|---|---|
& | 按位与 | a & b |
| ` | ` | 按位或 |
^ | 按位异或 | a ^ b |
~ | 按位取反 | ~a |
<< | 左移 | a << 2(相当于 a × 4) |
>> | 右移 | a >> 1(相当于 a ÷ 2) |
#include <stdio.h>
int main() {
unsigned char a = 0x0C; // 二进制 1100,即 12
unsigned char b = 0x0A; // 二进制 1010,即 10
printf("a & b = %d\n", a & b); // 1000 = 8
printf("a | b = %d\n", a | b); // 1110 = 14
printf("a ^ b = %d\n", a ^ b); // 0110 = 6
printf("~a = %d\n", (unsigned char)~a); // 0011 = 3
printf("a << 1 = %d\n", a << 1); // 11000 = 24
printf("a >> 1 = %d\n", a >> 1); // 0110 = 6
return 0;
}
区分按位与和逻辑与
初学者常把 &(按位与)和 &&(逻辑与)搞混。记住:&& 用来判断真/假,结果只有 0 或 1;& 用来操作二进制位,结果可以是任意整数。
运算符优先级与括号
当一个表达式里有多种运算符时,优先级决定了先算谁。常见的优先级大致是:
- 括号
() - 自增自减
++、-- - 算术运算符
*、/、%高于+、- - 关系运算符
<、>、<=、>=高于==、!= - 逻辑运算符
!>&&>|| - 赋值运算符
=优先级最低
提示
如果你记不住优先级,或者表达式稍微复杂一点,就加括号。括号不会让你的程序变慢,但能让程序更清楚、更少出错。
int result = a + b * c; // 先算乘法
int result2 = (a + b) * c; // 先算加法
赋值与复合赋值
赋值运算符 = 就是把右边的值赋给左边的变量。
int a;
a = 10; // 把 10 赋给 a
如果要对变量做加减乘除后再赋回去,可以用复合赋值运算符:
int a = 10;
a += 5; // 等价于 a = a + 5
a -= 3; // 等价于 a = a - 3
a *= 2; // 等价于 a = a * 2
a /= 4; // 等价于 a = a / 4
a %= 3; // 等价于 a = a % 3
复合赋值写起来更简洁,但初学者可能会觉得不够直观。看不习惯的时候,就展开写,等熟练了再用缩写。
条件运算符 ?:
条件运算符是 C 语言里唯一一个三元运算符,格式是:
条件 ? 表达式1 : 表达式2
如果条件为真,整个表达式的值就是 表达式1,否则是 表达式2。
#include <stdio.h>
int main() {
int a = 5, b = 3;
int max = (a > b) ? a : b;
printf("较大的数是:%d\n", max);
return 0;
}
这个运算符适合写简单的判断,比如取两个数的最大值。但如果逻辑太复杂,还是建议用 if-else,可读性更好。
常见错误与注意事项
- 把
==写成=
if (a = 5) { // 这是赋值,不是比较
...
}
- 整数除法 expecting 小数
int average = (a + b) / 2; // 如果 a + b 是奇数,结果会截断
- 逻辑运算符写成单个字符
if (a > 0 & b > 0) { // 错误!这是按位与,不是逻辑与
表达式不仅有结果,还有求值规则
int i = 1;
int result = i++ + i++; // 不要这样写
在一个表达式里多次修改同一对象,并且求值顺序没有满足语言规则,可能产生未定义行为。即使某个编译器每次给出同样结果,也不要依赖它。把副作用拆开最清楚:
int first = i++;
int second = i++;
int result = first + second;
短路求值可以保护后续操作
if (p != NULL && *p > 0) {
/* 只有 p 非空才会解引用 */
}
&& 左侧为假时不会执行右侧;|| 左侧为真时不会执行右侧。这个规则既能提高效率,也常用于保证右侧操作的前置条件。
整数溢出要区分类型
- 无符号整数按模
2^N回绕,这是定义良好的; - 有符号整数溢出是未定义行为;
- 浮点数可能得到无穷、NaN 或发生舍入,不等同于整数溢出。
不要用“溢出后一定变负数”判断有符号加法是否越界。应在运算前检查边界,或使用更宽类型。
位运算的适用范围
& | ^ ~ << >> 面向整数的二进制表示,常用于标志位、协议和底层寄存器。位移数量不能为负,也不能大于等于类型位宽;对负的有符号数做位移尤其容易产生实现相关或未定义结果。处理位级数据时通常优先使用明确宽度的无符号类型。
小结
这篇文章我们学习了 C 语言中常用的运算符:
- 算术运算符:
+、-、*、/、%,注意整数除法 - 自增自减:
++、--,分清前置和后置 - 关系运算符:
==、!=、<、>、<=、>= - 逻辑运算符:
&&、||、! - 赋值和复合赋值:
=、+=、-=等 - 条件运算符:
?:
记不住优先级的时候,多用括号,不会吃亏。
下一篇先补上表达式中最容易被忽略的一层:类型转换、整数提升和数值边界。理解操作数最终会变成什么类型后,再进入分支判断会更稳。