前面两篇文章,我们学会了怎么在 C 语言里存储数据——用 int 存整数,用 char 存字符,用 float 和 double 存小数——也学会了用 printf 输出、用 scanf 输入、用各种运算符做计算。但你有没有发现一个问题:到目前为止,我们写的所有程序都是"一条路走到黑"的。从上往下,一行一行执行,没有任何"决策"。

真正的程序显然不能这样。一个登录系统,密码对了才能进去,错了就得提示;一个计算器,用户选加法就做加法,选减法就做减法;一个游戏,角色血量掉到 0 就 Game Over,否则继续战斗。程序需要根据条件做出不同反应——这就是"流程控制"的意义。

C 语言是结构化的程序设计语言,支持三种基本结构:顺序结构、选择结构(分支)、循环结构。这三种结构你每天都在用——"如果下雨就带伞"是选择,"每天早上跑步 30 分钟"是循环。这篇文章我们集中攻坚选择结构:if-else 语句、关系运算符、条件运算符、逻辑运算符(&&、||、!)和 switch 语句。

在开始之前,有一个贯穿全文的规则需要记住:C 语言中,0 表示"假",非 0 表示"真"。这和 Python 的 True/False、Java 的 boolean 都不一样。C 语言在条件判断时,表达式结果为 0 就不执行,非 0 就执行。关系表达式和逻辑表达式的计算结果也通常是 0(假)或 1(真)。比如 3 > 2 的结果是 1,2 > 3 的结果是 0。这个"0/非0"的规则,是 C 语言贴近硬件的设计选择——CPU 判断条件靠的就是零标志位(Zero Flag)。CPU 执行比较指令后,结果为零就置位零标志位,C 语言编译器把这种硬件行为直接映射成了 if 的判断逻辑。

if 语句——程序的第一个"决策点"

if 是 C 语言中最基本的分支语句,语法和英语里的"if"一样直觉:

if (表达式)
    语句;

当表达式为真(非 0)时,执行后面的语句;为假(0)时,跳过不执行。

来看第一个例子——判断一个数是不是奇数:

#include <stdio.h>
 
int main()
{
    int num = 0;
    scanf("%d", &num);          /* 从键盘读入一个整数 */
 
    if (num % 2 == 1)           /* 如果 num 除以 2 余 1,说明是奇数 */
        printf("%d 是奇数\n", num);
    /* 如果 num%2 不等于 1(即 num 是偶数),这条 printf 不会执行 */
 
    return 0;
}

这里 num % 2 == 1 就是条件表达式。注意是两个等号 ==,不是 =。这个区别非常关键——我们稍后会讲。

光有 if 只能做"满足条件就做某事",如果还想处理"不满足条件"的情况呢?那就需要 else——二选一,必走其一:

#include <stdio.h>
 
int main()
{
    int num = 0;
    scanf("%d", &num);
 
    if (num % 2 == 1)
        printf("%d 是奇数\n", num);   /* 余数为 1,走这里 */
    else
        printf("%d 是偶数\n", num);   /* 余数为 0,走这里 */
 
    return 0;
}

再如判断是否成年:

#include <stdio.h>
 
int main()
{
    int age = 0;
    scanf("%d", &age);
 
    if (age >= 18)
        printf("成年\n");
    else
        printf("未成年\n");
 
    return 0;
}

if 的另一种玩法:直接判断非 0 值

因为"非 0 即真",所以条件不一定是比较表达式,任何表达式都可以:

#include <stdio.h>
 
int main()
{
    int flag = 0;
    scanf("%d", &flag);
 
    if (flag)              /* 等价于 if (flag != 0) */
        printf("flag 非 0,为真\n");
    else
        printf("flag 是 0,为假\n");
 
    if (!flag)             /* 等价于 if (flag == 0) */
        printf("flag 为 0\n");
 
    return 0;
}

这种写法在检查"是否读到文件末尾"(EOF 是负数,非 0 即真)、"函数是否调用成功"(返回 0 表示失败时用 !)时非常常见。但注意:初学者最好显式写 flag != 0,等熟练了再偷懒,否则容易把"判断值"和"判断真假"搞混。

if 控制多条语句——大括号不能省

默认情况下,if 和 else 只能控制紧跟在后面的一条语句。这是新手最容易踩的坑之一。如果你想控制多条语句,必须用大括号 {} 括起来:

