这一部分把进阶指针、字符串函数重构、变量生命周期、文件读写和控制台项目放在一起整理。它更像一章“把 C 语言基础变成项目能力”的复盘:既要懂指针,也要能把数据保存下来,还要能用链表或结构体维护程序状态。

4.0 指针数组步长

指针数组是“数组里的每个元素都是指针”。常见于字符串列表、菜单项、回调表。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void) {
const char *items[] = {"add", "delete", "search"};

for (int i = 0; i < 3; ++i) {
printf("%s\n", items[i]);
}

return 0;
}

复盘重点:items 是数组,items[i] 是指针,items[i][0] 才是字符。

4.1 指针差值计算

两个指针相减得到的是元素个数差,不是字节差。前提是它们指向同一个数组范围。

1
2
3
4
int nums[5] = {1, 2, 3, 4, 5};
int *a = &nums[1];
int *b = &nums[4];
printf("%td\n", b - a); // 3

4.2 指针基本特性

指针保存地址;指针类型决定解引用方式和步长;指针是否有效由生命周期和边界决定。

特性 复盘问题
地址 指向哪里
类型 按几个字节解释
步长 p + 1 前进多少
生命周期 指向对象是否还活着
权限 目标内存能否读写

4.3 指针数组

指针数组适合维护多个字符串或多个对象地址。安全边界是:数组本身有长度,每个指针也要保证非空。

1
2
3
4
5
6
7
void print_names(const char *names[], int count) {
for (int i = 0; i < count; ++i) {
if (names[i] != NULL) {
puts(names[i]);
}
}
}

4.4 数组指针

数组指针是“指向数组的指针”。它常用于二维数组传参。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

void print_matrix(int (*matrix)[3], int rows) {
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < 3; ++c) {
printf("%d ", matrix[r][c]);
}
printf("\n");
}
}

int (*matrix)[3] 表示指向“含 3 个 int 的数组”的指针。

4.5 指针函数

指针函数是“返回指针的函数”。返回值必须指向仍然有效的内存,不能返回局部变量地址。

1
2
3
4
5
6
const char *status_text(int code) {
if (code == 0) {
return "ok";
}
return "error";
}

4.6 函数指针

函数指针保存函数入口地址,常用于回调、菜单分发和策略表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

typedef int (*CalcFn)(int, int);

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main(void) {
CalcFn fn = add;
printf("%d\n", fn(3, 2));
fn = sub;
printf("%d\n", fn(3, 2));
return 0;
}

复盘重点:函数指针类型必须和真实函数签名一致。

4.7 字符与字符串

字符是单个值,字符串是以 \0 结尾的字符数组。字符串处理最大风险是边界和终止符。

内容 说明
'A' 字符常量
"A" 两个字符:A\0
char * 指向字符序列
char[] 字符数组,可作为缓冲区

4.8 重构 sizeof

sizeof 是编译期运算符,不是函数。课程里重构更多是为了理解数组、指针和类型大小的差异。

1
2
3
int nums[4];
printf("%zu\n", sizeof(nums)); // 整个数组大小
printf("%zu\n", sizeof(nums[0])); // 单个元素大小

4.9 重构 strlen

strlen 从字符串首地址开始数,直到遇到 \0。如果没有终止符,就会继续读到未知区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stddef.h>

size_t my_strlen(const char *s) {
if (s == NULL) {
return 0;
}

size_t len = 0;
while (s[len] != '\0') {
++len;
}
return len;
}

5.0 重构 strcmp

字符串比较不是只看长度,而是逐字符比较 ASCII/编码值,直到出现差异或都到结尾。

1
2
3
4
5
6
7
int my_strcmp(const char *a, const char *b) {
while (*a != '\0' && *a == *b) {
++a;
++b;
}
return (unsigned char)*a - (unsigned char)*b;
}

5.1 重构 strcat

拼接字符串一定要关心目标缓冲区容量。标准 strcat 不知道容量,项目里更推荐带长度的安全写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stddef.h>

int my_append(char *dst, size_t cap, const char *src) {
size_t len = my_strlen(dst);
size_t i = 0;

if (len >= cap) {
return 0;
}

while (src[i] != '\0' && len + i + 1 < cap) {
dst[len + i] = src[i];
++i;
}
dst[len + i] = '\0';
return src[i] == '\0';
}

5.2 局部变量生命周期

局部变量在函数栈帧中存在,函数返回后就不应该继续访问它的地址。

错误示例:

1
2
3
4
int *bad(void) {
int value = 10;
return &value;
}

5.3 游戏基址的秘密

这节可以理解为“程序运行时地址不是固定不变的”。模块基址、偏移、动态分配地址、ASLR 都会影响地址稳定性。复盘时只记录授权样本中的地址关系,不把它用于未授权修改。

5.4 重复包含定义机制

头文件要用 include guard 或 #pragma once 防止重复包含。

1
2
3
4
5
6
7
8
9
#ifndef STUDENT_H
#define STUDENT_H

typedef struct Student {
int id;
int score;
} Student;

#endif

5.5 静态变量特性

static 在不同位置含义不同:局部静态变量延长生命周期;文件作用域 static 限制链接范围。

5.6 寄存器变量

register 现在更多是历史语法,编译器会自己决定优化。复盘重点是理解“变量不一定真的长期存在内存中”,优化后可能在寄存器里。

5.7 枚举变量

枚举适合表达有限状态,比裸整数更清楚。

1
2
3
4
5
typedef enum MenuAction {
ACTION_ADD = 1,
ACTION_DELETE = 2,
ACTION_EXIT = 0
} MenuAction;

5.8 共享内存

共享内存用于多个进程访问同一段数据。安全复盘时要关注权限、同步、边界和数据一致性。

5.9 自定义数据类型

typedef 可以给复杂类型起别名,尤其是结构体、函数指针和回调类型。

6.0 结构体别名

结构体别名让项目代码更简洁,但不要为了短而牺牲语义。

6.1 函数指针类型

函数指针类型可以让菜单分发更清楚:

1
typedef void (*MenuHandler)(void);

6.2 文件数据样式

文件可以按文本看,也可以按二进制看。文本适合日志和配置,二进制适合结构化数据、图片、程序文件等。

6.3 文件指针数据

FILE * 表示一个打开的文件对象。打开后要检查,使用后要关闭。

1
2
3
4
FILE *fp = fopen("data.txt", "r");
if (fp != NULL) {
fclose(fp);
}

6.4 文件数据交互

文件读写要处理失败、边界和换行。不要默认读取一定成功。

6.5 文件内容读写

文本读写常用 fgets,二进制读写常用 fread / fwrite。任何读取都要检查返回值。

6.6 文件编码处理

编码问题会影响字符串长度、显示和解析。ASCII、GBK、UTF-8 在中文场景下尤其要区分。

14.7 - 15.8 项目:控制台游戏实践

这部分项目把前面的函数、结构体、链表、控制台输出和流程控制串起来。复盘时不用追求界面多花,重点是把项目拆清楚:

模块 复盘重点
控制台框架 窗口大小、刷新、输入处理
地图绘制 二维数组或坐标系统
节点管理 单链表维护对象
随机对象 随机数、边界和重复位置
操作流程 输入、移动、碰撞、死亡判断
运行检测 每一步都有可观察状态

学习检查清单

检查项 状态
能区分指针数组、数组指针、指针函数和函数指针 待复盘
能手写 strlenstrcmp 的基本版本并说明边界 待复盘
能说明局部变量地址为什么不能返回 待复盘
能用结构体和函数指针组织菜单逻辑 待复盘
能完成文件打开、读写、关闭和错误检查 待复盘