跳转到内容

Lesson 04: 判断奇偶 CNB

练习任务

编写一个 C 程序,读取用户输入的一个整数,判断并输出该数是奇数还是偶数。

期望输出:

num 7 is odd
num 12 is even

提示:你需要用 scanf 读取整数(回顾 Lesson 02——它和 printf 有什么区别?变量前是否需要 &?),然后用 if/else 判断奇偶。关键思考:如何判断一个数是偶数?% 运算符可以帮你得到除法的余数。

核心知识点

  • if/else 基本语法 — 条件为真执行 if 分支,为假执行 else 分支;else 可选
  • C 语言的真假定义 — 0 为假,任何非 0 值为真;条件表达式可以是任意整型表达式
  • 关系运算符 ==!=<<=>>= — 比较表达式返回 int(1 或 0)
  • 逻辑运算符 &&||! — 与、或、非,短路求值规则
  • 运算符优先级表 — 赋值 = 优先级极低,== vs = 的经典陷阱
  • 格式化输入 scanf — 格式说明符、取地址符 &、返回值检查
  • scanfprintf 的核心区别 — 按值传递:为什么一个需要 & 一个不需要
  • scanf 常见陷阱 — 缓冲区残留、返回值检查、缓冲区溢出
  • 取模运算符 % — 整数除法余数,奇偶判断的核心工具
  • 负数取模的 C99 行为 — 结果符号与被除数相同(向零截断)
  • 位运算 & 判断奇偶 — num & 1 检查最低位,一种更底层的视角
  • 二进制数制基础 — 二进制表示、最低位(LSB)决定奇偶性
  • if-else 链与 else if 阶梯 — 多路分支的写法与本质(嵌套 if)
  • 悬空 else 问题 — else 与最近的未配对 if 结合,花括号消除歧义
  • 三元条件运算符 ?: — 表达式级别的 if-else 替代
  • 嵌套 if 语句 — 条件中嵌套条件,层级化判断逻辑
  • 花括号 {} 的使用规则 — 单条语句 vs 复合语句,防御性编码习惯
  • 复合条件 — 用 &&|| 组合多个判断
  • 逻辑运算符真值表 — 与、或、非的完整真值表
  • 取反运算符 ! — 逻辑反转的技巧与常见用途
  • 浮点数比较 — 为什么不能用 ==,精度误差与容差比较
  • switch 语句简介 — 多路分支的另一种语法(预习)
  • 常见陷阱总览 — = 误写为 ==、遗漏花括号、条件反转、未初始化变量
  • if-else 汇编实现 — 条件跳转指令、分支预测与流水线惩罚
  • 无分支编程简介 — 用查表替代条件跳转

代码框架

odd_or_even.c
c
#include <stdio.h>

int main(void)
{
    int num;

    scanf("%d", &num);

    // 在这里用 if/else 判断 num 是否为偶数
    // 偶数输出: printf("num %d is even\n", num);
    // 奇数输出: printf("num %d is odd\n", num);

    return 0;
}

填充以上框架的关键思考:判断偶数的条件是什么?num % 2 的结果有几种可能?=== 有什么区别?

TIP

先不要往下翻看参考解答。尝试自己填完框架后编译运行,用正数、负数和 0 分别测试。


深度讲解

1. 条件语句 if/else:程序的分岔路口

程序的本质是「根据不同的情况做不同的事情」。if/else 是 C 语言实现这一逻辑的最基本工具——它让程序拥有了分支能力,不再只是从上到下顺序执行。

1.1 基本语法与语义

if_else_syntax.c
c
if (条件表达式)
    语句1;       // 条件为真时执行
else
    语句2;       // 条件为假时执行

if/else 语句由三部分组成:

  1. 关键字 if:标记分支结构的开始
  2. 条件表达式:写在圆括号 ( ) 中,决定走哪条路
  3. 两个分支if 分支(条件为真时执行)和 else 分支(条件为假时执行)

程序的执行流是这样的:

进入 if/else

计算 (条件表达式)

┌─ 结果为非 0(真)──→ 执行 if 分支

└─ 结果为 0(假)────→ 执行 else 分支

合流,继续执行后续代码

三条关键规则:

  1. 条件表达式可以是任意整数值的表达式——C 语言没有专门的布尔类型(C99 之前),0 表示假,任何非 0 值表示真
  2. else 是可选的——当只需要处理「条件为真」这一种情况时,可以省略 else
  3. 多条语句必须用 { } 包裹——形成复合语句(compound statement),否则只有紧跟在 ifelse 后的第一条语句属于该分支
braces_necessity.c
c
if (num % 2 == 0)
    printf("even\n");    // 仅这条属于 if 分支
else
{                        // 复合语句开始
    printf("odd\n");
    count_odd++;         // 这两条都属于 else 分支
}                        // 复合语句结束

WARNING

不加花括号是初学者最常犯的错误之一。即使 if/else 后只有一条语句,也建议养成加 { } 的习惯——后续添加语句时不容易遗漏,也让代码的归属关系一目了然。

1.2 条件表达式:C 语言的「真」与「假」

C 语言对「真」和「假」的定义极其简单,没有任何魔法:

假(false)= 整数 0
真(true) = 任何非 0 整数(1、-1、42、0xFF……)

这意味着你可以直接在 if 中写任何整型表达式:

truth_falsity.c
c
if (num % 2)       // 等价于 if (num % 2 != 0),奇数时余数为 ±1(非 0,真)
if (num & 1)       // 等价于 if ((num & 1) != 0),奇数时最低位为 1(真)
if (num)           // 等价于 if (num != 0),num 非零时为真
if (!num)          // 等价于 if (num == 0),num 为零时为真
if (x - 5)         // 等价于 if (x != 5),相减为 0 时才为假

这种设计让代码更简洁——但需要读者对「0 是假,非 0 是真」有清晰认知。

C99 引入了 _Bool 类型(通过 <stdbool.h> 可以使用 booltruefalse),但条件表达式的判定规则不变——仍然是 0 与非 0。true 只是整数 1 的宏定义,false0 的宏定义。

bool_example.c
c
#include <stdbool.h>

int main(void)
{
    bool flag = true;       // flag = 1
    if (flag)               // 等价于 if (flag != 0),真
        printf("true\n");

    flag = false;           // flag = 0
    if (!flag)              // 等价于 if (flag == 0),真
        printf("false\n");

    return 0;
}

1.3 关系运算符:比较的六种姿势

C 语言提供了六个关系运算符,用来比较两个值的大小关系:

运算符含义示例结果说明
==等于5 == 51(真)注意是双等号!
!=不等于5 != 31(真)
<小于3 < 51(真)
<=小于等于5 <= 51(真)
>大于5 > 31(真)
>=大于等于5 >= 51(真)

关键要点

  • 所有关系运算符的返回值都是 int 类型——1 表示真,0 表示假。没有专门的「布尔」类型。
  • ==比较=赋值——这是两个完全不同的运算符,但外观相似,是 C 语言中最经典的陷阱。
  • 关系运算符的优先级低于算术运算符——a + b < c * d 等价于 (a + b) < (c * d)
relational_operators.c
c
#include <stdio.h>