#include <stdio.h>
 
int main()
{
    int age = 0;
    scanf("%d", &age);
 
    /* 错误写法:不管 age 是多少,"可以谈恋爱了"都会打印 */
    /* 因为 if 只控制 printf("成年了\n") 这一条语句 */
    if (age >= 18)
        printf("成年了\n");
        printf("可以谈恋爱了\n");  /* 这条不受 if 控制! */
 
    return 0;
}

正确写法:

#include <stdio.h>
 
int main()
{
    int age = 0;
    scanf("%d", &age);
 
    if (age >= 18)
    {
        printf("成年了\n");
        printf("可以谈恋爱了\n");   /* 两条都在 if 的控制下 */
    }
    else
    {
        printf("未成年\n");
        printf("不可以早恋哦\n");
    }
 
    return 0;
}

大括号括起来的代码块叫复合语句。一个好的编码习惯是:哪怕 if 后面只有一条语句,也加上大括号。这样以后加代码不会出问题,代码逻辑也更清晰。很多大公司(比如 Linux 内核)的代码规范也强制要求 if 必须带大括号——这不是形式主义,而是用规则消灭一整类 bug。

if 条件里常见的中文符号坑

写 if 条件时还有一个容易被忽略的坑:条件里的比较符号。全角 >、<、= 和半角 >、<、== 在屏幕上看起来几乎一样,但编译器只认半角。如果复制了网上的全角符号,编译直接报错。中英文标点混用是新手编译报错的高发原因,遇到莫名其妙的"语法错误",先检查标点。

嵌套 if——多条件逐层判断

现实中的判断往往不是简单的"是或否"。比如判断一个数是正数、负数还是零——这是三路分支:

#include <stdio.h>
 
int main()
{
    int num = 0;
    scanf("%d", &num);
 
    if (num == 0)
        printf("输入的数字是 0\n");
    else if (num > 0)   /* else if 是嵌套在 else 里的 if */
        printf("输入的数字是正数\n");
    else
        printf("输入的数字是负数\n");
 
    return 0;
}

else if 的本质就是"在 else 里面再放一个 if"。你可以无限嵌套,形成多路分支。

再看一个根据年龄判断人生阶段的例子:

#include <stdio.h>
 
int main()
{
    int age = 0;
    scanf("%d", &age);
 
    if (age < 18)
        printf("少年\n");
    else if (age <= 44)
        printf("青年\n");
    else if (age <= 59)
        printf("中老年\n");
    else if (age <= 89)
        printf("老年\n");
    else
        printf("老寿星\n");
 
    return 0;
}

注意这个逻辑:条件从严格到宽松排列。else if 按顺序判断,命中第一个为真的条件后就跳过其余的。如果你把 age <= 89 放在 age <= 44 前面,逻辑就乱了——44 岁的人会被判为"老年"。

这是 else-if 链条最重要的设计原则:条件的顺序就是判断的优先级,宽泛的条件放在后面。类比现实:先问"是不是学生",再问"是不是研究生"——如果先问"是不是学生"就把研究生也圈进去了,后面那个判断就永远执行不到。

嵌套也可以放在 if 内部——比如先判断是不是正数,再判断是奇数还是偶数:

#include <stdio.h>
 
int main()
{
    int num = 0;
    scanf("%d", &num);
 
    if (num > 0)
    {
        if (num % 2 == 0)       /* 嵌套在 if(num>0) 内部 */
            printf("正偶数\n");
        else
            printf("正奇数\n");
    }
    else
    {
        printf("非正数\n");
    }
 
    return 0;
}

嵌套的层级越多,代码越难读。如果嵌套超过三层,就要考虑重构——要么用 else if 拉平,要么用 switch,要么把内层判断抽成函数。代码是写给"下一个读它的人"看的(那个人很可能就是三个月后的你自己)。

悬空 else——新手最容易掉的坑

看下面这段代码,你觉得会输出什么?

#include <stdio.h>
 
int main()
{
    int a = 0;
    int b = 2;
 
    if (a == 1)
        if (b == 2)
            printf("hehe\n");
    else
        printf("haha\n");
 
    return 0;
}

很多新手会想:"a 等于 0,不等于 1,所以走 else,输出 haha。"

