Lesson 04: 判断奇偶 CNB
练习任务
编写一个 C 程序,读取用户输入的一个整数,判断并输出该数是奇数还是偶数。
期望输出:
num 7 is oddnum 12 is even提示:你需要用
scanf读取整数(回顾 Lesson 02——它和printf有什么区别?变量前是否需要&?),然后用if/else判断奇偶。关键思考:如何判断一个数是偶数?%运算符可以帮你得到除法的余数。
核心知识点
if/else基本语法 — 条件为真执行 if 分支,为假执行 else 分支;else 可选- C 语言的真假定义 — 0 为假,任何非 0 值为真;条件表达式可以是任意整型表达式
- 关系运算符
==、!=、<、<=、>、>=— 比较表达式返回int(1 或 0) - 逻辑运算符
&&、||、!— 与、或、非,短路求值规则 - 运算符优先级表 — 赋值
=优先级极低,==vs=的经典陷阱 - 格式化输入
scanf— 格式说明符、取地址符&、返回值检查 scanf与printf的核心区别 — 按值传递:为什么一个需要&一个不需要scanf常见陷阱 — 缓冲区残留、返回值检查、缓冲区溢出- 取模运算符
%— 整数除法余数,奇偶判断的核心工具 - 负数取模的 C99 行为 — 结果符号与被除数相同(向零截断)
- 位运算
&判断奇偶 —num & 1检查最低位,一种更底层的视角 - 二进制数制基础 — 二进制表示、最低位(LSB)决定奇偶性
if-else链与else if阶梯 — 多路分支的写法与本质(嵌套 if)- 悬空 else 问题 — else 与最近的未配对 if 结合,花括号消除歧义
- 三元条件运算符
?:— 表达式级别的 if-else 替代 - 嵌套 if 语句 — 条件中嵌套条件,层级化判断逻辑
- 花括号
{}的使用规则 — 单条语句 vs 复合语句,防御性编码习惯 - 复合条件 — 用
&&和||组合多个判断 - 逻辑运算符真值表 — 与、或、非的完整真值表
- 取反运算符
!— 逻辑反转的技巧与常见用途 - 浮点数比较 — 为什么不能用
==,精度误差与容差比较 switch语句简介 — 多路分支的另一种语法(预习)- 常见陷阱总览 —
=误写为==、遗漏花括号、条件反转、未初始化变量 if-else汇编实现 — 条件跳转指令、分支预测与流水线惩罚- 无分支编程简介 — 用查表替代条件跳转
代码框架
#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 (条件表达式)
语句1; // 条件为真时执行
else
语句2; // 条件为假时执行if/else 语句由三部分组成:
- 关键字
if:标记分支结构的开始 - 条件表达式:写在圆括号
( )中,决定走哪条路 - 两个分支:
if分支(条件为真时执行)和else分支(条件为假时执行)
程序的执行流是这样的:
进入 if/else
↓
计算 (条件表达式)
↓
┌─ 结果为非 0(真)──→ 执行 if 分支
│
└─ 结果为 0(假)────→ 执行 else 分支
↓
合流,继续执行后续代码三条关键规则:
- 条件表达式可以是任意整数值的表达式——C 语言没有专门的布尔类型(C99 之前),0 表示假,任何非 0 值表示真
else是可选的——当只需要处理「条件为真」这一种情况时,可以省略else- 多条语句必须用
{ }包裹——形成复合语句(compound statement),否则只有紧跟在if或else后的第一条语句属于该分支
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 中写任何整型表达式:
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> 可以使用 bool、true、false),但条件表达式的判定规则不变——仍然是 0 与非 0。true 只是整数 1 的宏定义,false 是 0 的宏定义。
#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 == 5 | 1(真) | 注意是双等号! |
!= | 不等于 | 5 != 3 | 1(真) | |
< | 小于 | 3 < 5 | 1(真) | |
<= | 小于等于 | 5 <= 5 | 1(真) | |
> | 大于 | 5 > 3 | 1(真) | |
>= | 大于等于 | 5 >= 5 | 1(真) |
关键要点:
- 所有关系运算符的返回值都是
int类型——1表示真,0表示假。没有专门的「布尔」类型。 ==是比较,=是赋值——这是两个完全不同的运算符,但外观相似,是 C 语言中最经典的陷阱。- 关系运算符的优先级低于算术运算符——
a + b < c * d等价于(a + b) < (c * d)。
#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 即假」的规则。
#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):
| A | B | A && B |
|---|---|---|
| 0(假) | 0(假) | 0 |
| 0(假) | 非 0(真) | 0 |
| 非 0(真) | 0(假) | 0 |
| 非 0(真) | 非 0(真) | 1 |
逻辑或 ||(OR):
| A | B | A || 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)——一旦结果确定,就不再计算后续表达式:
// && 短路:左边为假时,右边不执行
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");短路求值的两个重要应用:
- 安全检查:先判断指针/索引是否有效,再访问其内容——避免空指针解引用或数组越界
- 性能优化:将计算代价高的条件放在后面,利用短路减少不必要的计算
#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 取反运算符 ! 的使用技巧
! 将真变假、假变真。它有几个常见的使用模式:
// 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 → 0WARNING
! 和 ~(按位取反)是完全不同的运算符。!5 结果是 0(逻辑非),~5 结果是 -6(按位取反)。混淆两者会导致难以排查的逻辑错误。
2.5 复合条件:用 && 和 || 组合判断
将多个关系表达式用逻辑运算符连接,形成复合条件:
// 判断一个年份是否为闰年
// 规则:能被 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 语言中最经典的陷阱:
// 陷阱 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)#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 的函数签名与工作原理
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:
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 需要写入变量的值——必须知道变量的地址,通过指针间接写入。
用一个简单的例子来感受按值传递:
#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 返回成功匹配并赋值的项数。忽视返回值是危险的——如果用户输入的不是数字,程序会在「不知道变量有没有被正确赋值」的情况下继续运行:
#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;
}检查返回值的三个好处:
- 尽早发现输入错误,避免使用未正确初始化的变量
- 可以给用户友好的错误提示
- 为后续的输入验证和重试逻辑打下基础
4.4 scanf 的常见陷阱
陷阱 1:输入缓冲区残留
scanf("%d", &num) 读取整数后,会在缓冲区中留下换行符 \n。如果后续使用 scanf("%c", &ch) 读取字符,会直接读到这个残留的 \n:
scanf("%d", &num); // 用户输入 "7\n",%d 读取 7,\n 留在缓冲区
scanf("%c", &ch); // 直接读到残留的 \n,而非用户新输入的字符解决方法:在 %c 前加一个空格,告诉 scanf 跳过所有空白字符:
scanf("%d", &num);
scanf(" %c", &ch); // 空格跳过所有空白字符(空格、\t、\n 等)陷阱 2:匹配失败后不清空缓冲区
当 scanf("%d", &num) 遇到字母输入时,匹配失败,错误的输入留在缓冲区。后续的 scanf 会反复读到同一段错误输入,陷入死循环:
#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 不检查目标数组的大小,输入过长会溢出:
char name[10];
scanf("%s", name); // 输入超过 9 字符会溢出,破坏栈上其他数据
scanf("%9s", name); // 安全:限制最多读取 9 字符,留 1 位给 \04.5 常用格式说明符速查
| 格式符 | 类型 | 说明 |
|---|---|---|
%d | int | 有符号十进制整数 |
%u | unsigned int | 无符号十进制整数 |
%f | float | 浮点数(十进制) |
%lf | double | 双精度浮点数(注意:printf 中 %f 即可,但 scanf 中必须 %lf) |
%c | char | 单个字符 |
%s | char[] | 字符串(自动加 \0,不读空白) |
%x | unsigned int | 十六进制整数 |
%o | unsigned int | 八进制整数 |
IMPORTANT
scanf 和 printf 对 double 的格式符要求不同:printf 用 %f(float 和 double 都自动提升为 double),scanf 用 %lf(必须明确告诉它是 double 还是 float)。混用会导致未定义行为。
5. 取模运算符 %:奇偶判断的核心工具
5.1 取模的基本语义
% 返回整数除法的余数:
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(被除数)相同:
7 % 3 → 1 // 7 / 3 = 2(向零截断), 2 × 3 = 6, 7 - 6 = 1
-7 % 3 → -1 // -7 / 3 = -2(向零截断),-2 × 3 = -6,-7 -(-6) = -1
7 % -3 → 1 // 7 / -3 = -2(向零截断),-2 × -3 = 6, 7 - 6 = 1
-7 % -3 → -1 // -7 / -3 = 2(向零截断), 2 × -3 = -6,-7 -(-6) = -1WARNING
这意味着 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
#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 ? even6. 位运算判断奇偶:从二进制视角看世界
除了取模,还可以用位运算来判断奇偶:
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³ 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⁰ = 1、2¹ = 2、2² = 4……除了最低位 2⁰ = 1 是奇数外,其他所有位(2¹ = 2、2² = 4、2³ = 8……)都是偶数。一个数的奇偶性完全由最低位决定——最低位为 1,整体就是奇数;最低位为 0,整体就是偶数。
6.3 & 1 操作的原理
& 是按位与(bitwise AND)运算符——对两个操作数的每一位分别做 AND 运算:
按位与规则:
1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0num & 1 将 num 与二进制 ...0001 做按位与——只保留最低位,其他所有位都被清零:
15 (00001111) 44 (00101100)
& 1 (00000001) & 1 (00000001)
-------------- --------------
1 (00000001) 0 (00000000)
→ 奇数 → 偶数num & 1 的结果:奇数返回 1(非 0 → 真),偶数返回 0(假)——恰好与 C 语言的真假定义完美吻合。
6.4 位运算的其他用途
位运算在 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 % 2 | idiv(有符号除法指令) | ~20-40 时钟周期 |
num & 1 | and(按位与指令) | 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 (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。上面的代码等价于:
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 (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 就变得不明确:
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,必须用花括号明确界定:
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 语句
在 if 或 else 分支内部再使用 if/else,形成嵌套结构——这是构建复杂判断逻辑的基本方式:
// 判断三角形类型
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 中的使用有两种风格,各有优劣:
// 风格 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,不会因为忘记加花括号而改变代码逻辑。
// 看似在 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 是语句——它不产生值,不能嵌入表达式中。而 ?: 是表达式——它返回一个值:
// 语法:条件 ? 值为真时的结果 : 值为假时的结果
// 基本用法
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";适用场景:简单的二选一赋值或函数参数选择。不宜嵌套过深——嵌套三元运算符会严重损害可读性:
// 不推荐:嵌套三元运算符
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 链更清晰:
// 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 的关键特性:
- 表达式必须是整数类型(
int、char、enum等),不能是浮点数或字符串 case标签必须是编译时常量break用于跳出switch——遗漏break会导致「贯穿」(fall-through)到下一个casedefault处理所有未匹配的情况(可选,但推荐)
switch 的详细讲解将在后续课程中展开。目前只需知道:当有 3 个以上的等值分支时,switch 通常是比 if-else 链更好的选择。
8. 浮点数比较:为什么不能用 ==
8.1 浮点数的精度问题
float 和 double 使用 IEEE 754 标准表示,很多十进制小数无法精确表示为二进制浮点数——就像 1/3 无法精确表示为十进制小数一样:
#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)——检查两个浮点数之差的绝对值是否小于一个很小的阈值:
#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.0、2.0),否则始终使用容差比较。
9. 常见陷阱总览
学习 if/else 和条件判断时,以下是最高频的错误:
9.1 = 误写为 ==(或反之)
// 陷阱:条件中写成了赋值
if (num % 2 = 0) // 编译错误:不能对表达式赋值
if (num % 2 == 0) // 正确:比较
// 陷阱:条件中赋值而非比较
if (x = 5) // 赋值成功,返回 5(非 0),条件永远为真
if (x == 5) // 正确:比较9.2 遗漏花括号
if (condition)
do_something();
do_also(); // 这行不在 if 内!永远会执行9.3 条件反转
// 陷阱:把「奇数」写成 num % 2 == 1,负奇数返回 -1 不满足
if (num % 2 == 1) // 对 -7 为假
// 正确:
if (num % 2 != 0) // 对正负奇数都为真9.4 未初始化变量
int num; // 未初始化——垃圾值
if (num % 2 == 0) // 用垃圾值判断——结果不可预测
printf("even\n");9.5 多余的分号
if (condition); // 分号形成了空的 if 体
do_something(); // 这行永远执行,不受 if 控制TIP
使用编译器的 -Wall -Wextra 选项可以捕获上述大部分错误。养成「零警告编译」的习惯。
10. if-else 的底层实现:从 C 到汇编
理解 if/else 在 CPU 层面如何执行,有助于写出更高效的代码。
10.1 汇编层面的条件分支
if (num % 2 == 0)
printf("even\n");
else
printf("odd\n");编译器将这段 C 代码翻译为条件跳转指令(以 x86-64 为例):
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 结束,继续执行后续代码 ----核心模式:
- 计算条件:用算术或逻辑指令计算出条件的真/假值
- 条件跳转:根据结果决定跳转到哪个分支
- 执行分支:执行对应分支的代码
- 合流:两个分支通过无条件跳转
jmp汇合到同一点
10.2 分支预测与流水线惩罚
现代 CPU 使用流水线(pipeline)技术——同时处理多条指令的不同阶段,大幅提升吞吐量。但条件跳转打破了流水线的顺畅:
CPU 流水线(简化示意):
取指 → 译码 → 执行 → 访存 → 写回
取指 → 译码 → 执行 → 访存 → 写回
取指 → 译码 → 执行 → 访存 → 写回
↑ ↑
遇到 jnz 需要等条件计算结果出来才知道下一条指令是什么为了减少等待,CPU 使用分支预测器(Branch Predictor)来猜测跳转方向:
- 预测正确:流水线继续运行,无额外开销
- 预测错误:需要清空流水线中已部分执行但方向错误的指令,重新取指——约 10-20 时钟周期的惩罚
对于奇偶判断这种「输入随机、一半奇一半偶」的场景,分支预测命中率约 50%,惩罚频繁。
10.3 无分支编程简介
在性能极其敏感的代码中,可以用无分支编程(branchless programming)消除条件跳转:
// 常规写法(有分支)
if (num & 1)
printf("odd\n");
else
printf("even\n");
// 无分支写法:用数组查表
const char *messages[] = {"even", "odd"};
printf("%s\n", messages[num & 1]); // 无条件跳转,仅一次数组索引num & 1 只可能是 0 或 1——恰好作为数组下标。CPU 只需要计算下标、读取数组元素、调用 printf,全程无分支。
TIP
对于入门学习,优先关注代码可读性和正确性。无分支编程是高级优化技巧,在大多数应用场景中得不偿失。编译器在 -O2 优化下也会自动进行分支优化。理解其原理即可,不必刻意使用。
参考解答
奇偶判断(取模版本)
#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 判断偶数是安全的,因为无论正负偶数余数都是 0。else 分支覆盖了所有 != 0 的情况,即所有奇数。
位运算版本
#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 分别测试过吗?
课堂讨论
- 局部变量
num如果没有初始化值,那么默认的值是不是 0? scanf函数的第 2 个参数,如果不用&,会有什么后果?- 如果用户输入的不是数字(比如字母),程序会怎样?
- 想一想为什么
printf不需要&num,而scanf需要&? num % 2 == 1能正确判断所有奇数吗?负奇数呢?if (a < b < c)在 C 语言中是什么意思?它和我们的直觉一致吗?
讨论答案
Q1: 局部变量 num 如果没有初始化值,默认的值是不是 0?
不是。 局部变量的默认值是不确定的垃圾值,绝对不一定是 0。
#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)或未定义行为。
int num;
scanf("%d", num); // 传递垃圾值作为地址
scanf("%d", &num); // 正确:传递 num 的地址scanf 的第二个参数需要一个地址(指针),告诉它「把读到的值写到哪个内存位置」。
&num传递的是num的地址(如0x7fff1234)——scanf向这个地址写入num传递的是num当前的值——一个垃圾值(如32767)——scanf把32767当作内存地址,尝试向地址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 匹配失败,变量保持原值(未初始化则是垃圾值),错误输入留在缓冲区。
int num; // 未初始化,垃圾值
scanf("%d", &num); // 用户输入 "abc"实际发生的事:
scanf("%d", ...)期望读取十进制整数- 遇到字母
'a',无法解析为整数——匹配失败 scanf立即返回0(成功匹配了 0 项),不修改numnum保持原值——未初始化则仍是垃圾值"abc\n"完整保留在输入缓冲区中,后续任何scanf都会反复读到它
健壮的写法——检查 scanf 返回值:
#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;
}如果需要让用户重新输入直到正确为止,还需清空缓冲区:
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) | 要写入值到变量 | 传地址:函数通过地址修改原变量 |
按值传递的含义——函数只能操作副本,无法修改原变量:
#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;
}要修改原变量,必须传递地址(指针),让函数通过地址间接写入:
#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。
7 % 2 → 1 // 正奇数,余数为 1
-7 % 2 → -1 // 负奇数,余数为 -1(C99 向零截断)因此 num % 2 == 1 对 -7 返回假(-1 != 1)——判断失败。
正确写法:
- 判断偶数:
num % 2 == 0✓(无论正负,偶数余数总是 0) - 判断奇数:
num % 2 != 0✓(余数为 1 或 -1,都不等于 0)
这是一个重要的教训:不要用 == 1 来判断奇数,用 != 0 才是安全的。这个原则也适用于其他取模场景——余数的符号总是跟随被除数。
// 安全判断
if (num % 2 == 0) // 偶数:正负都安全
printf("even\n");
else // 奇数:正负都安全(else 捕获所有 != 0 的情况)
printf("odd\n");Q6: if (a < b < c) 在 C 语言中是什么意思?
它不表示「b 在 a 和 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(即 b 在 a 和 c 之间)。但在 C 语言中,< 是左结合的,实际计算过程是:
a < b < c
→ (a < b) < c // 先计算 a < b
→ (1 < 5) < 3 // 1 < 5 为真 → 返回 1
→ 1 < 3 // 1 < 3 为真 → 返回 1
→ 条件为真!所以 a < b < c 在 a=1, b=5, c=3 的情况下返回真——但 b 显然不在 a 和 c 之间(5 不在 1 和 3 之间)!
正确写法:
if (a < b && b < c) // 正确:b 在 a 和 c 之间
printf("b is between a and c\n");这是运算符优先级和结合性的经典案例。永远用 && 连接多个比较,不要写链式比较。
课后练习
猜数字游戏:由用户 A 输入一个秘密数字,然后用户 B 来猜。程序告诉 B 是「太大」还是「太小」,直到猜对为止。
知识点提示:
if-else if-else链、while循环 +break随机猜数:将上面的程序改为计算机随机产生数字。提示:
rand()(需#include <stdlib.h>)、srand(time(NULL))(需#include <time.h>)。知识点提示:
rand() % N + 1生成 1~N 的随机数输入验证版:增加输入验证——如果用户输入的不是有效数字,提示重新输入。
知识点提示:检查
scanf返回值、用while (getchar() != '\n')清空缓冲区判断闰年:编写程序,输入一个年份,判断并输出是否为闰年。闰年规则:能被 4 整除但不能被 100 整除,或者能被 400 整除。
知识点提示:复合条件用
&&和||组合、运算符优先级、括号明确意图成绩等级转换:输入一个 0~100 的分数,输出对应的等级(A: 90~100, B: 80~89, C: 70~79, D: 60~69, F: 0~59)。需要验证输入是否在有效范围内。
知识点提示:
if-else if-else链、条件顺序的重要性、范围验证
练习1: 猜数字游戏
#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: 随机猜数
#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- 如果不调用
srand,rand()每次运行都产生相同的序列——不利于游戏体验
练习3: 输入验证版
#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;
}输入验证要点:
- 检查
scanf返回值——!= 1表示输入不是有效整数 while (getchar() != '\n')清空缓冲区,防止错误输入污染后续scanf(;表示空循环体)continue跳过本轮循环剩余代码,回到循环开始重新提示- 范围验证——即使输入是数字,也检查是否在 1~100 内
- 先验证格式(
scanf返回值),再验证范围——顺序不能颠倒
练习4: 判断闰年
#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: 成绩等级转换
#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;
}要点:
- 先验证输入格式(
scanf返回值),再验证范围 if-else if链的条件必须从高到低排列——如果先写score >= 60,90 分也会匹配到 Delse捕获所有< 60的情况,无需显式写score < 60- 也可以用
switch(score / 10)实现,但处理 A 等(90~100 跨两个十位数)不如if-else直观
参考资料
- ISO C99 Standard, Section 6.5.5 — 取模运算符的定义与向零截断规则
- GNU C Library: Formatted Input (scanf) —
scanf的格式符与返回值详解 - x86 Assembly: Conditional Jumps — 条件跳转指令与分支预测
- C Operator Precedence (cppreference) — C 语言运算符优先级完整参考
- IEEE 754 Floating-Point Standard — 浮点数表示与精度限制
"Simplicity is prerequisite for reliability." — Edsger W. Dijkstra