理解 C 语言项目的组织方式,学会如何设计合理的文件结构。
想象一下,如果你把所有的代码都放在一个文件里:
// ❌ 糟糕的做法:main.c 有 2000 行
#include <stdio.h>
// 100 行变量定义
// 500 行方块逻辑
// 300 行碰撞检测
// 400 行渲染代码
// 200 行输入处理
// 500 行游戏循环
int main() { ... }
问题:
✅ 每个文件负责一个功能
✅ 快速找到需要的代码
✅ 多人可以并行开发
✅ 代码可以在其他项目复用
tetris/
├── src/ # 源代码目录
│ ├── main.c # 程序入口(约 80 行)
│ ├── game.c/h # 游戏核心逻辑(约 120 行)
│ ├── board.c/h # 游戏板管理(约 50 行)
│ ├── tetromino.c/h # 方块定义和操作(约 110 行)
│ ├── render.c/h # ncurses 渲染(约 230 行)
│ ├── input.c/h # 键盘输入处理(约 90 行)
│ └── score.c/h # 分数和最高分(约 130 行)
├── docs/ # 教程文档
├── data/ # 游戏数据
│ └── highscores.txt # 最高分记录
├── obj/ # 编译中间文件(自动生成)
├── Makefile # 构建配置
└── README.md # 项目说明
| 扩展名 | 用途 | 内容 |
|---|---|---|
.c |
源文件 | 函数的实现(具体代码) |
.h |
头文件 | 函数的声明(接口) |
.c 和 .h?头文件(.h)- 接口
// tetromino.h
#ifndef TETROMINO_H
#define TETROMINO_H
// 告诉编译器:有这么一个函数
void tetromino_init(Tetromino* t, TetrominoType type);
#endif
源文件(.c)- 实现
// tetromino.c
#include "tetromino.h"
// 实际代码
void tetromino_init(Tetromino* t, TetrominoType type) {
t->type = type;
t->x = BOARD_WIDTH / 2 - 2;
t->y = 0;
// ... 具体实现
}
好处:
.h 就能使用函数职责: 初始化、游戏主循环、清理资源
// main.c 核心结构
#include "game.h"
#include "render.h"
#include "input.h"
int main(void) {
// 1. 初始化
render_init();
load_high_scores();
Game* game = game_create();
game_init(game);
// 2. 游戏主循环
while (game->running) {
input_handle_game(game, input_get_key());
game_update(game);
game_render(game);
napms(50);
}
// 3. 清理
game_destroy(game);
render_cleanup();
return 0;
}
为什么这样设计?
main() 函数保持简洁职责: 游戏状态管理、方块生成、游戏逻辑更新
// game.h - 声明
typedef struct {
Board board; // 游戏板
Tetromino current; // 当前方块
Tetromino next; // 下一个方块
Tetromino ghost; // 影子方块
int score;
int level;
bool running;
bool game_over;
} Game;
Game* game_create(void);
void game_init(Game* game);
void game_update(Game* game);
void game_calculate_ghost(Game* game);
为什么需要 Game 结构体?
职责: 管理 20x10 的游戏网格、锁定方块、消除行
// board.h
#define BOARD_WIDTH 10
#define BOARD_HEIGHT 20
typedef struct {
int cells[BOARD_HEIGHT][BOARD_WIDTH];
} Board;
void board_init(Board* board);
void board_lock_tetromino(Board* board, Tetromino* t);
int board_clear_lines(Board* board);
为什么用二维数组?
cells[20][10] 直观表示游戏区域:
行 0 [0][0] [0][1] ... [0][9]
行 1 [1][0] [1][1] ... [1][9]
...
行 19 [19][0] [19][1] ... [19][9]
- 0 表示空格
- 1-7 表示不同颜色的方块
职责: 方块定义、移动、旋转
// tetromino.h
typedef enum {
TETRO_I, TETRO_O, TETRO_T, TETRO_S,
TETRO_Z, TETRO_J, TETRO_L
} TetrominoType;
typedef struct {
TetrominoType type;
int x, y; // 位置
int shape[4][4]; // 4x4 形状矩阵
} Tetromino;
void tetromino_init(Tetromino* t, TetrominoType type);
void tetromino_rotate(Tetromino* t);
为什么用 4x4 矩阵?
职责: 使用 ncurses 绘制游戏界面
// render.h
void render_init(void); // 初始化 ncurses
void render_cleanup(void); // 清理
void render_game(Game* game); // 绘制整个游戏
void render_ghost(Tetromino* t); // 绘制影子
为什么单独分离渲染模块?
职责: 读取键盘输入、转换为游戏操作
// input.h
void input_handle_game(Game* game, int key);
为什么需要输入处理模块?
职责: 计算分数、管理最高分记录
// score.h
int score_calculate_lines(int lines, int level);
void save_high_score(const char* name, int score, ...);
void load_high_scores(void);
main.c
│
┌────────────┼────────────┐
↓ ↓ ↓
game.c render.c input.c
│ │ │
┌────┴────┐ │ │
↓ ↓ │ │
board.c tetromino.c│ │
│ │ │ │
└────┬────┘ │ │
↓ ↓ ↓
score.c
依赖规则:
main.c 依赖所有模块game.c 依赖 board.c 和 tetromino.c# 1. 定义变量
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -Isrc -O2
LDFLAGS = -lncurses
# 2. 查找所有源文件
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:src/%.c=obj/%.o)
# 3. 最终目标
tetris: $(OBJS)
$(CC) $(OBJS) -o tetris $(LDFLAGS)
# 4. 编译规则
obj/%.o: src/%.c
$(CC) $(CFLAGS) -c $< -o $@
# 5. 清理
clean:
rm -rf obj/*.o tetris
关键概念:
| 语法 | 含义 |
|---|---|
$(VAR) |
引用变量 |
$< |
第一个依赖文件 |
$@ |
目标文件 |
wildcard |
通配符匹配文件 |
# 查看详细编译过程
make clean
make -n # -n 表示只显示命令,不执行
# 在 main.c 的 main() 函数开头添加:
printf("游戏启动!\n");
# 重新编译运行
make
make run
# 统计所有 .c 文件的行数
wc -l src/*.c
# 输出示例:
80 src/main.c
120 src/game.c
...
900 total
.c 和 .h 的区别打开 src/game.h,找出 Game 结构体有哪些成员
在 src/ 目录中,哪个文件负责绘制游戏界面?
如果要添加”音效”功能,应该创建什么文件?放在哪里?
下一课:数据结构