第 5 章:运算符与表达式
第 5 章:运算符与表达式
“程序若无运算,恰如机器人只会站着发呆——有力气却使不出来。”
欢迎来到 C 语言的"十八般武艺"章节!运算符(Operator)和表达式(Expression)是让程序真正"动起来"的核心。我们在前几章学会了声明变量、输入输出,现在终于要给这台小机器装上"发动机"了。
想象一下:运算符就像是厨房里的各种刀具——有切菜的(/)、剁骨的(%)、还有神秘组合技(&&、||)。而表达式则是用这些刀具组合出来的"菜品"。准备好了吗?让我们开火!
5.1 算术运算符:加减乘除余
算术运算符是程序员最亲密的伙伴,从菜市场算账到游戏里计算伤害,全靠它们。
5.1.1 基本的加、减、乘
1
2
3
4
5
6
7
8
9
10
11
12
| #include <stdio.h>
int main() {
int a = 10;
int b = 3;
printf("a + b = %d\n", a + b); // 输出: a + b = 13
printf("a - b = %d\n", a - b); // 输出: a - b = 7
printf("a * b = %d\n", a * b); // 输出: a * b = 30
return 0;
}
|
等等!为什么乘法是 * 而不是 ×?——因为你的键盘上没 × 啊!C 语言诞生于 1972 年,那时候计算机还在用 ASCII 这种"简约风"字符集,一个萝卜一个坑,乘号就用 * 凑合了。同样,除号也没有 ÷,就用 / 顶上。
5.1.2 除法:整数除法 vs 浮点除法
这是新人最最最容易踩坑的地方!来看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #include <stdio.h>
int main() {
int x = 7;
int y = 2;
// 整数除法 —— 只保留整数部分,小数部分"咔嚓"掉了!
printf("x / y = %d\n", x / y); // 输出: x / y = 3
// 浮点除法 —— 只要除数或被除数有一个是浮点,结果就是小数
printf("x / 2.0 = %f\n", x / 2.0); // 输出: x / 2.0 = 3.500000
// 用强制类型转换来"解锁"小数
printf("(float)x / y = %f\n", (float)x / y); // 输出: (float)x / y = 3.500000
return 0;
}
|
生活比喻:整数除法就像你妈分饺子——7 个饺子 2 个人吃,每人分到 3 个,还剩 1 个(不是 3.5 个!)。你要想吃半个?得先"变身"成浮点数再说!
5.1.3 取余运算符 %:除法的"售后服务"
取余(也叫模运算)返回除法运算的余数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <stdio.h>
int main() {
int x = 7;
int y = 2;
printf("x %% y = %d\n", x % y); // 输出: x %% y = 1
// 注意:printf 中要输出 %,需要写 %%
// 经典应用:判断奇偶
if (x % 2 == 0) {
printf("%d 是偶数\n", x);
} else {
printf("%d 是奇数\n", x); // 输出: 7 是奇数
}
// 判断能否被 3 整除
int z = 15;
printf("%d %% 3 = %d\n", z, z % 3); // 输出: 15 % 3 = 0
return 0;
}
|
小白疑问:为什么 printf 里 % 要写 %%?
因为 % 在 printf 里是"特殊身份"——用来标记格式说明符(如 %d、%f)。你想输出字面的 %,就得 escaped 一下,写成 %%。这就像想说"老板姓李",你得写"李老板"以免混淆一样。
5.1.4 ⚠️ 负数除法:历史遗留的"坑"
这是 C 语言一个有趣的历史故事:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <stdio.h>
int main() {
int a = -7;
int b = 2;
// C89 标准:未定义!编译器想咋整咋整
// C99 及之后:向零截断(truncation toward zero)
// -7 / 2 = -3(不是 -4!)
printf("-7 / 2 = %d\n", a / b); // C99+: 输出: -7 / 2 = -3
printf("-7 %% 2 = %d\n", a % b); // C99+: 输出: -7 % 2 = -1
// 不同编译器的"史前遗迹"
// Turbo C(C89)可能输出:-7 / 2 = -4
// GCC/Clang(C99+)输出:-7 / 2 = -3
return 0;
}
|
C89 vs C99 的恩怨情仇:早期的 C89 觉得负数除法"无所谓",让各编译器自己决定。结果 Turbo C 说"向负无穷取整",GCC 说"不,我向零截断",大家吵得不可开交。到了 C99,标准委员会一拍桌子:“都给我向零截断!“这才平息了江湖纷争。
所以,如果你在考试或面试中遇到负数除法,一定要说明用的是哪个 C 标准!
5.2 关系运算符:谁更大?
关系运算符用于比较大小,结果只有两种:true(真)或 false(假)。在 C 语言中,真用 1 表示,假用 0 表示。
1
2
3
4
5
6
7
8
9
10
11
12
13
| #include <stdio.h>
int main() {
int age = 25;
int voting_age = 18;
printf("age > voting_age: %d\n", age > voting_age); // 输出: age > voting_age: 1 (真)
printf("age < voting_age: %d\n", age < voting_age); // 输出: age < voting_age: 0 (假)
printf("age >= 25: %d\n", age >= 25); // 输出: age >= 25: 1
printf("age <= 25: %d\n", age <= 25); // 输出: age <= 25: 1
return 0;
}
|
生活比喻:关系运算符就像超市门口的年龄验证机——你是 > 18 岁,还是 < 18 岁,结果只有"能进”(1)和"不能进”(0)。
注意:>= 不是 =>,<= 不是 <=(很多初学者会搞反!)。C 语言的运算符是从左到右读的,所以 >= 就是"大于等于"——先写 >,再写 =,合起来表示"大于或等于"。
5.3 相等运算符:== vs !=
5.3.1 ==:判断"相等"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #include <stdio.h>
int main() {
int password = 123456;
int input = 123456;
// 登录验证
if (input == password) {
printf("登录成功!\n"); // 输出: 登录成功!
} else {
printf("密码错误!\n");
}
return 0;
}
|
5.3.2 !=:判断"不相等"
1
2
3
4
5
6
7
8
9
10
11
12
| #include <stdio.h>
int main() {
int score = 59;
// 59 分不等同于及格线 60 分,所以...
if (score != 60) {
printf("对不起,%d 分不等于及格!\n", score); // 输出: 对不起,59 分不等于及格!
}
return 0;
}
|
天下第一坑:== 和 = 的区别!
= 是赋值运算符(把右边的值塞进左边)== 是比较运算符(判断两边是否相等)
很多新手会写 if (x = 5) 然后以为在判断 x 是否等于 5,结果 x 被改成了 5,条件永远为真!编译器一般会报警告,但如果你关了警告……debug 到天明吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #include <stdio.h>
int main() {
int x = 3;
// 错误写法:把 5 赋值给了 x,条件永远为真
// if (x = 5) { ... }
// 正确写法
if (x == 5) {
printf("x 是 5\n");
} else {
printf("x 不是 5,x = %d\n", x); // 输出: x 不是 5,x = 3
}
return 0;
}
|
5.4 逻辑运算符:与、或、非
逻辑运算符用于组合多个条件,就像生活中说"既要……又要……"、“或者……"。
5.4.1 &&:逻辑与(AND)—— “既要……又要……”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <stdio.h>
int main() {
int age = 20;
int has_id = 1; // 1 = 有身份证
// 进入网吧的条件:年满 18 且有身份证
if (age >= 18 && has_id == 1) {
printf("允许进入网吧!\n"); // 输出: 允许进入网吧!
} else {
printf("禁止进入!\n");
}
// 多个条件
int has_money = 1;
if (age >= 18 && has_id == 1 && has_money == 1) {
printf("可以上网还能买可乐!\n"); // 输出: 可以上网还能买可乐!
}
return 0;
}
|
5.4.2 ||:逻辑或(OR)—— “或者……”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #include <stdio.h>
int main() {
char vip = 'Y'; // VIP 会员
char student = 'N'; // 不是学生
// 打折条件:VIP会员 或者 学生
if (vip == 'Y' || student == 'Y') {
printf("恭喜!享受 8 折优惠!\n"); // 输出: 恭喜!享受 8 折优惠!
} else {
printf("原价购买,贫穷警告!\n");
}
return 0;
}
|
5.4.3 !:逻辑非(NOT)—— “反着来”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <stdio.h>
int main() {
int is_raining = 0; // 0 = 没下雨
// 没下雨就去野餐!
if (!is_raining) {
printf("天气晴朗,野餐去!\n"); // 输出: 天气晴朗,野餐去!
}
int is_closed = 1; // 1 = 门关了
if (!is_closed) {
printf("门开着,进来吧!\n");
} else {
printf("门关着呢,等会儿吧!\n"); // 输出: 门关着呢,等会儿吧!
}
return 0;
}
|
5.4.4 短路求值(Short-Circuit Evaluation):聪明的"偷懒”
这是逻辑运算符的隐藏技能!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <stdio.h>
int main() {
int a = 0;
int b = 5;
// && 的短路:左边是 0(假),右边根本不用看了!
// 因为 "假 && 什么都 = 假"
if (a != 0 && b / a > 0) { // 正常这里 b/a 会报错(除以0)
printf("这行不会打印\n");
} else {
printf("短路了!a != 0 是假,直接跳到 else\n"); // 输出: 短路了!
}
// || 的短路:左边是 1(真),右边不用看了!
// 因为 "真 || 什么都 = 真"
if (b > 0 || a / b == 0) {
printf("短路了!b > 0 是真,结果肯定是真!\n"); // 输出: 短路了!b > 0 是真,结果肯定是真!
}
return 0;
}
|
生活比喻:你去相亲,女嘉宾说"我要年入百万 且 身高一米八"。你一看——年入才 50 万,第一条就不满足,根本不用量身高了!这就是 && 的短路。
如果她说"我要年入百万 或 身高一米八"。你一看——年入 50 万,第一条不满足;但再一看——身高一米九!满足第二条!不用查收入了!这就是 || 的短路。
5.5 位运算符:潜入二进制世界
位运算符是 C 语言的"高级技能",直接操控数字的二进制位。如果说普通运算符是炒菜,位运算就是解剖细胞——精细到 bit 层面。
5.5.1 预备知识:原码、反码、补码(计算机的"暗号")
在深入位运算之前,我们必须搞清楚计算机是怎么存负数的。
原码(Sign-Magnitude):最高位表示符号(0 正 1 负),其余位表示绝对值。
+5 的原码:00000101-5 的原码:10000101
反码(Ones’ Complement):正数的反码等于原码;负数的反码是符号位不变,其余位取反。
+5 的反码:00000101-5 的反码:11111010
补码(Two’s Complement):正数的补码等于原码;负数的补码是反码加 1。
+5 的补码:00000101-5 的补码:11111011(11111010 + 1)
为什么要有补码?
因为人类不喜欢"两个零"(+0 和 -0)和进位复杂的减法器。补码让 CPU 只需用加法器就能做减法——你看,5 - 3 等于 5 + (-3),而 -3 在补码里就是 ...11111101,直接加就行了!
补码的巧妙之处:int 类型用 32 位,-1 就是全部位都是 1(0xFFFFFFFF),INT_MIN(-2147483648)的补码是 0x80000000,只有最高位是 1。
1
2
3
4
5
6
7
8
9
10
11
12
| #include <stdio.h>
int main() {
// 用 %x 查看十六进制(更能看清二进制)
int a = 5;
int b = -3;
printf("5 的十六进制: 0x%x\n", a); // 输出: 5 的十六进制: 0x5
printf("-3 的十六进制: 0x%x\n", b); // 输出: -3 的十六进制: 0xfffffffd
return 0;
}
|
5.5.2 &:按位与(AND)
对应位都为 1 才为 1,否则为 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <stdio.h>
int main() {
// 5 的二进制: 0101
// 3 的二进制: 0011
// ---------------
// 5 & 3 = 0001 = 1
printf("5 & 3 = %d\n", 5 & 3); // 输出: 5 & 3 = 1
// 常用技巧:判断某一位是否为 1
int flags = 0b11010; // 二进制: 11010 (C99 开始支持二进制字面量)
int bit_to_check = 0b00010; // 检查第二位
if (flags & bit_to_check) {
printf("第二位是 1!\n"); // 输出: 第二位是 1!
}
return 0;
}
|
生活比喻:按位与就像投票——只有所有评委都给"1"(通过),结果才是"1"。任何一位是"0"(反对),结果就是"0"。
5.5.3 |:按位或(OR)
对应位只要有一个是 1 就为 1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <stdio.h>
int main() {
// 5 的二进制: 0101
// 3 的二进制: 0011
// ---------------
// 5 | 3 = 0111 = 7
printf("5 | 3 = %d\n", 5 | 3); // 输出: 5 | 3 = 7
// 常用技巧:把某一位设置为 1
int flags = 0b10000; // 只设置了第 5 位
int new_bit = 0b00100; // 要设置的第 3 位
printf("设置前 flags = %d\n", flags); // 输出: 设置前 flags = 16
flags = flags | new_bit;
printf("设置后 flags = %d\n", flags); // 输出: 设置后 flags = 20
return 0;
}
|
生活比喻:按位或就像组队——只要有一方有"超能力"(该位为1),整个队伍就有这个能力。
5.5.4 ^:按位异或(XOR)
对应位不同为 1,相同为 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <stdio.h>
int main() {
// 5 的二进制: 0101
// 3 的二进制: 0011
// ---------------
// 5 ^ 3 = 0110 = 6
printf("5 ^ 3 = %d\n", 5 ^ 3); // 输出: 5 ^ 3 = 6
// 经典技巧:交换两个数(不用临时变量!)
int x = 5, y = 3;
printf("交换前: x = %d, y = %d\n", x, y); // 输出: 交换前: x = 5, y = 3
x = x ^ y;
y = x ^ y;
x = x ^ y;
printf("交换后: x = %d, y = %d\n", x, y); // 输出: 交换后: x = 3, y = 5
return 0;
}
|
异或的魔法:任何数异或其自身等于 0,任何数异或 0 等于其自身。所以 a ^ a = 0,a ^ 0 = a。
交换的原理:x = x ^ y → y = (x ^ y) ^ y = x → x = (x ^ y) ^ x = y。三次异或,乾坤大挪移!
5.5.5 ~:按位取反
所有位取反,0 变 1,1 变 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <stdio.h>
int main() {
// ~5 = !0101 = 1010 = -6(在补码体系中)
printf("~5 = %d\n", ~5); // 输出: ~5 = -6
// 经典技巧:~x + 1 = -x (求一个数的相反数)
int num = 42;
printf("~num + 1 = %d, -num = %d\n", ~num + 1, -num);
// 输出: ~num + 1 = -42, -num = -42
return 0;
}
|
为什么 ~5 = -6?
因为在补码体系中,~x = -(x+1)。这背后有一套数学证明,记住结论就行:~x = -(x+1)。
5.5.6 << 和 >>:左移与右移
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| #include <stdio.h>
int main() {
int a = 3; // 二进制: 00000011
// 左移:低位补 0,高位丢弃
printf("3 << 1 = %d\n", a << 1); // 输出: 3 << 1 = 6 (00000110)
printf("3 << 2 = %d\n", a << 2); // 输出: 3 << 2 = 12 (00001100)
// 右移:高位补符号位(算术右移),低位丢弃
int b = -8; // 二进制(补码): 11111111 11111111 11111111 11111000
printf("-8 >> 1 = %d\n", b >> 1); // 输出: -8 >> 1 = -4
// 无符号右移(逻辑右移):高位补 0
unsigned int c = (unsigned int)-8; // 强制转成无符号
printf("无符号 -8 >> 1 = %u\n", c >> 1);
// 重要结论:
// x << n == x * 2^n (左移 n 位相当于乘以 2 的 n 次方)
// x >> n == x / 2^n (右移 n 位相当于除以 2 的 n 次方)
int d = 16;
printf("16 << 2 = %d, 16 / 4 = %d\n", d << 2, d / 4); // 输出: 16 << 2 = 64, 16 / 4 = 4
printf("16 >> 2 = %d, 16 / 4 = %d\n", d >> 2, d / 4); // 输出: 16 >> 2 = 4, 16 / 4 = 4
return 0;
}
|
警告:左移和右移的位数必须小于操作数的位数!对 int(通常是 32 位)来说,移 32 位或以上是未定义行为!另外,有符号数的右移在某些平台可能是算术右移(补符号位),在另一些平台可能是逻辑右移,不可移植。如果需要可移植的右移,请使用无符号数。
5.6 赋值运算符:把钱装进口袋
5.6.1 基本赋值 =
赋值运算符 = 把右边表达式的值赋给左边的变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #include <stdio.h>
int main() {
int a = 10; // 这也是赋值,初始化时直接赋值
printf("a = %d\n", a); // 输出: a = 10
a = 20; // 这是赋值,把 20 装进 a
printf("a = %d\n", a); // 输出: a = 20
// 赋值表达式本身也有值!
// a = 20 的值是 20
int b;
b = (a = 30); // 先把 30 赋给 a,再把 a 的值赋给 b
printf("a = %d, b = %d\n", a, b); // 输出: a = 30, b = 30
return 0;
}
|
注意:= 和 == 完全不是一回事!= 是赋值,== 是比较。前者改变变量,后者不改变。
5.6.2 复合赋值运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| #include <stdio.h>
int main() {
int x = 10;
x += 5; // 等价于 x = x + 5
printf("x += 5 后: x = %d\n", x); // 输出: x += 5 后: x = 15
x -= 3; // 等价于 x = x - 3
printf("x -= 3 后: x = %d\n", x); // 输出: x -= 3 后: x = 12
x *= 2; // 等价于 x = x * 2
printf("x *= 2 后: x = %d\n", x); // 输出: x *= 2 后: x = 24
x /= 4; // 等价于 x = x / 4
printf("x /= 4 后: x = %d\n", x); // 输出: x /= 4 后: x = 6
x %= 4; // 等价于 x = x % 4
printf("x %%= 4 后: x = %d\n", x); // 输出: x %= 4 后: x = 2
x &= 3; // 等价于 x = x & 3
printf("x &= 3 后: x = %d\n", x); // 输出: x &= 3 后: x = 2
x |= 1; // 等价于 x = x | 1
printf("x |= 1 后: x = %d\n", x); // 输出: x |= 1 后: x = 3
x ^= 2; // 等价于 x = x ^ 2
printf("x ^= 2 后: x = %d\n", x); // 输出: x ^= 2 后: x = 1
x <<= 2; // 等价于 x = x << 2
printf("x <<= 2 后: x = %d\n", x); // 输出: x <<= 2 后: x = 4
x >>= 1; // 等价于 x = x >> 1
printf("x >>= 1 后: x = %d\n", x); // 输出: x >>= 1 后: x = 2
return 0;
}
|
为什么要用复合赋值?
- 省手指:少敲几个字
- 表达更清晰:
x += 5 明确表示"增加 5",比 x = x + 5 更直观 - 编译器优化:某些情况下
x += 5 可能比 x = x + 5 更高效(虽然现代编译器都能优化)
5.7 自增自减:++i vs i++
这是 C 语言最经典、最容易搞混的知识点之一!
5.7.1 前置自增 ++i vs 后置自增 i++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <stdio.h>
int main() {
int i = 5;
int j = 5;
// 前置 ++i:先自增,再返回值
printf("++i = %d, i = %d\n", ++i, i); // 输出: ++i = 6, i = 6
// 后置 i++:先返回值,再自增
printf("i++ = %d, i = %d\n", i++, i); // 输出: i++ = 6, i = 7
// 重置
i = 5; j = 5;
int a = ++i; // i 先变成 6,然后 a 拿到 6
int b = j++; // b 拿到 5,然后 j 变成 6
printf("a = %d, b = %d, i = %d, j = %d\n", a, b, i, j);
// 输出: a = 6, b = 5, i = 6, j = 6
return 0;
}
|
生活比喻:
++i 就像先刷卡再出门——卡里的钱先涨(自增),然后你出门(返回值)i++ 就像先出门再刷卡——你先用原卡出门(返回值),进门再刷卡(自增)
关键是:返回值不同,但最终 i 的值一样(都加了 1)。
5.7.2 自减同理
1
2
3
4
5
6
7
8
9
| #include <stdio.h>
int main() {
int i = 5;
printf("--i = %d\n", --i); // 输出: --i = 4
printf("i-- = %d, i = %d\n", i--, i); // 输出: i-- = 4, i = 3
return 0;
}
|
5.7.3 副作用(Side Effect)与序列点
每个运算符都可能产生副作用——即修改了某些值(最常见的就是修改变量)。
1
2
3
4
5
6
7
8
9
10
11
12
13
| #include <stdio.h>
int main() {
int i = 1;
// 单独使用时,++i 和 i++ 效果一样
i++;
printf("i = %d\n", i); // 输出: i = 2
++i;
printf("i = %d\n", i); // 输出: i = 3
return 0;
}
|
副作用的问题:i++ 和 ++i 在单独使用时没区别,但放在表达式里就有区别了。更糟糕的是,有些写法是未定义行为(后面 5.13 节会详细讲)。记住一条:如果不需要返回值,只想自增,用 ++i 或 i++ 都可以;如果需要返回值,慎重选择!
5.8 条件运算符:? : —— C 语言的三元表达式
条件运算符是 C 语言唯一的三元运算符(一口气吃三个操作数),格式为:
如果条件为真,整个表达式的值是值1;如果条件为假,整个表达式的值是值2。
5.8.1 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #include <stdio.h>
int main() {
int score = 75;
// 传统写法
char grade;
if (score >= 60) {
grade = 'P';
} else {
grade = 'F';
}
printf("分数 %d, 等级 %c\n", score, grade); // 输出: 分数 75, 等级 P
// 条件运算符一行搞定!
grade = (score >= 60) ? 'P' : 'F';
printf("分数 %d, 等级 %c\n", score, grade); // 输出: 分数 75, 等级 P
// 求最大值
int a = 10, b = 20;
int max = (a > b) ? a : b;
printf("最大值: %d\n", max); // 输出: 最大值: 20
// 求最小值
int min = (a < b) ? a : b;
printf("最小值: %d\n", min); // 输出: 最小值: 10
return 0;
}
|
5.8.2 嵌套条件运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| #include <stdio.h>
int main() {
int score = 85;
char grade;
// 嵌套:多个条件判断
grade = (score >= 90) ? 'A' :
(score >= 80) ? 'B' :
(score >= 70) ? 'C' :
(score >= 60) ? 'D' : 'F';
printf("分数 %d, 等级 %c\n", score, grade); // 输出: 分数 85, 等级 B
return 0;
}
|
嵌套警告:条件运算符可以嵌套,但不建议嵌套太深(超过 2-3 层可读性会变差)。如果条件太多,还是用 if-else 更清晰。
5.8.3 条件运算符的返回值特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <stdio.h>
int main() {
int a = 5, b = 10;
// 条件运算符的返回值可以被赋值
int max = (a > b) ? a : b;
printf("max = %d\n", max); // 输出: max = 10
// 条件运算符本身是表达式,不是语句,可以用在任何地方
printf("较大值是: %d\n", (a > b) ? a : b); // 输出: 较大值是: 10
return 0;
}
|
5.9 逗号运算符
逗号运算符用 , 分隔多个表达式,从左到右依次求值,整个逗号表达式的值是最后一个表达式的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #include <stdio.h>
int main() {
// 基本用法
int a = (1, 2, 3); // 只有最后一个值被赋给 a
printf("a = %d\n", a); // 输出: a = 3
// for 循环中的经典用法
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i = %d, j = %d\n", i, j);
}
// 输出:
// i = 0, j = 10
// i = 1, j = 9
// i = 2, j = 8
// i = 3, j = 7
// i = 4, j = 6
// 逗号运算符的价值:在一个地方做多个操作
int x = 10;
int y = 20;
int temp;
// 交换 x 和 y
temp = x, x = y, y = temp;
printf("x = %d, y = %d\n", x, y); // 输出: x = 20, y = 10
return 0;
}
|
注意:逗号运算符的优先级是所有运算符中最低的!所以 a = 1, 2, 3 会被解析为 (a = 1), 2, 3,而不是 a = (1, 2, 3)。如果想让逗号表达式作为一个整体,要加括号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #include <stdio.h>
int main() {
int a, b;
// 没有括号:从左到右依次执行,a = 1,然后 b = 2
a = 1, b = 2;
printf("a = %d, b = %d\n", a, b); // 输出: a = 1, b = 2
// 有括号:先计算逗号表达式 (1, 2),结果为 2,赋给 a
a = (1, 2, 3);
printf("a = %d\n", a); // 输出: a = 3
return 0;
}
|
5.10 运算符优先级与结合性
当一个表达式里有多个运算符时,该先算谁?这就是优先级(Precedence) 和结合性(Associativity) 要解决的问题。
5.10.1 优先级:谁先"插队"
1
2
3
4
5
6
7
8
9
10
11
12
13
| #include <stdio.h>
int main() {
// 乘除先于加减
int result = 2 + 3 * 4; // 先算 3*4=12,再算 2+12=14
printf("2 + 3 * 4 = %d\n", result); // 输出: 2 + 3 * 4 = 14
// 如果想先加:加括号!
result = (2 + 3) * 4; // 先算 2+3=5,再算 5*4=20
printf("(2 + 3) * 4 = %d\n", result); // 输出: (2 + 3) * 4 = 20
return 0;
}
|
5.10.2 结合性:当优先级相同时
当优先级相同时,看结合性——从左算到右,还是从右算到左?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <stdio.h>
int main() {
// 加减同优先级,从左到右结合
int result = 10 - 3 - 2; // (10 - 3) - 2 = 5
printf("10 - 3 - 2 = %d\n", result); // 输出: 10 - 3 - 2 = 5
// 赋值运算符从右到左结合
int a, b, c;
a = b = c = 10; // c = 10 -> b = c -> a = b
printf("a = %d, b = %d, c = %d\n", a, b, c); // 输出: a = 10, b = 10, c = 10
return 0;
}
|
5.10.3 完整优先级表(从高到低)
以下是 C 语言运算符的完整优先级表,记住常用的一些就够了:
| 优先级 | 运算符 | 结合性 | 说明 |
|---|
| 1 | () [] . -> | 从左到右 | 函数调用、数组下标、结构体成员 |
| 2 | ! ~ ++ -- + - * & sizeof | 从右到左 | 单目运算符(一元) |
| 3 | * / % | 从左到右 | 乘法、除法、取余 |
| 4 | + - | 从左到右 | 加法、减法 |
| 5 | << >> | 从左到右 | 位移 |
| 6 | < <= > >= | 从左到右 | 关系 |
| 7 | == != | 从左到右 | 相等 |
| 8 | & | 从左到右 | 按位与 |
| 9 | ^ | 从左到右 | 按位异或 |
| 10 | ` | ` | 从左到右 |
| 11 | && | 从左到右 | 逻辑与 |
| 12 | ` | | ` |
| 13 | ? : | 从右到左 | 条件运算符(三元) |
| 14 | = += -= *= /= %= &= ^= |= <<= >>= | 从右到左 | 赋值 |
| 15 | , | 从左到右 | 逗号 |
1
2
3
4
5
6
7
8
9
10
11
12
13
| #include <stdio.h>
int main() {
// 例子:逻辑与优先级高于逻辑或
int result = 1 || 0 && 0; // 等价于 1 || (0 && 0) = 1 || 0 = 1
printf("1 || 0 && 0 = %d\n", result); // 输出: 1 || 0 && 0 = 1
// 例子:关系运算优先级高于相等运算?不,我们来验证
result = 3 < 4 == 1; // (3 < 4) == 1 -> 1 == 1 -> 1
printf("3 < 4 == 1 = %d\n", result); // 输出: 3 < 4 == 1 = 1
return 0;
}
|
实战建议:不要背优先级表!当你对优先级有疑问时,加括号。括号是最清晰、最不容易出错的做法。代码是写给人看的,不是写给编译器看的——你自己几个月后看代码也看不懂 a = b + c * d << e 是什么意思。
5.11 表达式与语句的区别
这是很多初学者容易混淆的概念。
5.11.1 表达式(Expression)
表达式是由运算符和操作数组成的,有值的东西。
1
2
3
4
5
6
| // 这些都是表达式(有值):
5 + 3 // 值是 8
x = 5 // 值是 5(赋值表达式的值)
x > 5 // 值是 1(真)或 0(假)
a + b * c // 值取决于 a, b, c
printf("%d", x) // 值是打印的字符数(在这个例子中是 1)
|
5.11.2 语句(Statement)
语句是执行动作的指令,以分号 ; 结尾(或者用花括号 {} 包裹的复合语句)。
1
2
3
4
5
6
7
8
9
| // 这些都是语句:
x = 5; // 赋值语句
; // 空语句(啥也不干)
if (x > 0) {} // if 语句
while (x > 0) {} // while 语句
{ // 复合语句(代码块)
int y = 10;
y = y + x;
}
|
5.11.3 关键区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <stdio.h>
int main() {
int a = 5, b;
// a = 5 是一个表达式,有值(5),可以被使用
b = (a = 10); // 先把 10 赋给 a,表达式 a=10 的值是 10,再赋给 b
printf("a = %d, b = %d\n", a, b); // 输出: a = 10, b = 10
// if 后面需要的是一个表达式(条件)
if (b = 0) { // 错误!这是赋值,不是比较!b 被改成 0 了
printf("不会执行\n");
} else {
printf("b = %d(被改成 0 了!)\n", b); // 输出: b = 0(被改成 0 了!)
}
// if (b == 0) 才正确!
b = 0;
if (b == 0) {
printf("b 确实是 0\n"); // 输出: b 确实是 0
}
return 0;
}
|
一句话总结:表达式有值,语句执行动作。表达式可以嵌套在其他表达式里,语句不能。
5.12 左值与右值
5.12.1 左值(Lvalue):可以放在赋值左边的东西
左值表示一个存储位置(可以取地址),可以出现在赋值的左边或右边。
1
2
3
4
5
| int a = 5; // a 是左值:有存储空间,可以被赋值
a = 10; // 正确:左值在左边
int b = a; // 正确:左值也可以在右边(读取值)
// a + 3 = 5; // 错误!a + 3 不是左值,没有存储空间
|
5.12.2 右值(Rvalue):只能放在赋值右边的东西
右值是一个值(可以理解为临时的"过客"),不能被赋值。
1
2
| int a = 5;
int b = a + 3; // a + 3 是右值,是一个临时值,不能赋值给别的东西
|
5.12.3 左值可以当右值用,右值不能当左值用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <stdio.h>
int main() {
int a = 5; // a 是左值
int b = a; // a 作为右值使用(读取 a 的值),b 是左值
// const 修饰后变成"只读左值",不能赋值但可以读取
const int c = 10;
// c = 20; // 错误!不能给 const 变量赋值
printf("c = %d\n", c); // 正确:c 可以读取
// 数组名是左值,但array[i] 是左值
int arr[3] = {1, 2, 3};
arr[0] = 100; // 正确
// arr = NULL; // 错误!数组名不能被赋值
return 0;
}
|
生活比喻:左值就像一个有门牌号的房子,可以往里搬东西(赋值),也可以看看里面有什么(读取)。右值就像一个快递包裹,只能看看里面是什么,不能改变里面的东西(临时值,不能被赋值)。
5.13 序列点与未定义行为:i = i++ 的危险
这是 C 语言里最"阴险"的角落之一!
5.13.1 序列点(Sequence Point)
序列点是执行过程中的一个点,在此点之前的所有副作用(Side Effect)都必须已完成,而之后的副作用尚未开始。
C 语言中的序列点包括:
;(语句结束)&&、||、,(运算符)? :(条件运算符)- 函数调用(参数求值完毕之后,但函数执行之前)
5.13.2 未定义行为(Undefined Behavior)
如果你在同一个表达式中对同一个变量进行多次修改,且修改之间没有序列点分隔,结果不可预测!这就是未定义行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <stdio.h>
int main() {
int i = 1;
// i = i++; // 未定义行为!i 被修改两次,没有序列点分隔
// 编译器可能理解为:先赋值后自增,或先自增后赋值
// 不同编译器、不同优化级别可能产生不同结果!
// 正确写法:使用单独的语句
i = 1;
i++; // 先使用 i 的值,然后 i 自增
printf("i = %d\n", i); // 输出: i = 2
// 或者
i = 1;
++i; // 先自增,然后使用
printf("i = %d\n", i); // 输出: i = 2
return 0;
}
|
5.13.3 危险的例子们
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <stdio.h>
int main() {
int i = 1, j;
// 危险!这些都可能是未定义行为
// i = i++ + ++i; // i 被修改多次
// j = (i = 2) + (i = 3); // i 被修改多次
// printf 也可能有坑(参数求值顺序未指定)
// i = 1;
// printf("%d %d\n", i, i++); // 未定义!i 和 i++ 谁先求值?
// 安全的写法
i = 1;
j = i++; // j = 1, i = 2
printf("j = %d, i = %d\n", j, i); // 输出: j = 1, i = 2
return 0;
}
|
记住:C 语言的序列点很少。除了 ; 之外,最重要的就是 &&、||、, 和 ? :。如果一个变量在同一个表达式中被修改超过一次,且中间没有这些序列点,你的程序就是在走钢丝。
5.14 隐式转换详解:类型的"自动变形"
C 语言在进行运算时,如果两个操作数类型不同,会自动进行类型转换。这就像不同国家的货币在进行交易时,会自动按汇率换算一样。
整型提升是把 char、short、int 等较小的整数类型提升为 int 或 unsigned int 的过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <stdio.h>
int main() {
char a = 'A'; // 'A' 的 ASCII 码是 65
char b = 1;
char c = a + b; // 这里发生了整型提升!
// a 和 b 被提升为 int,计算后结果还是 int,再截断为 char
printf("'A' + 1 = '%c' (ASCII %d)\n", c, c);
// 输出: 'A' + 1 = 'B' (ASCII 66)
// 整型提升的规则:
// char、signed char、unsigned char -> int(如果 int 能表示所有值)
// 或 -> unsigned int(否则)
// short、unsigned short 同理
return 0;
}
|
为什么要整型提升?
因为 CPU 通常对 int 类型的运算最"顺手"。把小的类型提升为 int 再运算,效率更高。
5.14.2 寻常算术转换(Usual Arithmetic Conversions)
当运算符的两个操作数类型不同时,C 语言会使用寻常算术转换来统一类型。这个规则就像一个"升级金字塔":
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| #include <stdio.h>
int main() {
// 金字塔(从低到高):
// long double
// ^
// double
// ^
// float
// ^
// unsigned long long
// ^
// long long
// ^
// unsigned long
// ^
// long
// ^
// unsigned int
// ^
// int (底部)
int a = 5;
double b = 2.0;
double result = a / b; // a 被提升为 double,5.0 / 2.0 = 2.5
printf("a / b = %f\n", result); // 输出: a / b = 2.500000
// 如果都是 int,结果是 int
int x = 5, y = 2;
printf("x / y = %d\n", x / y); // 输出: x / y = 2
// 有符号数和无符号数的混合
int m = -10;
unsigned int n = 5;
// m 自动转换为 unsigned int,结果是巨大的正数!
printf("m + n = %u\n", m + n); // 输出: m + n = 4294967291
return 0;
}
|
警告:有符号数和无符号数混用是危险的陷阱!
int m = -10; unsigned int n = 5; 的结果是 m + n = 4294967291(一个巨大的正数)。
原因:m 被转换成 unsigned int,-10 的补码 0xFFFFFFF6 被直接当作无符号数解释,就是 4294967286 + 5 = 4294967291。
建议:尽量避免有符号和无符号数混合运算。如果必须,谨慎检查。
这是函数调用时自动发生的隐式转换,主要针对可变参数函数(如 printf)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <stdio.h>
int main() {
// printf 的整型提升:
// char、short 自动提升为 int
// float 自动提升为 double
char c = 'A';
printf("char as %%d: %d\n", c); // char 提升为 int
// 输出: char as %d: 65
short s = 100;
printf("short as %%d: %d\n", s); // short 提升为 int
// 输出: short as %d: 100
float f = 3.14f;
printf("float as %%f: %f\n", f); // float 提升为 double
// 输出: float as %f: 3.140000
return 0;
}
|
为什么 printf 需要特殊处理?
因为 printf 是可变参数函数,不知道参数的具体类型。printf 靠格式说明符(如 %d、%f)来"猜测"参数类型。所以你必须确保格式说明符和实际参数类型匹配,否则会出错!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| #include <stdio.h>
int main() {
int i = 65;
char c = 'A';
// 正确匹配
printf("int: %d, char: %c\n", i, c); // 输出: int: 65, char: A
// 错误匹配(但很多编译器不会报错,只会警告)
// printf("char as int: %d\n", c); // 正确:char 可以用 %d
// printf("int as char: %c\n", i); // 正确:int 可以用 %c
printf("int as char: %c\n", i); // 输出: int as char: A
return 0;
}
|
本章小结
本章我们学习了 C 语言的运算符与表达式,这是让程序真正"运转"起来的关键。核心知识点包括:
算术运算符:+ - * / %,其中除法分为整数除法和浮点除法,负数除法在 C99 后规定向零截断
关系运算符:> < >= <=,用于比较大小,结果为 1(真)或 0(假)
相等运算符:== !=,注意 == 和 = 的区别(比较 vs 赋值)
逻辑运算符:&& || !,支持短路求值,性能优化的利器
位运算符:& | ^ ~ << >>,直接操作二进制位,是底层编程的基础
赋值运算符:= 及复合赋值 += -= 等,从右到左结合
自增自减:++i 先自增再返回值,i++ 先返回值再自增,两者单独使用时效果相同
条件运算符:? : 是 C 唯一的三元运算符,格式简洁但注意可读性
逗号运算符:从左到右求值,整个表达式值为最后一个表达式的值
优先级与结合性:必要时加括号,不要依赖记忆
表达式 vs 语句:表达式有值,语句执行动作
左值 vs 右值:左值有存储位置,右值是临时值
序列点与未定义行为:同一个变量多次修改且无序列点分隔是危险的
隐式转换:整型提升、寻常算术转换、默认实参提升,理解类型转换规则避免踩坑
“运算符是 C 语言的灵魂,表达式是 C 语言的诗歌。学会它们,你就学会了和计算机对话的艺术!”