int main(void)
{
    int a = 10, b = 20;

    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(假)
    printf("a <= b: %d\n", a <= b);  // 输出 1(真)
    printf("a >= b: %d\n", a >= b);  // 输出 0(假)

    // 关系运算符的结果可以直接参与运算
    int result = (a < b) + (a > b);  // 1 + 0 = 1
    printf("result: %d\n", result);  // 输出 1

    return 0;
}

2. 逻辑运算符:组合条件判断

单个关系表达式只能判断一种情况,但现实中的判断往往是复合的——「如果年龄大于 18 并且 有身份证」、「如果成绩小于 60 或者 缺考」。逻辑运算符让多个条件组合在一起。

2.1 逻辑与 &&、逻辑或 ||、逻辑非 !

运算符含义示例结果
&&逻辑与(AND)(5 > 3) && (2 < 4)1(两边都为真)
||逻辑或(OR)(5 > 3) || (2 > 4)1(至少一边为真)
!逻辑非(NOT)!(5 > 3)0(真变假)

逻辑运算符的操作数可以是任意整型表达式——仍然遵循「非 0 即真,0 即假」的规则。

logical_operators.c
c
#include <stdio.h>

int main(void)
{
    int x = 5, y = 0, z = -3;

    // &&:两边都为真时结果才为真
    printf("x && y: %d\n", x && y);  // 5 && 0 → 0(假)
    printf("x && z: %d\n", x && z);  // 5 && -3 → 1(真)

    // ||:至少一边为真时结果为真
    printf("x || y: %d\n", x || y);  // 5 || 0 → 1(真)
    printf("y || 0: %d\n", y || 0);  // 0 || 0 → 0(假)

    // !:真假反转
    printf("!x: %d\n", !x);          // !5 → 0(假)
    printf("!y: %d\n", !y);          // !0 → 1(真)
    printf("!!x: %d\n", !!x);        // !!5 → 1(双重否定,归一化为 1)

    return 0;
}

2.2 真值表

逻辑运算符的行为可以用真值表来精确定义:

逻辑与 &&(AND)

ABA && B
0(假)0(假)0
0(假)非 0(真)0
非 0(真)0(假)0
非 0(真)非 0(真)1

逻辑或 ||(OR)

ABA || B
0(假)0(假)0
0(假)非 0(真)1
非 0(真)0(假)1
非 0(真)非 0(真)1

逻辑非 !(NOT)

A!A
0(假)1
非 0(真)0

从真值表可以看出:

  • && 是「全真才真」——只要有一个是假,结果就是假
  • || 是「有真就真」——只要有一个是真,结果就是真
  • ! 是「真假反转」——0 变 1,非 0 变 0

2.3 短路求值:性能与安全的双重保障

&&|| 的一个重要特性是短路求值(short-circuit evaluation)——一旦结果确定,就不再计算后续表达式:

short_circuit.c
c
// && 短路:左边为假时,右边不执行
if (ptr != NULL && ptr->value > 0)  // ptr 为 NULL 时,ptr->value 不会执行
    printf("valid\n");

// || 短路:左边为真时,右边不执行
if (index < 0 || index >= MAX)      // index < 0 为真时,不检查 index >= MAX
    printf("out of range\n");

短路求值的两个重要应用:

  1. 安全检查:先判断指针/索引是否有效,再访问其内容——避免空指针解引用或数组越界
  2. 性能优化:将计算代价高的条件放在后面,利用短路减少不必要的计算
short_circuit_demo.c
c
#include <stdio.h>

int expensive_check(void)
{
    printf("expensive_check() called!\n");
    return 1;
}

int main(void)
{
    int x = 0;

    // && 短路:左边为 0(假),右边不执行
    if (x && expensive_check())
        printf("both true\n");
    // 输出:无(expensive_check 没有被调用)

    // || 短路:左边为非 0(真),右边不执行
    x = 1;
    if (x || expensive_check())
        printf("at least one true\n");
    // 输出:at least one true(expensive_check 没有被调用)

    return 0;
}

TIP

短路求值不仅是一种性能优化,更是编写安全代码的基础。检查指针非空后再访问成员、检查索引合法后再访问数组,都依赖短路求值来避免崩溃。

2.4 取反运算符 ! 的使用技巧

! 将真变假、假变真。它有几个常见的使用模式:

not_operator.c
c
// 1. 检查是否为 0
if (!num)          // 等价于 if (num == 0)
    printf("num is zero\n");

// 2. 检查字符串是否为空(指针是否为 NULL)
if (!str)          // 等价于 if (str == NULL)
    printf("str is NULL\n");

// 3. 翻转条件逻辑
if (!(a > b))      // 等价于 if (a <= b)
    printf("a is not greater than b\n");

// 4. 双重否定归一化——将任意非 0 值转换为 1
int normalized = !!x;  // x 非 0 → 1,x 为 0 → 0

WARNING

!~(按位取反)是完全不同的运算符。!5 结果是 0(逻辑非),~5 结果是 -6(按位取反)。混淆两者会导致难以排查的逻辑错误。

2.5 复合条件:用 &&|| 组合判断

将多个关系表达式用逻辑运算符连接,形成复合条件:

compound_conditions.c
c
// 判断一个年份是否为闰年
// 规则:能被 4 整除但不能被 100 整除,或者能被 400 整除
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
    printf("%d is a leap year\n", year);
else
    printf("%d is not a leap year\n", year);

// 判断一个字符是否为小写字母
if (ch >= 'a' && ch <= 'z')
    printf("'%c' is a lowercase letter\n", ch);

// 判断一个数字是否在某个范围内
if (num >= 1 && num <= 100)
    printf("num is between 1 and 100\n");

TIP

写复合条件时,善用括号 ( ) 来明确运算顺序。虽然 C 有严格的优先级规则(&& 优先级高于 ||),但加括号让意图更清晰,减少出错可能。


3. 运算符优先级:谁先算,谁后算

C 语言有大量运算符,它们之间的优先级关系是初学者最容易困惑的地方之一。

3.1 完整优先级表

以下是 C 语言常用运算符的优先级,从上到下优先级递减(同一行优先级相同):

优先级运算符结合性说明
1(最高)() [] -> .左→右函数调用、下标、成员访问
2! ~ ++ -- + - * & (type) sizeof右→左一元运算符(注意 *& 在这里是解引用和取地址)
3* / %左→右乘、除、取模
4+ -左→右加、减
5< <= > >=左→右关系运算符
6== !=左→右相等/不等
7&&左→右逻辑与
8||左→右逻辑或
9?:右→左三元条件
10= += -= *= /= %= &= |= ^= <<= >>=右→左赋值(优先级极低)
11(最低),左→右逗号运算符

3.2 最经典的优先级陷阱:= vs ==

赋值运算符 = 的优先级极低——远低于关系运算符和逻辑运算符。这导致了 C 语言中最经典的陷阱:

assignment_vs_comparison.c
c
// 陷阱 1:在条件中使用 = 而非 ==
if (x = 5)     // 赋值!先把 5 赋给 x,然后判断 5(非 0 → 真)
if (x == 5)    // 比较!判断 x 是否等于 5

