第 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 的补码:1111101111111010 + 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 = 0a ^ 0 = a

交换的原理:x = x ^ yy = (x ^ y) ^ y = xx = (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;
}

为什么要用复合赋值?

  1. 省手指:少敲几个字
  2. 表达更清晰x += 5 明确表示"增加 5",比 x = x + 5 更直观
  3. 编译器优化:某些情况下 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 节会详细讲)。记住一条:如果不需要返回值,只想自增,用 ++ii++ 都可以;如果需要返回值,慎重选择!


5.8 条件运算符:? : —— C 语言的三元表达式

条件运算符是 C 语言唯一的三元运算符(一口气吃三个操作数),格式为:

1
条件 ?1 :2

如果条件为真,整个表达式的值是值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 语言在进行运算时,如果两个操作数类型不同,会自动进行类型转换。这就像不同国家的货币在进行交易时,会自动按汇率换算一样。

5.14.1 整型提升(Integer Promotion)

整型提升是把 charshortint 等较小的整数类型提升为 intunsigned 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

建议:尽量避免有符号和无符号数混合运算。如果必须,谨慎检查。

5.14.3 默认实参提升(Default Argument Promotions)

这是函数调用时自动发生的隐式转换,主要针对可变参数函数(如 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 语言的运算符与表达式,这是让程序真正"运转"起来的关键。核心知识点包括:

  1. 算术运算符+ - * / %,其中除法分为整数除法和浮点除法,负数除法在 C99 后规定向零截断

  2. 关系运算符> < >= <=,用于比较大小,结果为 1(真)或 0(假)

  3. 相等运算符== !=,注意 === 的区别(比较 vs 赋值)

  4. 逻辑运算符&& || !,支持短路求值,性能优化的利器

  5. 位运算符& | ^ ~ << >>,直接操作二进制位,是底层编程的基础

  6. 赋值运算符= 及复合赋值 += -= 等,从右到左结合

  7. 自增自减++i 先自增再返回值,i++ 先返回值再自增,两者单独使用时效果相同

  8. 条件运算符? : 是 C 唯一的三元运算符,格式简洁但注意可读性

  9. 逗号运算符:从左到右求值,整个表达式值为最后一个表达式的值

  10. 优先级与结合性:必要时加括号,不要依赖记忆

  11. 表达式 vs 语句:表达式有值,语句执行动作

  12. 左值 vs 右值:左值有存储位置,右值是临时值

  13. 序列点与未定义行为:同一个变量多次修改且无序列点分隔是危险的

  14. 隐式转换:整型提升、寻常算术转换、默认实参提升,理解类型转换规则避免踩坑

“运算符是 C 语言的灵魂,表达式是 C 语言的诗歌。学会它们,你就学会了和计算机对话的艺术!”

最后修改 March 29, 2026: 新增 C 教程 (93a26d7)