错!这段代码什么都不输出。

原因在于:else 总是和离它最近的那个未配对的 if 匹配。上面的代码中,else 看起来和第一个 if 对齐,但它实际上和第二个 if (b == 2) 配对。所以这段代码等价于:

if (a == 1)
{
    if (b == 2)
        printf("hehe\n");
    else
        printf("haha\n");
}

因为 a == 1 为假,整个大括号里的代码都不执行——自然什么都没输出。

这就是悬空 else 问题。它的根源是:C 语言的语法只规定了"else 匹配最近的 if",而缩进只是给人看的,编译器根本不看缩进。解决办法很简单:始终用大括号明确表达你的意图,不要依赖缩进。如果你想让 else 和第一个 if 配对:

if (a == 1)
{
    if (b == 2)
        printf("hehe\n");
}
else           /* 现在 else 明确和第一个 if 配对 */
{
    printf("haha\n");
}

这个坑在面试题里出现的频率极高,考试/笔试时看到"多层 if 嵌套 + 缩进误导"的题目,第一反应就是检查 else 的配对关系。

关系操作符——比较大小的六种方式

C 语言提供了六个关系运算符:

运算符含义示例
>大于a > b
<小于a < b
>=大于等于a >= b
<=小于等于a <= b
==等于a == b
!=不等于a != b

两个需要特别注意的点:

第一,==(等于)和 =(赋值)不要搞混。 if (a = 3) 是赋值——把 3 赋给 a,然后判断 a 是否为真(3 非 0,永远为真)。if (a == 3) 才是判断相等。一个有经验的防错技巧是把常量写在左边:if (3 == a)——如果误写为 if (3 = a),编译器会直接报错,因为不能给常量赋值。

为什么 if (a = 3) 能编译通过?因为 C 语言中赋值表达式本身有值(就是赋进去的值),所以 a = 3 这个表达式的值是 3,非 0 即真。C 语言语法上允许这种写法,是历史遗留——有些老代码故意用它(比如 if ((c = getchar()) != EOF) 这种"一边赋值一边判断"的惯用法),但对初学者来说,它 99% 是笔误。编译器通常会给出警告("在条件表达式中使用赋值"),把警告当成错误来对待,能帮你拦住大部分笔误。

第二,多个关系运算符不能连用。 i < j < k 在数学里表示"j 在 i 和 k 之间",但在 C 语言里完全不是这个意思。实际执行顺序是 (i < j) < k——先算 i < j 得到 0 或 1,再用这个 0 或 1 和 k 比较。来看一个经典错误:

#include <stdio.h>
 
int main()
{
    int age = 0;
    scanf("%d", &age);
 
    /* 错误写法:18 <= age <= 36,这个表达式永远为真! */
    if (18 <= age <= 36)    /* 输入 10 也会打印"青年" */
        printf("青年\n");
 
    return 0;
}

你输入 10,程序会输出"青年"。因为 18 <= 10 为假(结果是 0),然后 0 <= 36 为真——所以你输入任何数都会进入 if。

正确写法是用逻辑与 && 连接两个条件:

if (age >= 18 && age <= 36)   /* 两个条件必须同时满足 */
    printf("青年\n");

关系运算符的优先级

关系运算符的优先级低于算术运算符,高于赋值运算符:

int result = 3 + 4 > 5;   /* 先算 3+4=7,再算 7>5,result = 1 */
int r2 = a < b == c < d;  /* 实际是 (a<b) == (c<d):先算两个比较,再比较两个结果 */

== 和 != 的优先级低于 >、<、>=、<=(即等于号更"松散")。这个细节在复杂表达式里会影响结果,建议记不住就加括号。

条件操作符——if-else 的简写版

当 if-else 的逻辑很简单(只是根据条件选一个值)时,可以用条件操作符来简化:

表达式1 ? 表达式2 : 表达式3

如果表达式 1 为真,整个表达式的结果是表达式 2;否则是表达式 3。它需要三个操作数,所以也叫三目操作符。

对比两种写法——找出两个数中的较大值:

#include <stdio.h>
 
int main()
{
    int a = 0, b = 0;
    scanf("%d %d", &a, &b);
 
    /* if-else 写法 */
    int m;
    if (a > b)
        m = a;
    else
        m = b;
 
    /* 条件操作符写法,一行搞定 */
    int n = a > b ? a : b;
 
    printf("较大值: %d (if写法), %d (三目写法)\n", m, n);
 
    return 0;
}