// 陷阱 2:将比较结果赋值给变量
int result;
result = a == b;   // result = 1 或 0(比较的结果)
// 等价于:
result = (a == b); // 括号明确意图

// 陷阱 3:链式比较在 C 中不按直觉工作
if (1 <= x <= 10)  // 不是「x 在 1 到 10 之间」!
// 实际计算:(1 <= x) <= 10 → (0 或 1) <= 10 → 永远为真!
// 正确写法:
if (1 <= x && x <= 10)
precedence_demo.c
c
#include <stdio.h>

int main(void)
{
    int a = 0, b = 1, c = 2;

    // 优先级:* 高于 +,所以 a * b 先算
    printf("%d\n", a + b * c);       // 0 + (1 * 2) = 2

    // 优先级:< 高于 ==
    // a < b 先算 → 0 < 1 → 1(真),然后 1 == c → 1 == 2 → 0(假)
    printf("%d\n", a < b == c);      // (a < b) == c → 1 == 2 → 0

    // && 优先级高于 ||
    // b && c → 1 && 2 → 1,然后 a || 1 → 0 || 1 → 1
    printf("%d\n", a || b && c);     // a || (b && c) → 0 || 1 → 1

    return 0;
}

CAUTION

永远用 == 做比较,用 = 做赋值。编译时加 -Wall 可让编译器对 if (x = 5) 发出警告。部分程序员习惯写 if (5 == x)(常量在左),这样误写为 if (5 = x) 时编译器必然报错——因为不能给常量赋值。


4. 格式化输入 scanf:程序与用户的对话

回顾 Lesson 02 中我们学过的 printf——它负责将程序的内部数据输出到屏幕上。scanf 是它的对称搭档,负责将用户的键盘输入读取到程序内部。两者共同构成程序与外部世界的「对话通道」。

4.1 scanf 的函数签名与工作原理

scanf_signature.c
c
int scanf(const char *format, ...);

三个关键点:

  • 返回值:成功匹配并赋值的输入项数;遇到输入结束(EOF)返回 EOF(通常定义为 -1);匹配失败返回 0
  • format:格式控制字符串,与 printf 的格式符类似——%d(整数)、%f(浮点)、%c(字符)、%s(字符串)
  • 后续参数必须是指针——指向存储输入值的变量的内存地址

scanf 的工作流程:

用户键盘输入 终端驱动 输入缓冲区 scanf 按格式解析 通过指针写入变量

scanf 从输入缓冲区中逐字符读取,尝试匹配 format 指定的模式。匹配成功的数据通过指针写入对应变量。匹配失败时,未匹配的字符留在缓冲区中。

4.2 为什么 scanf 需要 &——按值传递的本质

这是初学者最常困惑的问题。对比 Lesson 02 的 printf

scanf_ampersand.c
c
printf("%d", num);    // 不需要 &:只读取 num 的值并显示
scanf("%d", &num);    // 需要 &:要修改 num 的值
scanf("%d", num);     // 传递垃圾值作为地址——几乎必然崩溃

根本原因:C 语言的函数参数传递方式是按值传递(pass by value)——函数接收的是实参的副本,无法直接修改原变量。

printf("%d", num) 的传参模型:
┌──────────┐     ┌──────────┐
  main()   printf() 
  num = 7 ──→  x = 7 只读副本,原变量 num 不变
└──────────┘     └──────────┘

scanf("%d", &num) 的传参模型:
┌──────────┐     ┌──────────┐
  main()   scanf()  
  num = ? ←──  addr 通过地址直接写入,修改原变量 num
  @0x7ff  0x7ff
└──────────┘     └──────────┘

printf 只需要读取变量的值来显示——传值就足够了。scanf 需要写入变量的值——必须知道变量的地址,通过指针间接写入。

用一个简单的例子来感受按值传递:

pass_by_value.c
c
#include <stdio.h>

void try_modify(int x)
{
    x = 100;     // 只修改了局部副本 x
    printf("inside:  x = %d\n", x);  // 输出 100
}

void real_modify(int *px)
{
    *px = 100;   // 通过指针修改原变量
    printf("inside: *px = %d\n", *px); // 输出 100
}

int main(void)
{
    int a = 5;

    try_modify(a);
    printf("after try_modify:  a = %d\n", a);   // 输出 5(不变!)

    real_modify(&a);
    printf("after real_modify: a = %d\n", a);   // 输出 100

    return 0;
}

scanf 内部就类似 real_modify——它需要指针参数来修改调用者的变量。

IMPORTANT

如果忘记写 &scanf 会把 num 当前的垃圾值当作内存地址使用——几乎必然导致段错误(Segmentation Fault)或未定义行为。

4.3 scanf 的返回值:不要忽视它

scanf 返回成功匹配并赋值的项数。忽视返回值是危险的——如果用户输入的不是数字,程序会在「不知道变量有没有被正确赋值」的情况下继续运行:

scanf_return_value.c
c
#include <stdio.h>

int main(void)
{
    int num;

    printf("Please enter an integer: ");

    if (scanf("%d", &num) != 1)
    {
        printf("Error: invalid input!\n");
        return 1;          // 非正常退出
    }

    // 到这里,num 一定被正确赋值了
    printf("You entered: %d\n", num);
    return 0;
}

检查返回值的三个好处:

  1. 尽早发现输入错误,避免使用未正确初始化的变量
  2. 可以给用户友好的错误提示
  3. 为后续的输入验证和重试逻辑打下基础

4.4 scanf 的常见陷阱

陷阱 1:输入缓冲区残留

scanf("%d", &num) 读取整数后,会在缓冲区中留下换行符 \n。如果后续使用 scanf("%c", &ch) 读取字符,会直接读到这个残留的 \n

buffer_residue.c
c
scanf("%d", &num);    // 用户输入 "7\n",%d 读取 7,\n 留在缓冲区
scanf("%c", &ch);     // 直接读到残留的 \n,而非用户新输入的字符

解决方法:在 %c 前加一个空格,告诉 scanf 跳过所有空白字符:

buffer_residue_fix.c
c
scanf("%d", &num);
scanf(" %c", &ch);   // 空格跳过所有空白字符(空格、\t、\n 等)

陷阱 2:匹配失败后不清空缓冲区

scanf("%d", &num) 遇到字母输入时,匹配失败,错误的输入留在缓冲区。后续的 scanf 会反复读到同一段错误输入,陷入死循环:

clear_buffer.c
c
#include <stdio.h>

int main(void)
{
    int num, result;

    while (1)
    {
        printf("Enter a number: ");
        result = scanf("%d", &num);

        if (result == 1)
        {
            printf("You entered: %d\n", num);
            break;
        }
        else
        {
            printf("Invalid input, try again.\n");
            // 清空缓冲区中的错误输入
            while (getchar() != '\n')
                ;  // 逐个丢弃字符,直到遇到换行符
        }
    }

    return 0;
}

陷阱 3:字符串缓冲区溢出

%s 不检查目标数组的大小,输入过长会溢出:

buffer_overflow.c
c
char name[10];
scanf("%s", name);    // 输入超过 9 字符会溢出,破坏栈上其他数据
scanf("%9s", name);   // 安全:限制最多读取 9 字符,留 1 位给 \0

