# C 语言题目知识点梳理

> [!summary]
> 原有题目已按知识模块重新归类。本文侧重“为什么这样写”和“容易错在哪里”；可直接复用的代码见 [[计算机系/CLanguage/C语言常用算法模板.md]]。原笔记中混入的 Python 内容已移至 [[计算机系/Python/Python常用算法模板.md]]。

## 题目与知识点总览

| 题目类型 | 核心知识点 | 主要易错点 |
| --- | --- | --- |
| 随机四则运算 | `rand`、`srand`、`time`、分支结构 | 重复播种、除数为零、格式字符串错误 |
| 成绩等级判断 | 函数返回值、`if...else if` | 区间顺序错误、等级字母大小写不一致 |
| 最大公约数与分数约分 | 辗转相除法、递归、函数边界 | 对零取模、负数处理、分母为零 |
| 杨辉三角 | 二维数组、递推、边界初始化 | 数组越界、边界元素未赋值、排版空格混乱 |
| 冒泡/选择/插入排序 | 数组交换、循环不变量、复杂度 | 循环上下界写错、插入位置错误 |
| 顺序/二分查找 | 标志变量、区间收缩 | 二分前未排序、左右边界更新错误 |
| 字符串处理 | 字符数组、结束符、`ctype.h` | 使用 `gets`、计数器未重置、`else` 绑定错误 |
| 指针与数组求和 | 数组退化、指针算术、解引用 | 越界、混淆指针与指针所指的值 |
| 结构体综合题 | 赋值、传参、返回、结构体指针 | 值传递无法修改原对象、字符数组容量不足 |
| 动态数组 | `malloc`、空指针检查、`free` | 分配大小错误、内存泄漏、使用已释放内存 |
| 文件读写与复制 | `FILE *`、打开模式、字符/行/格式化 I/O | 路径转义、未检查打开结果、忘记关闭文件 |

## 1. 随机数与分支结构

### 1.1 随机数的基本用法

```c
#include <stdlib.h>
#include <time.h>

srand((unsigned)time(NULL));  // 通常只在程序开始时执行一次
int x = rand();
int y = rand() % 20;          // 得到 0～19
```

- `rand()` 产生伪随机整数；相同种子会产生相同序列。
- `srand()` 用来设置种子，不应放进频繁执行的循环中。
- `time(NULL)` 与 `time(0)` 在这里等价：二者都表示不保存返回的日历时间到指针参数中；习惯上写 `NULL` 更清楚。
- `rand() % n` 适合入门练习，但严格来说可能存在取模偏差，不适合高质量随机或安全场景。

### 1.2 `else if`、嵌套 `if` 与 `switch`

```c
if (op == 0) {
    /* ... */
} else if (op == 1) {
    /* ... */
} else {
    /* ... */
}
```

`else if` 本质上是 `else { if (...) ... }` 的紧凑写法。判断离散的整数或枚举值时，`switch` 往往更清楚；判断范围（如成绩等级）时，`if...else if` 更合适。

使用 `switch` 时应注意：

- 每个普通分支末尾通常需要 `break`，否则会继续执行后续分支。
- `default` 处理未预料的值。
- 除法分支必须先保证除数不为零。

### 1.3 成绩分级

应按阈值从高到低判断：

```c
char grade(int score)
{
    if (score >= 90) return 'A';
    if (score >= 80) return 'B';
    if (score >= 70) return 'C';
    if (score >= 60) return 'D';
    return 'E';
}
```

如果先写 `score >= 60`，那么 90 分也会在该分支提前结束。还应统一输出的大小写，并检查分数是否位于合理范围。

## 2. 函数、递归与最大公约数

### 2.1 参数与返回值

- C 语言函数参数默认按值传递。
- 修改普通形参不会影响实参。
- 若要修改调用者的数据，应传入地址，并在函数内通过指针访问。
- 结构体也能作为参数或返回值；结构体按值传参时会复制一份数据。

### 2.2 辗转相除法

核心关系为：

$$
\gcd(a,b)=\gcd(b,a\bmod b)
$$

当 `b == 0` 时结束并返回 `a`。把终止条件写成 `a % b == 0` 虽然在部分正常输入下可用，但 `b == 0` 时会发生未定义行为，因此直接判断 `b` 更稳妥。

递归与循环可以表达同一算法：

- 递归版本短，贴近数学定义；
- 循环版本不消耗递归调用栈，更便于控制。

### 2.3 分数约分与有理数平均值

设两个分数为 $a/b$ 和 $c/d$，则：

$$
\frac{a}{b}+\frac{c}{d}=\frac{ad+bc}{bd}
$$

处理分数时应维护以下不变量：

1. 分母不能为零；
2. 每次运算后用最大公约数约分，减小溢出风险；
3. 约定分母恒为正，负号放在分子；
4. 求 $n$ 个分数的平均值时，先求和，再把最终分母乘以 `n`；
5. `int` 很容易溢出，综合题中可优先使用 `long long`。