再如根据条件赋不同值:

int b = a > 5 ? 3 : -3;  /* 如果 a>5,b=3,否则 b=-3 */

条件操作符可以嵌套,但不建议嵌套太深——可读性会急剧下降。一次嵌套还可以接受(比如 a > b ? (a > c ? a : c) : (b > c ? b : c) 找三个数的最大值),两层以上的嵌套就建议拆成 if-else。

条件操作符的易错点

  1. 它仍然是表达式,有值——可以直接用在 printf 里:printf("%s", a > b ? "大" : "小");
  2. 优先级低于大多数运算符——a + b > c ? d : e 实际是 (a + b > c) ? d : e,但为了可读性请加括号
  3. 三个操作数的类型可能不同——a > 0 ? 3 : 3.14 的结果类型是 double(int 自动转成 double)
  4. 嵌套三目是可读性杀手——看到 ?:?:?: 连写,请拆开

逻辑操作符——组合多个条件

现实世界的判断往往是多个条件组合在一起。C 语言提供了三个逻辑运算符:

运算符含义说明
!逻辑非(取反)真变假,假变真
&&逻辑与(并且)两个都为真,结果才为真
||逻辑或(或者)至少一个为真,结果就为真

逻辑非 !

#include <stdio.h>
 
int main()
{
    int flag = 0;             /* 0 表示假 */
    if (!flag)                /* !0 = 1(真),条件成立 */
        printf("flag 是假的,执行这里\n");
 
    flag = 5;                 /* 非 0 表示真 */
    if (!flag)                /* !5 = 0(假),条件不成立 */
        printf("这行不会打印\n");
 
    return 0;
}

! 的运算结果只有 0 或 1:!0 = 1,!任何非0值 = 0。它和位取反 ~ 完全不同——~ 是按位翻转(每一位都反过来),! 是逻辑取反(整体真假翻转)。以后学位运算时要区分清楚。

逻辑与 &&

判断月份是否在 3 到 5 月之间(春天):

#include <stdio.h>
 
int main()
{
    int month = 0;
    scanf("%d", &month);
 
    if (month >= 3 && month <= 5)
        printf("春季\n");
    else
        printf("不是春季\n");
 
    return 0;
}

逻辑或 ||

判断月份是否是冬季(12 月、1 月或 2 月):

#include <stdio.h>
 
int main()
{
    int month = 0;
    scanf("%d", &month);
 
    if (month == 12 || month == 1 || month == 2)
        printf("冬季\n");
    else
        printf("不是冬季\n");
 
    return 0;
}

注意 || 连接的每个条件都必须写完整——month == 1 || 2 是错误的写法,它实际是 (month == 1) || 2,而 2 恒为真,整个表达式恒真。这个错误非常隐蔽,面试题常考。

实战:判断闰年

闰年的规则是:(1) 能被 4 整除但不能被 100 整除,或者 (2) 能被 400 整除。用逻辑运算符组合:

#include <stdio.h>
 
int main()
{
    int year = 0;
    scanf("%d", &year);
 
    /* 用逻辑运算符组合两个条件 */
    if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
        printf("%d 是闰年\n", year);
    else
        printf("%d 不是闰年\n", year);
 
    return 0;
}

分析一下这个表达式:year % 4 == 0 && year % 100 != 0 是"能被 4 整除且不能被 100 整除",year % 400 == 0 是"能被 400 整除",两者用 || 连接。外层括号不能省——否则 && 的优先级高于 ||,会先算 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)……等等,其实不加括号结果也一样,因为 && 优先级本来就高于 ||。但写上括号是在告诉读代码的人"这是一个整体",这比编译器需不需要更重要。

实战:判断一个字符

结合上一篇的 ASCII 知识,判断输入的字符是字母、数字还是其他:

#include <stdio.h>
 
int main()
{
    char ch = 0;
    scanf("%c", &ch);
 
    if (ch >= 'A' && ch <= 'Z')
        printf("大写字母\n");
    else if (ch >= 'a' && ch <= 'z')
        printf("小写字母\n");
    else if (ch >= '0' && ch <= '9')
        printf("数字字符\n");
    else
        printf("其他字符\n");
 
    return 0;
}