4.5 常用格式说明符速查

格式符类型说明
%dint有符号十进制整数
%uunsigned int无符号十进制整数
%ffloat浮点数(十进制)
%lfdouble双精度浮点数(注意:printf%f 即可,但 scanf 中必须 %lf
%cchar单个字符
%schar[]字符串(自动加 \0,不读空白)
%xunsigned int十六进制整数
%ounsigned int八进制整数

IMPORTANT

scanfprintfdouble 的格式符要求不同:printf%ffloatdouble 都自动提升为 double),scanf%lf(必须明确告诉它是 double 还是 float)。混用会导致未定义行为。


5. 取模运算符 %:奇偶判断的核心工具

5.1 取模的基本语义

% 返回整数除法的余数

modulo_basic.c
c
int remainder = 10 % 3;   // remainder = 1(10 = 3 × 3 + 1)
int quotient  = 10 / 3;   // quotient  = 3(整数除法,向零截断)

C99 标准保证以下等式始终成立:

a == (a / b) * b + (a % b)

也就是说,% 的结果就是「被除数减去商的整数倍后剩下的部分」。

5.2 负数取模的 C99 行为

在 C89/C90 中,负数取模的行为是实现定义的(implementation-defined)——编译器可以自由选择向上取整还是向下取整。C99 统一了规则:向零截断(truncated division),即 a % b 的结果符号与 a(被除数)相同:

modulo_negative.c
c
 7 %  31    //  7 /  3 =  2(向零截断), 2 ×  3 =  6, 7 -  6 =  1
-7 %  3-1    // -7 /  3 = -2(向零截断),-2 ×  3 = -6,-7 -(-6) = -1
 7 % -31    //  7 / -3 = -2(向零截断),-2 × -3 =  6, 7 -  6 =  1
-7 % -3-1    // -7 / -3 =  2(向零截断), 2 × -3 = -6,-7 -(-6) = -1

WARNING

这意味着 num % 2 对负奇数返回 -1,而非 1。因此判断奇数应写 num % 2 != 0,而不是 num % 2 == 1——后者对负奇数失效。

5.3 奇偶判断的数学原理

偶数 = 2 × k 除以 2 余数为 0
奇数 = 2 × k + 1 除以 2 余数为 1(正奇数)或 -1(负奇数)

因此,安全的奇偶判断:

  • 判断偶数num % 2 == 0 —— 对正数、负数、0 均成立
  • 判断奇数num % 2 != 0 —— 比 == 1 更安全,因为负奇数的余数是 -1
parity_check.c
c
#include <stdio.h>

int main(void)
{
    int test_values[] = {0, 7, -7, 12, -12};
    int n = sizeof(test_values) / sizeof(test_values[0]);

    for (int i = 0; i < n; i++)
    {
        int num = test_values[i];
        printf("num = %3d: ", num);
        printf("num %% 2 = %2d, ", num % 2);
        printf("== 0 ? %s, ", (num % 2 == 0) ? "even" : "odd");
        printf("!= 0 ? %s\n", (num % 2 != 0) ? "odd" : "even");
    }

    return 0;
}

输出:

num =   0: num % 2 =  0, == 0 ? even, != 0 ? even
num =   7: num % 2 =  1, == 0 ? odd,  != 0 ? odd
num =  -7: num % 2 = -1, == 0 ? odd,  != 0 ? odd
num =  12: num % 2 =  0, == 0 ? even, != 0 ? even
num = -12: num % 2 =  0, == 0 ? even, != 0 ? even

6. 位运算判断奇偶:从二进制视角看世界

除了取模,还可以用位运算来判断奇偶:

bitwise_odd_even.c
c
if (num & 1)
    printf("odd\n");
else
    printf("even\n");

短短一行代码,背后却藏着一个关于计算机如何表示数字的基本原理。

6.1 二进制数制基础

计算机内部,所有数据最终都以二进制(binary)存储——只有 0 和 1。理解二进制是理解位运算的前提。

以 8 位无符号整数为例,每一位代表一个 2 的幂:

位位置:  7    6    5    4    3    2    1    0
权重:   2⁷   2⁶   2⁵   2⁴   2⁰
值:    128   64   32   16   8    4    2    1

一个二进制数 00001101 的值 = 1×8 + 1×4 + 0×2 + 1×1 = 13

6.2 最低位决定奇偶

观察偶数和奇数的二进制表示,会发现一个简单规律:

偶数(最低位为 0):
  0 00000000    2 00000010    4 00000100    6 00000110
  8 00001000   10 00001010   12 00001100   14 00001110

奇数(最低位为 1):
  1 00000001    3 00000011    5 00000101    7 00000111
  9 00001001   11 00001011   13 00001101   15 00001111

规律:所有偶数的最低位(Least Significant Bit, LSB)为 0,所有奇数的最低位为 1

为什么?因为二进制每一位的权重是 2 的幂:2⁰ = 12¹ = 22² = 4……除了最低位 2⁰ = 1 是奇数外,其他所有位(2¹ = 22² = 42³ = 8……)都是偶数。一个数的奇偶性完全由最低位决定——最低位为 1,整体就是奇数;最低位为 0,整体就是偶数。

6.3 & 1 操作的原理

&按位与(bitwise AND)运算符——对两个操作数的每一位分别做 AND 运算:

按位与规则:
  1 & 1 = 1
  1 & 0 = 0
  0 & 1 = 0
  0 & 0 = 0

num & 1num 与二进制 ...0001 做按位与——只保留最低位,其他所有位都被清零:

  15 (00001111)         44 (00101100)
&  1 (00000001)       &  1 (00000001)
--------------        --------------
   1 (00000001)         0 (00000000)
 奇数 偶数

num & 1 的结果:奇数返回 1(非 0 → 真),偶数返回 0(假)——恰好与 C 语言的真假定义完美吻合。

6.4 位运算的其他用途

位运算在 C 语言中非常强大,除了奇偶判断,还有许多应用:

bitwise_operations.c
c
// 乘 2(左移 1 位)
int doubled = num << 1;     // 等价于 num * 2

// 除 2(右移 1 位)
int halved = num >> 1;      // 等价于 num / 2(对正数)

// 判断是否为 2 的幂
if (num > 0 && (num & (num - 1)) == 0)
    printf("%d is a power of 2\n", num);

// 交换两个变量的值(不使用临时变量)
a ^= b;  b ^= a;  a ^= b;

// 将第 n 位置 1
value |= (1 << n);

// 将第 n 位清零
value &= ~(1 << n);

// 翻转第 n 位
value ^= (1 << n);

6.5 性能对比:取模 vs 位运算

方法底层指令(x86)典型延迟
num % 2idiv(有符号除法指令)~20-40 时钟周期
num & 1and(按位与指令)1 时钟周期

理论上位运算更快——除法是 CPU 中最慢的基本运算之一,而按位与是单周期指令。

TIP