## 3. 数组与二维数组

### 3.1 一维数组

- 数组下标从 `0` 开始，长度为 `n` 时合法范围是 `0` 到 `n - 1`。
- 数组传给函数时会退化为指向首元素的指针，因此函数通常还需要单独接收长度。
- 访问 `a[i]` 等价于 `*(a + i)`。

### 3.2 杨辉三角

第 `i` 行第 `j` 个元素满足：

$$
a[i][j]=
\begin{cases}
1, & j=0\text{ 或 }j=i\\
a[i-1][j-1]+a[i-1][j], & 0<j<i
\end{cases}
$$

实现时先处理两侧边界，再计算内部元素。输出前应限制输入行数不超过数组容量。利用对称性只计算半行虽然能减少少量运算，但会增加边界复杂度；入门时优先选择完整递推版本。

## 4. 字符串与字符分类

### 4.1 字符串的本质

C 字符串是以 `\0` 结尾的字符数组。遍历字符串常写为：

```c
for (size_t i = 0; s[i] != '\0'; ++i) {
    /* 处理 s[i] */
}
```

数组必须留出存放 `\0` 的空间。例如要保存最多 10 个可见字符，至少需要 `char s[11]`。

### 4.2 大小写转换与字符判断

直接执行 `c -= 32` 依赖 ASCII 编码。更通用的做法是使用 `<ctype.h>`：

- `islower`、`isupper`：判断大小写字母；
- `isalpha`：判断字母；
- `isdigit`：判断数字；
- `isalnum`：判断字母或数字；
- `toupper`、`tolower`：转换大小写。

向这些函数传参时，稳妥写法是先转换为 `unsigned char`，避免负值导致未定义行为。

### 4.3 输入函数

- `gets` 无法限制长度，已从 C11 标准中移除，禁止使用。
- 读一整行优先使用 `fgets(buffer, sizeof buffer, stdin)`。
- `scanf("%80s", s)` 可限制单词长度，但遇到空白就停止。
- `scanf_s` 是可选的安全扩展，不是所有编译器都支持；其 `%s` 等格式通常还要求额外传入缓冲区大小。

### 4.4 密码合法性检查中的典型错误

原题涉及“长度、字母、数字、点号及非法字符”的分类。常见错误有：

1. 统计数组定义在多轮循环外，却没有在每个密码开始时清零；
2. 连续写多个独立 `if`，最后一个 `else` 只与最近的 `if` 配对；
3. 将“是否出现”写成复杂计数，其实布尔标志更直接；
4. 没覆盖“既无字母也无数字”的情况；
5. 输入字符串时未限制长度。

## 5. 指针

### 5.1 指针遍历数组

```c
int sum(const int *a, size_t n)
{
    int total = 0;
    for (const int *p = a; p < a + n; ++p) {
        total += *p;
    }
    return total;
}
```

- `p` 保存地址，`*p` 表示该地址中的值。
- `p++` 会移动到下一个同类型元素，而不是简单增加一个字节。
- 只读取数据时用 `const` 表明函数不会修改数组。
- 指针只能在同一数组及其尾后位置范围内进行有意义的比较和移动，不能解引用尾后指针 `a + n`。

### 5.2 指针数组与字符串排序

`const char *colors[]` 是“指针数组”，每个元素指向一个字符串。若排序时只交换指针，不会复制字符串内容，效率较高。字符串不能用 `<` 比较字典序，应使用 `strcmp`。

字符串字面量不应通过 `char *` 修改，因此推荐声明为 `const char *`。

## 6. 结构体

### 6.1 四种常见操作

```c
struct Student {
    int id;
    char name[20];
};
```

1. **整体赋值**：同类型结构体可以直接执行 `b = a;`，数组成员也会被复制。
2. **按值传参**：函数得到副本，修改副本不影响原对象。
3. **作为返回值**：函数可以返回一个结构体值。
4. **指针传参**：传入 `&stu`，函数中通过 `p->member` 修改原对象。

`p->id` 等价于 `(*p).id`。

### 6.2 结构体综合题

原题中的结构体应用可归纳为：

- 学生总分统计并记录最大值下标；
- 男女配对：用状态字段标记元素是否已使用；
- 有理数：把分子和分母组合成一个逻辑对象；
- 结构体数组：批量保存并遍历记录。

设计时应注意：

- 字符数组容量要包含末尾的 `\0`；
- 用 `%s` 输入时设置最大宽度；
- 状态字段最好使用有含义的名字，如 `used`，而不是 `q`；
- 同分如何处理、没有合法配对怎么办，都应由题意明确。

## 7. 排序与查找

### 7.1 三种基础排序

| 算法 | 核心操作 | 平均/最坏时间 | 额外空间 | 稳定性 |
| --- | --- | --- | --- | --- |
| 冒泡排序 | 相邻逆序元素交换 | $O(n^2)$ | $O(1)$ | 稳定 |
| 选择排序 | 每轮选出最小元素放到前面 | $O(n^2)$ | $O(1)$ | 通常不稳定 |
| 插入排序 | 把当前元素插入已排序区间 | $O(n^2)$ | $O(1)$ | 稳定 |