注意 ch >= 'A' && ch <= 'Z' 这种写法依赖 ASCII 码的连续性——'A''Z' 是 6590 连续编码,所以区间判断有效。这种"字符区间判断"在输入校验里非常常用。

短路求值——逻辑运算符的"偷懒"机制

C 语言的逻辑运算符有一个重要特性:从左到右求值,一旦能确定最终结果,就不再计算右边的表达式。这叫"短路求值"。

  • 对于 &&:如果左边为假(0),整个表达式必为假,右边不计算
  • 对于 ||:如果左边为真(非 0),整个表达式必为真,右边不计算

来看一个能体现短路效果的例子:

#include <stdio.h>
 
int main()
{
    int i = 0, a = 0, b = 2, c = 3, d = 4;
 
    i = a++ && ++b && d++;
    /* a++ 返回 0(假),发生短路!
     * ++b 和 d++ 都不会执行!
     * 最终:a=1, b=2(未变), c=3, d=4(未变) */
 
    printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d);
    /* 输出:a=1, b=2, c=3, d=4 */
 
    /* 换成 || 再看看 */
    i = 0;
    a = 0;
    i = a++ || ++b || d++;
    /* a++ 返回 0(假),不短路,继续计算 ++b
     * ++b 返回 3(真),发生短路!d++ 不执行
     * 最终:a=1, b=3, d=4 */
 
    printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d);
    /* 输出:a=1, b=3, c=3, d=4 */
 
    return 0;
}

短路求值不只是性能优化——它还能用来写更安全的代码。比如先判断指针是否为 NULL 再访问:

if (ptr != NULL && ptr->value > 0)  /* 如果 ptr 是 NULL,右边的访问不会执行 */
    printf("%d\n", ptr->value);

如果 ptr 是 NULL,ptr != NULL 为假,短路发生,ptr->value 不会被计算——避免了空指针崩溃。但反过来也要注意:不要在逻辑表达式中夹带"必须执行"的副作用操作。比如 if (a > 0 && b++ > 0)——如果 a > 0 为假,b++ 就不会执行,这可能有违你的预期。

这条"副作用警告"在实际代码里最常见的翻车场景是:while (n != 0 && n % 2 == 0) { ... } 中有人把 n-- 写进条件里,结果循环次数和预期完全对不上。条件里只放"判断",不放"修改",这是一条值得遵守的纪律。

逻辑运算符与位运算符的区别

初学者经常把 && 和 &、|| 和 | 搞混。它们的区别是本质性的:

  • && 是逻辑与:先看真/假,结果是 0 或 1,有短路
  • & 是位与:对两个整数的每一位做 AND,结果是一个整数,无短路
int a = 3;    /* 二进制 0011 */
int b = 5;    /* 二进制 0101 */
 
int r1 = a && b;   /* 3 和 5 都是真,结果为 1 */
int r2 = a & b;    /* 0011 & 0101 = 0001 = 1(巧合的是这里都等于1) */
 
/* 换个例子就能看出差别 */
int c = 2;    /* 二进制 0010 */
printf("%d\n", c && 4);   /* 1:两个都非0,逻辑结果为真 */
printf("%d\n", c & 4);    /* 0:0010 & 0100 = 0000 */

记法:&& 比 & 长,长的那对是"逻辑版"(返回 0/1,短路);短的那对是"位操作版"(按位运算)。还有 ! 和 ~ 的区别类似——! 逻辑取反返回 0/1,~ 按位取反翻转所有位。

操作符优先级总表(含分支相关)

前面零散提到了优先级,这里给出一张总表(从高到低),方便对照:

优先级操作符说明
1(最高)() [] -> .括号、下标、成员访问
2! ~ ++ -- -(负号)(类型) *(解引用)&(取地址)单目操作符
3* / %乘除取余
4+ -加减
5<< >>移位
6< <= > >=关系(比较)
7== !=相等性
8&按位与
9^按位异或
10|按位或
11&&逻辑与
12||逻辑或
13?:条件(三目)
14(最低)= += -= 等赋值

这张表的记忆要点:

  • 单目 > 算术 > 移位 > 关系 > 位运算 > 逻辑 > 三目 > 赋值,整体呈"从紧到松"
  • && 高于 ||——所以 a || b && c 等于 a || (b && c)
  • 三目和赋值都是右结合(从右往左算)
  • 拿不准就加括号,括号永远合法