在现代编译器开启优化(-O2-O3)后,num % 2 通常被自动优化为 num & 1——编译器比你更懂优化。因此不必为了性能刻意使用位运算。从表达意图的角度选择:% 2 说的是「判断余数」(数学视角),& 1 说的是「检查最低位」(硬件视角)。选择更贴合当前语境语义的写法。


7. if/else 进阶结构

if/else 不只是简单的二选一。通过嵌套和组合,可以构建出任意复杂度的分支逻辑。

7.1 if-else 链与 else-if 阶梯

当需要处理多种互斥的情况时,可以用 else if 形成链:

if_else_chain.c
c
if (score >= 90)
    grade = 'A';
else if (score >= 80)
    grade = 'B';
else if (score >= 70)
    grade = 'C';
else if (score >= 60)
    grade = 'D';
else
    grade = 'F';

本质揭示else if 不是 C 语言的关键字——它只是 else 后跟一个嵌套的 if。上面的代码等价于:

else_if_unrolled.c
c
if (score >= 90)
    grade = 'A';
else
    if (score >= 80)
        grade = 'B';
    else
        if (score >= 70)
            grade = 'C';
        else
            if (score >= 60)
                grade = 'D';
            else
                grade = 'F';

缩进对齐只是书写习惯,编译器看到的永远是层层嵌套的 if-else 结构。else if 写法更紧凑、可读性更好,但理解其本质有助于理解悬空 else 问题。

执行逻辑:条件从上到下依次检查,一旦命中就跳出整个链。因此条件的顺序至关重要——如果把 score >= 60 放在最前面,90 分的学生也会拿到 D。

if_else_chain_order.c
c
// 错误顺序:宽泛条件在前,后续条件永远不会被检查
if (score >= 60)
    grade = 'D';       // 90 分的学生也被评为 D!
else if (score >= 70)
    grade = 'C';       // 永远走不到这里
// ...

// 正确顺序:从最严格到最宽松
if (score >= 90)
    grade = 'A';
else if (score >= 80)
    grade = 'B';
// ...

7.2 悬空 else 问题(Dangling Else)

if 嵌套而 else 数量少于 if 时,else 属于哪个 if 就变得不明确:

dangling_else.c
c
if (a > 0)
    if (b > 0)
        printf("both positive\n");
else
    printf("???\n");   // 这个 else 属于哪个 if?

C 语言的规则else 总是与最近的、尚未配对的 if 结合。上例中 else 属于内层 if (b > 0),而非外层 if (a > 0)——尽管缩进让人误以为它属于外层。

要让它属于外层 if,必须用花括号明确界定:

dangling_else_fix.c
c
if (a > 0) {
    if (b > 0)
        printf("both positive\n");
} else {
    printf("a is not positive\n");
}

花括号将内层 if 封闭起来,外层的 else 自然就与外层 if 配对。

CAUTION

缩进只影响人眼,不影响编译器。编译器完全忽略缩进,只看花括号 { } 来确定代码块边界。永远用 { } 来明确 if/else 的归属关系。

7.3 嵌套 if 语句

ifelse 分支内部再使用 if/else,形成嵌套结构——这是构建复杂判断逻辑的基本方式:

nested_if.c
c
// 判断三角形类型
if (a + b > c && a + c > b && b + c > a)
{
    // 是三角形
    if (a == b && b == c)
        printf("equilateral triangle\n");        // 等边
    else if (a == b || b == c || a == c)
        printf("isosceles triangle\n");          // 等腰
    else
        printf("scalene triangle\n");            // 不等边
}
else
{
    printf("not a triangle\n");
}

嵌套层次不宜过深——通常建议不超过 3 层。过深的嵌套让代码难以阅读和维护,可以考虑:

  • 提前 return 减少嵌套
  • 将内层逻辑提取为独立函数
  • else if 链替代深层嵌套

7.4 花括号 {} 的使用规则

花括号在 if/else 中的使用有两种风格,各有优劣:

brace_styles.c
c
// 风格 1:始终使用花括号(推荐给初学者)
if (num % 2 == 0) {
    printf("even\n");
} else {
    printf("odd\n");
}

// 风格 2:单条语句省略花括号
if (num % 2 == 0)
    printf("even\n");
else
    printf("odd\n");
风格优点缺点
始终加 {}不会遗漏;后续添加语句安全多两行,视觉上略显冗余
单行省略 {}代码紧凑后续加语句容易忘记加花括号

IMPORTANT

对于初学者,强烈建议始终使用花括号。这是一个防御性编程习惯——当你在调试时临时添加一行 printf,不会因为忘记加花括号而改变代码逻辑。

brace_pitfall.c
c
// 看似在 else 中添加了调试语句
if (num % 2 == 0)
    printf("even\n");
else
    printf("debug: num = %d\n", num);  // 你以为这是 else 的一部分
    printf("odd\n");                   // 实际上这行已经不在 else 中了!
// 上面的代码等价于:
if (num % 2 == 0)
    printf("even\n");
else
    printf("debug: num = %d\n", num);
printf("odd\n");  // 无论奇偶都会执行!

7.5 三元条件运算符 ?:

if-else语句——它不产生值,不能嵌入表达式中。而 ?:表达式——它返回一个值:

ternary_operator.c
c
// 语法:条件 ? 值为真时的结果 : 值为假时的结果

// 基本用法
int max = (a > b) ? a : b;

// 嵌入 printf 中
printf("%s\n", (num % 2 == 0) ? "even" : "odd");

// 替代简单的 if-else 赋值
// 之前:
if (score >= 60)
    result = "pass";
else
    result = "fail";
// 之后:
const char *result = (score >= 60) ? "pass" : "fail";

适用场景:简单的二选一赋值或函数参数选择。不宜嵌套过深——嵌套三元运算符会严重损害可读性:

ternary_nested.c
c
// 不推荐:嵌套三元运算符
int sign = (x > 0) ? 1 : (x < 0) ? -1 : 0;  // 能工作,但难以阅读

// 推荐:用 if-else 链
int sign;
if (x > 0)
    sign = 1;
else if (x < 0)
    sign = -1;
else
    sign = 0;

TIP

?: 是 C 语言中唯一的三元运算符。它让代码更紧凑,但不要为了「酷」而滥用。当条件逻辑超过一行时,用 if-else 更清晰。

7.6 switch 语句简介(预习)

当需要根据同一个表达式的多个离散值进行分支时,switch 语句比 if-else 链更清晰:

switch_preview.c
c
// if-else 链写法
if (day == 1)
    printf("Monday\n");
else if (day == 2)
    printf("Tuesday\n");
else if (day == 3)
    printf("Wednesday\n");
// ... 更多分支 ...
else
    printf("Invalid day\n");

// switch 写法
switch (day)
{
    case 1:  printf("Monday\n");    break;
    case 2:  printf("Tuesday\n");   break;
    case 3:  printf("Wednesday\n"); break;
    // ...
    default: printf("Invalid day\n"); break;
}

switch 的关键特性:

  • 表达式必须是整数类型intcharenum 等),不能是浮点数或字符串
  • case 标签必须是编译时常量
  • break 用于跳出 switch——遗漏 break 会导致「贯穿」(fall-through)到下一个 case
  • default 处理所有未匹配的情况(可选,但推荐)

