运算符基础:让数据动起来
上一篇里我们把 C 语言的数据类型从里到外拆了一遍——整数怎么存、小数怎么存、字符怎么存。但光有数据还不够,我们还得让数据"动起来":做加减乘除、比较大小、判断真假。这些操作在 C 语言里由运算符来完成。
你可以把运算符理解为 C 语言里的"动词"——变量和常量是名词,运算符把它们连接起来变成表达式,表达式组合成语句,语句构成程序。我们日常编程中用到的运算符其实就那么几个,但每一个都有它的小脾气。这一篇我们把最常用的算术、关系、逻辑运算符过一遍,重点关注那些容易踩坑的地方。位运算和更深层的求值顺序问题,我们留到下一篇专门讨论。
学习目标 完成本章后,你将能够:
- [ ] 熟练使用五种算术运算符和自增自减运算符
- [ ] 理解整数除法的"向零取整"规则
- [ ] 掌握关系运算符和逻辑运算符的短路求值特性
- [ ] 正确使用条件运算符和逗号运算符
环境说明
我们接下来的所有实验都在这个环境下进行:
- 平台:Linux x86_64(WSL2 也可以)
- 编译器:GCC 13+ 或 Clang 17+
- 编译选项:
-Wall -Wextra -std=c17
第一步——加减乘除:算术运算符
五个基本运算符
C 语言提供了五种基本算术运算符:+(加)、-(减)、*(乘)、/(除)、%(取模)。前四个适用于所有数值类型,取模 % 只适用于整数。
int a = 10 + 3; // 13
int b = 10 - 3; // 7
int c = 10 * 3; // 30
int d = 10 / 3; // 3(整数除法,小数部分直接丢弃)
int e = 10 % 3; // 1(10 除以 3 的余数)这里有一个新手很容易踩的坑:两个整数做除法,结果还是整数。10 / 3 不是 3.333...,而是 3。小数部分被直接丢弃了,不是四舍五入。
⚠️ 踩坑预警 如果你想要得到带小数的除法结果,至少有一个操作数必须是浮点数。
10 / 3得3,但10.0 / 3或10 / 3.0得3.333...。
负数除法:向零取整
C99 标准明确规定:整数除法向零取整。也就是说,结果的小数部分被丢弃后,结果趋近于零。7 / 2 是 3,-7 / 2 是 -3(不是 -4)。取模运算的余数符号和被除数相同:-7 % 2 是 -1。
int a = 7 / 2; // 3
int b = -7 / 2; // -3(向零取整)
int c = -7 % 2; // -1(余数符号与被除数相同)来验证一下:
gcc -Wall -Wextra -std=c17 div_demo.c -o div_demo && ./div_demo运行结果:
7 / 2 = 3
-7 / 2 = -3
-7 %% 2 = -1第二步——自增和自减:两个特殊的运算符
前缀和后缀的区别
++(自增)和 --(自减)是 C 语言里比较特殊的运算符——它们既能放在变量前面(前缀),也能放在变量后面(后缀)。单独使用时两者效果一样,但在表达式内部混用时行为不同。
打个比方来理解:前缀 ++x 就像"先涨价再结账"——先把值加 1,然后返回新值;后缀 x++ 就像"先结账再涨价"——先返回当前值,然后再加 1。
int x = 5;
int a = ++x; // x 先变成 6,a 得到 6
int b = x++; // b 先得到 6,然后 x 变成 7
printf("a=%d, b=%d, x=%d\n", a, b, x);运行结果:
a=6, b=6, x=7千万别这么写
这里有一个非常重要的事情需要提醒大家——不要在同一个表达式中对同一个变量多次使用 ++/--:
int i = 3;
int a = i++ + ++i; // 未定义行为!这种写法在 C 标准中是未定义行为(Undefined Behavior,简称 UB)。简单来说,标准规定"不许这么写",编译器可以按任何方式处理——不同编译器可能给出完全不同的结果。关于为什么它是 UB,我们会在下一篇讲序列点的时候详细解释。现在只需要记住:一个表达式里不要对同一个变量用两次 ++ 或 --。
⚠️ 踩坑预警
i = i++、a[i] = i++、printf("%d %d", i++, i++)这些写法全部是未定义行为。在面试题里看到这种东西,知道它是 UB 就行,别去猜"答案是多少"——因为根本没有正确答案。
第三步——比较和判断:关系与逻辑运算符
关系运算符
关系运算符用来比较两个值的大小关系,结果是"真"或"假"。在 C 语言中,"真"用整数 1 表示,"假"用整数 0 表示。
int a = (5 > 3); // 1(真)
int b = (5 < 3); // 0(假)
int c = (5 == 5); // 1(相等)
int d = (5 != 5); // 0(不相等)一个常见的笔误是把 ==(相等比较)写成 =(赋值)。if (x = 5) 永远为真(因为赋值表达式的值是 5,非零即真),而且 x 被意外修改了。好的编译器遇到这种写法会发出警告,建议开启 -Wall 让编译器帮你盯着。
逻辑运算符
逻辑运算符有三个:&&(逻辑与)、||(逻辑或)、!(逻辑非)。它们操作的是"真假值"——把操作数当作布尔值来看待,零是假,非零是真。
if (age >= 18 && age <= 65) {
// age 在 18 到 65 之间
}
if (score < 0 || score > 100) {
// score 不在合法范围
}
if (!is_valid) {
// is_valid 为假时执行
}短路求值——非常实用的特性
&& 和 || 有一个非常重要的特性叫短路求值。对于 &&,如果左操作数为假,右操作数压根不会被计算——因为整个表达式已经是假了,右边是什么都不影响结果。|| 正好相反,左操作数为真时右边不计算。
这个特性在实际编程中非常有用,最经典的场景是先检查指针是否为空,再访问指针指向的内容:
// 安全地解引用指针
if (ptr != NULL && ptr->value > 0) {
// 如果 ptr 是 NULL,ptr->value 不会被访问
// 避免了空指针解引用导致的崩溃
}如果 ptr 是空指针,ptr != NULL 为假,由于短路求值,ptr->value 不会被求值,程序安全。如果没有短路求值,即使 ptr 是空的也会尝试访问 ptr->value,程序直接崩溃。
来验证一下短路求值的效果:
#include <stdio.h>
int counter = 0;
int increment(void)
{
counter++;
printf("increment() 被调用了,counter = %d\n", counter);
return counter;
}
int main(void)
{
int result = (0 && increment()); // 左边为 0(假),右边不会执行
printf("result = %d, counter = %d\n", result, counter);
result = (1 || increment()); // 左边为 1(真),右边不会执行
printf("result = %d, counter = %d\n", result, counter);
return 0;
}运行结果:
result = 0, counter = 0
result = 1, counter = 0很好,increment() 一次都没被调用——短路求值生效了。
第四步——条件运算符与逗号运算符
条件运算符 ?:
条件运算符是 C 语言里唯一的三目运算符,语法是 condition ? expr1 : expr2。如果 condition 为真,整个表达式的值是 expr1,否则是 expr2。
你可以把它理解为一个"浓缩版的 if-else"——在需要根据条件选择一个值、但又不想写完整 if-else 语句的场景下特别方便:
int max = (a > b) ? a : b; // 取较大值
const char* label = (count == 1) ? "item" : "items"; // 单复数条件运算符可以嵌套,但超过两层就开始影响可读了:
const char* grade = (score >= 90) ? "A" :
(score >= 80) ? "B" :
(score >= 60) ? "C" : "F";逗号运算符
逗号运算符 , 是 C 中优先级最低的运算符。它从左到右依次求值两个操作数,整个表达式的值是右操作数的值:
int a = (1, 2, 3); // 先求值 1,再求值 2,最后求值 3,a = 3这个运算符单独用的场景很少,最常见的用法是在 for 循环中同时维护多个变量:
for (int i = 0, j = n - 1; i < j; i++, j--) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}注意 int i = 0, j = n - 1 中的逗号是声明分隔符(不是逗号运算符),但 i++, j-- 中的逗号确实是逗号运算符。
C++ 衔接
C++ 在运算符方面做了两件重要的事情。第一件是引入了 <stdbool.h> 的 C++ 版本——bool、true、false 在 C++ 中是语言内置的关键字,不像 C 中是宏。第二件是运算符重载——你可以给自定义类型定义 +、== 等运算符的行为,让自定义类型用起来像内置类型一样自然。
但有一个重要限制:虽然 C++ 允许重载 && 和 ||,但重载后会丧失短路求值特性。因为重载的运算符本质上是函数调用,两个参数都会被求值,短路特性就没了。所以实践中,永远不要重载 && 和 ||。
小结
到这里我们已经把 C 语言最常用的运算符过了一遍。核心要点:整数除法直接丢弃小数部分,不是四舍五入;自增自减的前缀后缀在表达式中有不同的行为,但不要在同一个表达式里对同一变量用两次;&& 和 || 的短路求值非常实用,先检查安全条件再做实际操作是常见的编程模式。
接下来问题来了——我们还没讲位运算。如果你以后要接触嵌入式开发,位运算就是家常便饭:配置硬件寄存器、解析通信协议的位字段,都离不开它。这些内容加上更深层的运算符优先级和求值顺序,就是我们下一篇要啃的骨头。
练习
练习 1:整数除法预测
不实际运行,预测以下表达式的值,然后写程序验证:
printf("%d\n", 7 / 2);
printf("%d\n", -7 / 2);
printf("%d\n", 7 / -2);
printf("%d\n", 7 % 2);
printf("%d\n", -7 % 2);练习 2:短路求值实战
写一个函数,安全地从数组中找到第一个大于指定值的元素。利用短路求值确保不越界:
/// @brief 在数组中查找第一个大于 threshold 的元素
/// @param arr 数组
/// @param len 数组长度
/// @param threshold 阈值
/// @return 找到的元素的索引,未找到返回 -1
int find_first_above(const int* arr, size_t len, int threshold);