switch 语句——多路分支的更优选择

当需要判断的条件值有很多种可能时,if-else if-else 写一长串又丑又难读。switch 语句就是为这种场景设计的:

switch (表达式)
{
    case 值1: 语句1;
    case 值2: 语句2;
    ...
    default:  默认语句;
}

注意两个限制:(1) switch 后的表达式必须是整型表达式——浮点数和字符串不能用在 switch 里;(2) case 后的值必须是整型常量表达式(不能是变量)。

为什么不能是浮点数或字符串?因为 switch 的底层实现是"跳转表"或"逐值比较",而浮点数的相等比较本身就不可靠(上一篇刚讲过 0.1+0.2 的问题),字符串比较在 C 里又是一个复杂话题(strcmp)。所以 C 语言把 switch 限定在整型世界——这也是"什么场景用什么结构"的典型例子。

来看一个对比——根据 n % 3 的余数输出信息。if-else 版本:

#include <stdio.h>
 
int main()
{
    int n = 0;
    scanf("%d", &n);
 
    /* if-else 写法 */
    if (n % 3 == 0)
        printf("整除,余数为 0\n");
    else if (n % 3 == 1)
        printf("余数是 1\n");
    else
        printf("余数是 2\n");
 
    return 0;
}

用 switch 改写:

#include <stdio.h>
 
int main()
{
    int n = 0;
    scanf("%d", &n);
 
    switch (n % 3)
    {
        case 0:
            printf("整除,余数为 0\n");
            break;           /* 跳出 switch,必不可少! */
        case 1:
            printf("余数是 1\n");
            break;
        case 2:
            printf("余数是 2\n");
            break;
    }
 
    return 0;
}

switch 的执行机制——从入口标签开始

理解 switch 的关键在于:case 不是"独立的块",而是"入口标签"。程序计算出表达式值之后,会直接跳到匹配的 case 那一行开始执行——然后一直往下走,直到遇到 break 或 switch 结束。这解释了为什么漏掉 break 会"穿透"。

break 在 switch 中的作用

switch 中的 case 只是一个"入口标签",不是独立的分支。如果没有 break,代码会**穿透(fall-through)**到下一个 case 继续执行。

去掉 break 看看效果:

#include <stdio.h>
 
int main()
{
    int n = 7;     /* 7 % 3 = 1,按说只走 case 1 */
 
    switch (n % 3)
    {
        case 0:
            printf("整除,余数为 0\n");
            /* 没有 break!会继续往下走 */
        case 1:
            printf("余数是 1\n");
            /* 没有 break!会继续往下走 */
        case 2:
            printf("余数是 2\n");
            /* 所有 case 都被执行了! */
    }
 
    return 0;
}
/* 输出:
 * 余数是 1
 * 余数是 2
 * ——"穿透"了!*/

实际开发中,可以利用"穿透"来让多个 case 共享同一段代码——比如工作日和休息日的判断:

#include <stdio.h>
 
int main()
{
    int day = 0;
    scanf("%d", &day);
 
    switch (day)
    {
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
            printf("工作日\n");
            break;       /* 1~5 都走这里,然后跳出 */
        case 6:
        case 7:
            printf("休息日\n");
            break;       /* 6、7 都走这里,然后跳出 */
        default:
            printf("输入错误\n");
            break;
    }
 
    return 0;
}

这种"多个 case 共享执行体"的模式在实际开发中很常见——代码简洁、逻辑清晰。

注意:这种"故意穿透"是有意为之,必须在注释里写明白(比如 /* fall through */),否则读代码的人会以为你漏了 break。编译器(比如 GCC 的 -Wimplicit-fallthrough)也提供了检查,帮助区分"故意穿透"和"忘记 break"。

default 子句——处理"意料之外"的值

当 switch 的表达式的值和所有 case 都不匹配时,程序会跳到 default。它不一定要放在最后——可以放在任意位置——但惯例是放在末尾:

#include <stdio.h>
 