switch 的详细讲解将在后续课程中展开。目前只需知道:当有 3 个以上的等值分支时,switch 通常是比 if-else 链更好的选择。


8. 浮点数比较:为什么不能用 ==

8.1 浮点数的精度问题

floatdouble 使用 IEEE 754 标准表示,很多十进制小数无法精确表示为二进制浮点数——就像 1/3 无法精确表示为十进制小数一样:

float_comparison.c
c
#include <stdio.h>

int main(void)
{
    float a = 0.1f;
    float b = 0.2f;
    float c = a + b;     // 期望 0.3

    printf("c = %.20f\n", c);            // 可能输出 0.30000001192092895508
    printf("c == 0.3: %d\n", c == 0.3f); // 输出 0(假!)

    return 0;
}

8.2 正确的浮点数比较方式

使用容差比较(epsilon comparison)——检查两个浮点数之差的绝对值是否小于一个很小的阈值:

float_epsilon.c
c
#include <stdio.h>
#include <math.h>

int main(void)
{
    float a = 0.1f;
    float b = 0.2f;
    float c = a + b;
    float epsilon = 1e-6f;  // 容差:0.000001

    if (fabsf(c - 0.3f) < epsilon)
        printf("c is approximately 0.3\n");
    else
        printf("c is NOT 0.3\n");

    return 0;
}

WARNING

==!= 用于浮点数几乎总是不安全的。除非你明确知道浮点数是精确的整数(如 1.02.0),否则始终使用容差比较。


9. 常见陷阱总览

学习 if/else 和条件判断时,以下是最高频的错误:

9.1 = 误写为 ==(或反之)

trap_assignment.c
c
// 陷阱:条件中写成了赋值
if (num % 2 = 0)     // 编译错误:不能对表达式赋值
if (num % 2 == 0)    // 正确:比较

// 陷阱:条件中赋值而非比较
if (x = 5)           // 赋值成功,返回 5(非 0),条件永远为真
if (x == 5)          // 正确:比较

9.2 遗漏花括号

trap_missing_braces.c
c
if (condition)
    do_something();
    do_also();       // 这行不在 if 内!永远会执行

9.3 条件反转

trap_inverted.c
c
// 陷阱:把「奇数」写成 num % 2 == 1,负奇数返回 -1 不满足
if (num % 2 == 1)    // 对 -7 为假
// 正确:
if (num % 2 != 0)    // 对正负奇数都为真

9.4 未初始化变量

trap_uninitialized.c
c
int num;                  // 未初始化——垃圾值
if (num % 2 == 0)         // 用垃圾值判断——结果不可预测
    printf("even\n");

9.5 多余的分号

trap_semicolon.c
c
if (condition);           // 分号形成了空的 if 体
    do_something();       // 这行永远执行,不受 if 控制

TIP

使用编译器的 -Wall -Wextra 选项可以捕获上述大部分错误。养成「零警告编译」的习惯。


10. if-else 的底层实现:从 C 到汇编

理解 if/else 在 CPU 层面如何执行,有助于写出更高效的代码。

10.1 汇编层面的条件分支

if_else_to_asm.c
c
if (num % 2 == 0)
    printf("even\n");
else
    printf("odd\n");

编译器将这段 C 代码翻译为条件跳转指令(以 x86-64 为例):

asm
    mov  eax, [num]       ; 将 num 加载到 eax 寄存器
    and  eax, 1           ; num & 1(编译器将 %2 优化为 &1)
    jnz  odd_branch       ; 结果非零(奇数),跳转到 odd 分支
    ; ---- even 分支 ----
    lea  rdi, [even_str]  ; "even" 字符串地址放入 rdi
    call printf           ; 调用 printf
    jmp  end_if           ; 跳过 odd 分支
odd_branch:
    ; ---- odd 分支 ----
    lea  rdi, [odd_str]   ; "odd" 字符串地址放入 rdi
    call printf           ; 调用 printf
end_if:
    ; ---- if-else 结束,继续执行后续代码 ----

核心模式:

  1. 计算条件:用算术或逻辑指令计算出条件的真/假值
  2. 条件跳转:根据结果决定跳转到哪个分支
  3. 执行分支:执行对应分支的代码
  4. 合流:两个分支通过无条件跳转 jmp 汇合到同一点

10.2 分支预测与流水线惩罚

现代 CPU 使用流水线(pipeline)技术——同时处理多条指令的不同阶段,大幅提升吞吐量。但条件跳转打破了流水线的顺畅:

CPU 流水线(简化示意):
取指 译码 执行 访存 写回
取指 译码 执行 访存 写回
取指 译码 执行 访存 写回

  遇到 jnz                需要等条件计算结果出来才知道下一条指令是什么

为了减少等待,CPU 使用分支预测器(Branch Predictor)来猜测跳转方向:

  • 预测正确:流水线继续运行,无额外开销
  • 预测错误:需要清空流水线中已部分执行但方向错误的指令,重新取指——约 10-20 时钟周期的惩罚

对于奇偶判断这种「输入随机、一半奇一半偶」的场景,分支预测命中率约 50%,惩罚频繁。

10.3 无分支编程简介

在性能极其敏感的代码中,可以用无分支编程(branchless programming)消除条件跳转:

branchless.c
c
// 常规写法(有分支)
if (num & 1)
    printf("odd\n");
else
    printf("even\n");

// 无分支写法:用数组查表
const char *messages[] = {"even", "odd"};
printf("%s\n", messages[num & 1]);   // 无条件跳转,仅一次数组索引

num & 1 只可能是 01——恰好作为数组下标。CPU 只需要计算下标、读取数组元素、调用 printf,全程无分支。

TIP

对于入门学习,优先关注代码可读性和正确性。无分支编程是高级优化技巧,在大多数应用场景中得不偿失。编译器在 -O2 优化下也会自动进行分支优化。理解其原理即可,不必刻意使用。


参考解答

奇偶判断(取模版本)
odd_or_even_modulo.c
c
#include <stdio.h>

int main(void)
{
    int num;

    scanf("%d", &num);

    if (num % 2 == 0)
        printf("num %d is even\n", num);
    else
        printf("num %d is odd\n", num);

    return 0;
}

核心逻辑:num % 2 对偶数返回 0,对奇数返回 1(正奇数)或 -1(负奇数)。使用 == 0 判断偶数是安全的,因为无论正负偶数余数都是 0else 分支覆盖了所有 != 0 的情况,即所有奇数。

位运算版本
odd_or_even_bitwise.c
c
#include <stdio.h>

int main(void)
{
    int num;

    scanf("%d", &num);

    if (num & 1)
        printf("num %d is odd\n", num);
    else
        printf("num %d is even\n", num);

    return 0;
}

num & 1 直接检查二进制最低位:奇数最低位为 1(非 0 → 真),偶数最低位为 0(假)。注意判断顺序与取模版本相反——奇数在前(& 1 为真时),偶数在 else 中。

对照检查:你的 scanf 第二个参数是否加了 &?判断条件用的是 num % 2 == 0 还是 num % 2 = 0(赋值)?用正数、负数和 0 分别测试过吗?