理解排序时要抓住“循环不变量”：

- 冒泡排序每轮结束后，未排序区间的最大元素到达末端；
- 选择排序每轮结束后，前缀区间已经放好最小元素；
- 插入排序开始处理 `a[i]` 前，`a[0..i-1]` 已有序。

### 7.2 顺序查找

从头到尾比较即可，不要求数组有序，时间复杂度为 $O(n)$。找到后可立即返回下标，不必同时维护 `flag` 和 `break`。

### 7.3 二分查找

二分查找的前提是数组已经按相同规则排序。闭区间写法维护 `[left, right]`：

- 循环条件：`left <= right`；
- 中点：`left + (right - left) / 2`；
- 目标较小：`right = mid - 1`；
- 目标较大：`left = mid + 1`。

时间复杂度为 $O(\log n)$。若数组无序，二分查找的结果没有保证。

## 8. 动态内存

```c
int *a = malloc((size_t)n * sizeof *a);
if (a == NULL) {
    /* 分配失败 */
}
/* 使用 a */
free(a);
a = NULL;
```

- `malloc` 返回 `void *`；在 C 语言中无需强制类型转换。
- `sizeof *a` 能避免类型改变后忘记同步修改分配表达式。
- 使用前检查返回值，使用完调用 `free`。
- `struct Student stu[n];` 属于变长数组（VLA），部分编译器不支持；需要跨编译器时可改用固定容量或动态分配。

## 9. 文件处理

### 9.1 基本流程

1. 用 `fopen` 打开文件；
2. 检查返回值是否为 `NULL`；
3. 执行读写；
4. 检查必要的读写结果；
5. 用 `fclose` 关闭文件。

常用模式：

| 模式 | 含义 |
| --- | --- |
| `"r"` | 只读，文件必须存在 |
| `"w"` | 只写，文件存在则清空，不存在则创建 |
| `"a"` | 追加写入 |
| `"rb"`、`"wb"` | 二进制读、写 |

### 9.2 I/O 函数层次

- 字符：`fgetc`、`fputc`；
- 行/字符串：`fgets`、`fputs`；
- 格式化：`fscanf`、`fprintf`；
- 二进制块：`fread`、`fwrite`。

复制文本文件时，可逐行执行 `fgets` 与 `fputs`；若要求原样复制任意文件，应使用二进制模式和 `fread`/`fwrite`。

### 9.3 Windows 路径与资源管理

反斜杠在 C 字符串中用于转义，因此路径应写为：

```c
"D:\\source.txt"
```

也可在 Windows 上写成 `"D:/source.txt"`。写完文件后若要重新以读模式打开，应先 `fclose` 原文件，再次 `fopen`；不能覆盖原指针后丢失尚未关闭的文件流。

## 10. 综合题的解题方法

### 10.1 学生最高总分

1. 设计结构体保存学号、姓名、各科成绩和总分；
2. 输入一条记录后立即计算总分；
3. 用下标记录当前最大值对应的学生；
4. 明确并列最大值时保留第一位还是最后一位。

### 10.2 配对问题

1. 用结构体保存类别、姓名和 `used` 状态；
2. 从一端选出未使用者；
3. 从另一端寻找类别互补且未使用者；
4. 成功配对后同时标记；
5. 若可能找不到配对，必须处理失败分支，不能默认数据永远合法。

### 10.3 分数平均值

1. 逐个读取并拒绝零分母；
2. 通分相加；
3. 每次相加后约分；
4. 求和结束后除以个数；
5. 统一分母符号并按整数、零或普通分数输出。

## 11. 原代码中的高频问题清单

- [ ] 代码块围栏是否成对，是否误把多个程序塞进同一个无法编译的代码块？
- [ ] 所有程序是否包含所需头文件？
- [ ] `main`、花括号和圆括号是否完整？
- [ ] `printf` 格式符与参数数量、类型是否匹配？
- [ ] 数组和字符串是否可能越界？
- [ ] 是否使用了已移除的 `gets`？
- [ ] 多组测试之间的计数器和标志是否重置？
- [ ] `else` 是否与预期的 `if` 配对？
- [ ] 二分查找前是否保证有序？
- [ ] 除法、取模和分数运算前是否排除了零？
- [ ] `malloc` 是否检查并最终 `free`？
- [ ] `fopen` 是否检查，所有成功打开的文件是否关闭？
- [ ] Windows 路径中的反斜杠是否正确转义？

## 12. 建议复习顺序

1. 基本输入输出、表达式和分支；
2. 循环与一维数组；
3. 函数、参数和返回值；
4. 字符串与二维数组；
5. 指针及数组传参；
6. 结构体与动态内存；
7. 排序、查找和递归；
8. 文件处理；
9. 用结构体、函数和算法完成综合题。