int main()
{
    int day = 0;
    scanf("%d", &day);
 
    switch (day)
    {
        case 1: printf("星期一\n"); break;
        case 2: printf("星期二\n"); break;
        case 3: printf("星期三\n"); break;
        case 4: printf("星期四\n"); break;
        case 5: printf("星期五\n"); break;
        case 6: printf("星期六\n"); break;
        case 7: printf("星期天\n"); break;
        default:
            printf("输入错误,请输入 1~7\n");
            break;
    }
 
    return 0;
}

养成写 default 的习惯——即使你认为所有情况都已覆盖。default 是程序的"兜底网":万一用户输入了意外值,程序至少有明确的响应,而不是静默地什么都不做。很多健壮性就是这么一点点堆出来的。

switch 里 case 后的大括号

如果某个 case 里的逻辑比较复杂,需要定义局部变量,必须加大括号,否则编译器报错("case 后不能直接定义变量"或跳过了初始化):

switch (op)
{
    case 1:
    {
        int temp = 100;      /* 需要用大括号包住才能定义变量 */
        printf("%d\n", temp);
        break;
    }
    case 2:
        printf("无需变量\n");
        break;
}

实战:简单计算器

下面是一个综合练习——根据用户输入的运算符执行对应的运算,把今天学的全部串联起来:

#include <stdio.h>
 
int main()
{
    double num1 = 0.0, num2 = 0.0;  /* 两个操作数 */
    char op = 0;                      /* 运算符 */
 
    printf("请输入算式(如 3+5):");
    scanf("%lf %c %lf", &num1, &op, &num2);
    /* %lf 读取 double,%c 读取字符 */
 
    switch (op)
    {
        case '+':
            printf("%.2lf + %.2lf = %.2lf\n",
                   num1, num2, num1 + num2);
            break;
 
        case '-':
            printf("%.2lf - %.2lf = %.2lf\n",
                   num1, num2, num1 - num2);
            break;
 
        case '*':
            printf("%.2lf * %.2lf = %.2lf\n",
                   num1, num2, num1 * num2);
            break;
 
        case '/':
            if (num2 != 0.0)           /* 防止除零错误 */
                printf("%.2lf / %.2lf = %.2lf\n",
                       num1, num2, num1 / num2);
            else
                printf("错误:除数不能为 0!\n");
            break;
 
        default:
            printf("不支持的运算符:%c\n", op);
            printf("支持的运算符:+ - * /\n");
            break;
    }
 
    return 0;
}

这个计算器虽然只有几十行,但用到了 switch、case、break、default,以及除法前判断除数的防御性编程——你已经有能力独立写出这样的程序了。

注意 scanf("%lf %c %lf", ...) 里 %c 前面的空格:它跳过了 3 和 + 之间的空格。如果写成 scanf("%lf%c%lf", ...),输入 3 + 5(带空格)时 %c 会读到空格而不是 +——这就是上一篇讲的 %c 不跳过空白字符的坑,在真实程序里立刻就会咬你一口。

if-else if 与 switch 的选择

关于 if-else if 和 switch 的选择,一个简单的原则:当判断同一个变量的不同取值时用 switch(如星期几、菜单选项),当条件涉及范围判断或不同变量时用 if-else(如温度区间、组合条件)。两种都能实现分支,选更清晰的那个。

补充一个细节:switch 在条件分支很多时,编译器可能生成"跳转表"(直接算出跳转位置),执行效率比一串 if-else 的逐个比较更高。这是 switch 的隐藏优势——不过对现在的你来说,可读性才是第一位的。


本篇思考题

  1. 判断下面代码的输出,并解释原因:int a = 1, b = 2, c = 3; if (a = b) printf("%d", c);(注意 a = b 是赋值!)
  2. 为什么 if (18 <= age <= 36) 恒为真?正确的写法是什么?
  3. 悬空 else 问题的根源是什么?用什么编码习惯可以一劳永逸地避开它?
  4. int x = 5, y = 0; if (x || y++) 执行后 y 的值是多少?如果把 || 换成 | 呢?
  5. switch 里忘记写 break 会发生什么?"故意穿透"的正确姿势是什么?
  6. 用 if-else if 和 switch 各写一个判断 1~7 对应星期几的程序,对比两种写法的优劣。
  7. 验证:int a = 3, b = 4, c = 5; 计算 a < b && b < c、a < b || c < b、!(a > b) 的结果分别是多少?

思考题参考答案与详解