课堂讨论

  1. 局部变量 num 如果没有初始化值,那么默认的值是不是 0?
  2. scanf 函数的第 2 个参数,如果不用 &,会有什么后果?
  3. 如果用户输入的不是数字(比如字母),程序会怎样?
  4. 想一想为什么 printf 不需要 &num,而 scanf 需要 &
  5. num % 2 == 1 能正确判断所有奇数吗?负奇数呢?
  6. if (a < b < c) 在 C 语言中是什么意思?它和我们的直觉一致吗?

讨论答案

Q1: 局部变量 num 如果没有初始化值,默认的值是不是 0?

不是。 局部变量的默认值是不确定的垃圾值,绝对不一定是 0。

uninitialized_var.c
c
#include <stdio.h>

int main(void)
{
    int num;                // 未初始化,值不确定
    printf("num = %d\n", num);  // 可能输出 32767、-448293 等任意值
    return 0;
}

原因:局部变量分配在(stack)上。栈空间被反复使用——新分配的栈帧包含之前函数调用残留的数据。C 语言的设计哲学是「零开销」——不自动初始化以避免不必要的性能代价。

对比

变量类型存储位置默认初始化
全局变量 int g;数据段/BSS自动为 0(操作系统在程序启动时清零 BSS 段)
静态局部 static int s;数据段/BSS自动为 0(同上)
局部变量 int l;不初始化(垃圾值)
进程内存布局:
高地址 ┌────────────────────┐
 (Stack)      │ ← 局部变量在此,不初始化
         ↓↓

         ↑↑
 (Heap)       │ ← malloc 分配,也不初始化
       ├────────────────────┤
   BSS 段(零初始化) 未初始化全局/静态变量,启动时清零
       ├────────────────────┤
   数据段(已初始化) 有初始值的全局/静态变量
       ├────────────────────┤
   代码段 (Text)      │ ← 程序指令
低地址 └────────────────────┘

规则:始终显式初始化局部变量。编译时加 -Wall 可让编译器警告未初始化变量的使用。

Q2: scanf 的第 2 个参数如果不用 &,会有什么后果?

严重后果——通常是段错误(Segmentation Fault)或未定义行为。

scanf_no_ampersand.c
c
int num;
scanf("%d", num);    // 传递垃圾值作为地址
scanf("%d", &num);   // 正确:传递 num 的地址

scanf 的第二个参数需要一个地址(指针),告诉它「把读到的值写到哪个内存位置」。

  • &num 传递的是 num 的地址(如 0x7fff1234)——scanf 向这个地址写入
  • num 传递的是 num 当前的值——一个垃圾值(如 32767)——scanf32767 当作内存地址,尝试向地址 32767 写入数据

地址 32767 几乎必然不在程序的内存映射范围内,操作系统会发送 SIGSEGV 信号,导致程序崩溃。

更隐蔽的危险:如果 num 碰巧包含一个合法地址值(极低概率),程序不会崩溃,但会写入错误的内存位置——导致数据损坏,更难排查。

编译时警告:现代编译器(如 GCC)会对 scanf 的参数类型进行检查,scanf("%d", num) 会触发类似 warning: format '%d' expects argument of type 'int *', but argument 2 has type 'int' 的警告。永远不要忽视这些警告。

Q3: 如果用户输入的不是数字(比如字母),程序会怎样?

scanf 匹配失败,变量保持原值(未初始化则是垃圾值),错误输入留在缓冲区。

scanf_non_numeric.c
c
int num;                // 未初始化,垃圾值
scanf("%d", &num);      // 用户输入 "abc"

实际发生的事:

  1. scanf("%d", ...) 期望读取十进制整数
  2. 遇到字母 'a',无法解析为整数——匹配失败
  3. scanf 立即返回 0(成功匹配了 0 项),不修改 num
  4. num 保持原值——未初始化则仍是垃圾值
  5. "abc\n" 完整保留在输入缓冲区中,后续任何 scanf 都会反复读到它

健壮的写法——检查 scanf 返回值:

scanf_robust.c
c
#include <stdio.h>

int main(void)
{
    int num;

    printf("Please enter an integer: ");

    if (scanf("%d", &num) != 1)
    {
        printf("Error: invalid input!\n");
        return 1;
    }

    printf("You entered: %d\n", num);
    return 0;
}

如果需要让用户重新输入直到正确为止,还需清空缓冲区:

scanf_retry.c
c
int num, result;
while (1)
{
    printf("Enter an integer: ");
    result = scanf("%d", &num);
    if (result == 1)
        break;
    printf("Invalid! Try again.\n");
    while (getchar() != '\n')   // 清空缓冲区
        ;
}
Q4: 为什么 printf 不需要 &num 而 scanf 需要 &?

根本原因:C 的参数传递是「按值传递」(pass by value)。

函数需求传参方式
printf("%d", num)读取值来显示传值:把 num 的值复制一份传给函数
scanf("%d", &num)写入值到变量传地址:函数通过地址修改原变量

按值传递的含义——函数只能操作副本,无法修改原变量:

pass_by_value_demo.c
c
#include <stdio.h>

void try_modify(int x)
{
    x = 100;  // 只修改了局部副本 x
}

int main(void)
{
    int a = 5;
    try_modify(a);
    printf("%d\n", a);  // 输出 5,a 没有变!
    return 0;
}

要修改原变量,必须传递地址(指针),让函数通过地址间接写入:

pass_by_pointer.c
c
#include <stdio.h>

void real_modify(int *px)
{
    *px = 100;  // 通过指针解引用,修改原变量
}

int main(void)
{
    int a = 5;
    real_modify(&a);
    printf("%d\n", a);  // 输出 100
    return 0;
}

scanf 就是这个原理——它需要写入用户输入的值到变量中,所以必须知道变量的地址。printf 只需要读取变量的值来格式化输出,传值就够了。

Q5: num % 2 == 1 能正确判断所有奇数吗?

不能。 对负奇数,num % 2 返回 -1,而非 1

modulo_negative_odd.c
c
 7 % 21    // 正奇数,余数为 1
-7 % 2-1    // 负奇数,余数为 -1(C99 向零截断)

因此 num % 2 == 1-7 返回假(-1 != 1)——判断失败。

正确写法

  • 判断偶数:num % 2 == 0 ✓(无论正负,偶数余数总是 0)
  • 判断奇数:num % 2 != 0 ✓(余数为 1 或 -1,都不等于 0)

这是一个重要的教训:不要用 == 1 来判断奇数,用 != 0 才是安全的。这个原则也适用于其他取模场景——余数的符号总是跟随被除数。

parity_safe.c
c
// 安全判断
if (num % 2 == 0)      // 偶数:正负都安全
    printf("even\n");
else                   // 奇数:正负都安全(else 捕获所有 != 0 的情况)
    printf("odd\n");
Q6: if (a < b < c) 在 C 语言中是什么意思?

它不表示「b 在 a 和 c 之间」——而是一个运算符优先级导致的陷阱。

chained_comparison.c
c
int a = 1, b = 5, c = 3;

if (a < b < c)           // 不表示 1 < 5 < 3
    printf("true\n");
else
    printf("false\n");

很多人直觉地以为 a < b < c 等价于数学中的 a < b < c(即 bac 之间)。但在 C 语言中,<左结合的,实际计算过程是:

a < b < c
 (a < b) < c        // 先计算 a < b
 (1 < 5) < 3        // 1 < 5 为真 → 返回 1
 1 < 3              // 1 < 3 为真 返回 1
 条件为真!

所以 a < b < ca=1, b=5, c=3 的情况下返回真——但 b 显然不在 ac 之间(5 不在 1 和 3 之间)!

正确写法

chained_comparison_fix.c
c
if (a < b && b < c)    // 正确:b 在 a 和 c 之间
    printf("b is between a and c\n");

这是运算符优先级和结合性的经典案例。永远用 && 连接多个比较,不要写链式比较。


课后练习

  1. 猜数字游戏:由用户 A 输入一个秘密数字,然后用户 B 来猜。程序告诉 B 是「太大」还是「太小」,直到猜对为止。

    知识点提示if-else if-else 链、while 循环 + break

  2. 随机猜数:将上面的程序改为计算机随机产生数字。提示:rand()(需 #include <stdlib.h>)、srand(time(NULL))(需 #include <time.h>)。

    知识点提示rand() % N + 1 生成 1~N 的随机数

  3. 输入验证版:增加输入验证——如果用户输入的不是有效数字,提示重新输入。

    知识点提示:检查 scanf 返回值、用 while (getchar() != '\n') 清空缓冲区

  4. 判断闰年:编写程序,输入一个年份,判断并输出是否为闰年。闰年规则:能被 4 整除但不能被 100 整除,或者能被 400 整除。

    知识点提示:复合条件用 &&|| 组合、运算符优先级、括号明确意图

  5. 成绩等级转换:输入一个 0~100 的分数,输出对应的等级(A: 90~100, B: 80~89, C: 70~79, D: 60~69, F: 0~59)。需要验证输入是否在有效范围内。

    知识点提示if-else if-else 链、条件顺序的重要性、范围验证

练习1: 猜数字游戏
guessing_game.c
c
#include <stdio.h>

int main(void)
{
    int secret, guess;

    printf("Player A, enter a secret number: ");
    scanf("%d", &secret);

    printf("\n\n\n\n\n\n\n\n\n\n");  // 简单「清屏」
    printf("Secret number set! Player B guesses.\n\n");

    while (1)
    {
        printf("Player B, enter your guess: ");
        scanf("%d", &guess);

        if (guess > secret)
            printf("Too high! Try again.\n");
        else if (guess < secret)
            printf("Too low! Try again.\n");
        else
        {
            printf("Correct! The number is %d.\n", secret);
            break;
        }
    }

    return 0;
}

逻辑分析if-else if-else 链处理三种情况(太大、太小、猜对),while (1) 创建无限循环,break 确保猜对才退出。条件判断的顺序是任意的——三种情况互斥,任意顺序都正确。

练习2: 随机猜数
random_guessing.c
c
#include <stdio.h>
#include <stdlib.h>   // rand(), srand()
#include <time.h>     // time()

int main(void)
{
    int secret, guess;

    srand((unsigned int)time(NULL));   // 用当前时间戳设置随机种子
    secret = rand() % 100 + 1;         // 生成 1~100 的随机数

    printf("I have chosen a number between 1 and 100.\n\n");

    while (1)
    {
        printf("Enter your guess: ");
        scanf("%d", &guess);

        if (guess > secret)
            printf("Too high!\n");
        else if (guess < secret)
            printf("Too low!\n");
        else
        {
            printf("Correct! The number is %d.\n", secret);
            break;
        }
    }

    return 0;
}

关键点

  • srand(time(NULL)) 用当前时间戳初始化随机数生成器,确保每次运行产生不同的随机序列
  • rand() % 100 生成 0~99 的随机数,+ 1 映射到 1~100
  • 如果不调用 srandrand() 每次运行都产生相同的序列——不利于游戏体验
练习3: 输入验证版
guessing_with_validation.c
c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int secret, guess;
    int result;

    srand((unsigned int)time(NULL));
    secret = rand() % 100 + 1;

    printf("I have chosen a number between 1 and 100.\n\n");

    while (1)
    {
        printf("Enter your guess: ");

        result = scanf("%d", &guess);
        if (result != 1)
        {
            printf("Invalid input! Please enter a number.\n");
            while (getchar() != '\n')   // 清空缓冲区中的错误输入
                ;
            continue;                    // 重新开始循环
        }

        if (guess < 1 || guess > 100)
        {
            printf("Please enter a number between 1 and 100.\n");
            continue;
        }

        if (guess > secret)
            printf("Too high!\n");
        else if (guess < secret)
            printf("Too low!\n");
        else
        {
            printf("Correct! The number is %d.\n", secret);
            break;
        }
    }

    return 0;
}

输入验证要点

  1. 检查 scanf 返回值——!= 1 表示输入不是有效整数
  2. while (getchar() != '\n') 清空缓冲区,防止错误输入污染后续 scanf; 表示空循环体)
  3. continue 跳过本轮循环剩余代码,回到循环开始重新提示
  4. 范围验证——即使输入是数字,也检查是否在 1~100 内
  5. 先验证格式(scanf 返回值),再验证范围——顺序不能颠倒
练习4: 判断闰年
leap_year.c
c
#include <stdio.h>

int main(void)
{
    int year;

    printf("Enter a year: ");
    scanf("%d", &year);

    if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
        printf("%d is a leap year.\n", year);
    else
        printf("%d is not a leap year.\n", year);

    return 0;
}

条件解析

  • year % 4 == 0:能被 4 整除
  • year % 100 != 0:不能被 100 整除
  • && 连接:能被 4 整除不能被 100 整除
  • year % 400 == 0:能被 400 整除
  • || 连接:满足上面任一条件即为闰年

括号是必需的——&& 优先级高于 ||,但加括号让逻辑一目了然。

练习5: 成绩等级转换
grade_converter.c
c
#include <stdio.h>

int main(void)
{
    int score;

    printf("Enter score (0-100): ");
    if (scanf("%d", &score) != 1)
    {
        printf("Invalid input!\n");
        return 1;
    }

    if (score < 0 || score > 100)
    {
        printf("Score must be between 0 and 100.\n");
        return 1;
    }

    // 注意:条件从高到低排列,确保正确匹配
    if (score >= 90)
        printf("Grade: A\n");
    else if (score >= 80)
        printf("Grade: B\n");
    else if (score >= 70)
        printf("Grade: C\n");
    else if (score >= 60)
        printf("Grade: D\n");
    else
        printf("Grade: F\n");

    return 0;
}

要点

  1. 先验证输入格式(scanf 返回值),再验证范围
  2. if-else if 链的条件必须从高到低排列——如果先写 score >= 60,90 分也会匹配到 D
  3. else 捕获所有 < 60 的情况,无需显式写 score < 60
  4. 也可以用 switch(score / 10) 实现,但处理 A 等(90~100 跨两个十位数)不如 if-else 直观

参考资料

"Simplicity is prerequisite for reliability." — Edsger W. Dijkstra

Released under the MIT License.