第 1 题:输出 3。if (a = b) 是赋值而不是比较:它把 b 的值(2,即上面声明的 b = 2)赋给 a,a 变成 2,并且这个赋值表达式本身的值就是 2(非 0 即真),所以条件成立,执行 printf("%d", c),输出 c 的值 3。想判断相等应写 a == b。

第 2 题:因为 18 <= age <= 36 会被拆成两步:先算 (18 <= age),得到 0 或 1;再用这个 0 或 1 和 36 比较——无论结果是 0 还是 1,都比 36 小,条件恒为真。正确写法是用逻辑与连接两个独立条件:age >= 18 && age <= 36。

第 3 题:根源是 C 语言的语法规则——else 总是和离它最近的那个未配对的 if 匹配,而缩进只是给人看的,编译器完全不看缩进,所以"看起来对齐"的 else 实际可能配给了内层 if。要一劳永逸地避开,只需始终用大括号 {} 明确表达 if/else 的归属,不要依赖缩进来暗示逻辑。

第 4 题:y 的值是 0。因为 x = 5 非 0 即真,|| 触发短路求值,右边的 y++ 根本不会执行,所以 y 保持最初的 0。如果把 || 换成按位或 |:| 没有短路,两边都必定执行,于是 y++ 执行,y 变成 1。

第 5 题:忘记写 break 时,switch 在处理完匹配的 case 后,会穿透(fall-through)到后面的 case 继续执行,直到遇到 break 或 switch 结束——于是多个分支被"连锁"执行。要"故意穿透",正确姿势是把多个 case 标签堆叠在一起共享同一段代码,例如:

case 1:
case 2:
case 3:
    printf("工作日\n");
    break;

并在共享代码处加注释(如 /* fall through:1~5 共用此分支 */),让读代码的人明白这是有意的,而不是漏了 break。

第 6 题:if-else if 版本:

#include <stdio.h>
int main()
{
    int day = 0;
    scanf("%d", &day);
    if (day == 1) printf("星期一\n");
    else if (day == 2) printf("星期二\n");
    else if (day == 3) printf("星期三\n");
    else if (day == 4) printf("星期四\n");
    else if (day == 5) printf("星期五\n");
    else if (day == 6) printf("星期六\n");
    else if (day == 7) printf("星期天\n");
    else printf("输入错误\n");
    return 0;
}

switch 版本:

#include <stdio.h>
int main()
{
    int day = 0;
    scanf("%d", &day);
    switch (day)
    {
        case 1: printf("星期一\n"); break;
        case 2: printf("星期二\n"); break;
        case 3: printf("星期三\n"); break;
        case 4: printf("星期四\n"); break;
        case 5: printf("星期五\n"); break;
        case 6: printf("星期六\n"); break;
        case 7: printf("星期天\n"); break;
        default: printf("输入错误\n"); break;
    }
    return 0;
}

对比:当判断的是同一个变量的多个离散取值时,switch 结构更清晰直观(一眼看出有几个分支、有兜底项),且编译器对分支多的情况可能生成跳转表、执行更快;if-else if 更适合范围判断(如温度区间、成绩段)或由多个不同变量组合起来的条件。

第 7 题:

  • a < b && b < c:3<4 得 1,4<5 得 1,1 && 1 = 1;
  • a < b || c < b:3<4 得 1,5<4 得 0,1 || 0 = 1;
  • !(a > b):3>4 得 0,!0 = 1。

三个表达式的结果都是 1。

小结

从最基础的 if 开始,一路走到 else if 多路嵌套、悬空 else 问题、六个关系运算符(记住 == 不是 =,不能连用!)、条件操作符 ?: 的简写、三个逻辑运算符 ! && || 的规则和短路特性,再到 switch 语句的穿透与 default——C 语言的分支决策体系就这样建立起来了。

这一篇里反复出现的主题是"编译器只看语法,不看你的意图":else 匹配最近的 if、关系运算符连用被拆解、switch 没有 break 就穿透——每一个坑都源于"人以为"和"编译器实际做"之间的偏差。而唯一的解药就是:写明确、写规范、用大括号、别炫技。

下一篇我们将进入流程控制的另一半——循环。while、for、do-while 三种循环方式,break 和 continue 的控制技巧,以及一个能把你前面所有知识融会贯通的项目:猜数字游戏。准备好了吗?