c_snake_game

第 10 课:模块化编程 📦

将大型项目拆分成多个模块,每个模块负责特定功能。


10.1 为什么需要模块化?

想象你要写一个 10000 行的程序,全部放在一个文件里:

// ❌ 所有代码在一个文件
// main.c - 10000 行
// 找到 bug 试试?

模块化的好处:


10.2 头文件的作用

声明 vs 实现

// snake.h - 声明(接口)
#ifndef SNAKE_H
#define SNAKE_H

typedef struct Snake Snake;
Snake* snake_create(int x, int y);
void snake_move(Snake* s);
void snake_destroy(Snake* s);

#endif

// snake.c - 实现(具体代码)
#include "snake.h"

Snake* snake_create(int x, int y) {
    // 实际实现
}

void snake_move(Snake* s) {
    // 实际实现
}

10.3 头文件保护

防止头文件被重复包含:

// ❌ 没有保护
// 如果被包含两次,会重复定义

// ✅ 有保护
#ifndef SNAKE_H
#define SNAKE_H

// 内容只会被处理一次

#endif

10.4 我们的项目结构

src/
├── main.c              # 程序入口
├── game.c / game.h     # 游戏核心
├── snake.c / snake.h   # 蛇模块
├── food.c / food.h     # 食物模块
├── ui.c / ui.h         # 界面模块
├── input.c / input.h   # 输入模块
├── score.c / score.h   # 分数模块
└── utils.c / utils.h   # 工具函数

10.5 模块依赖关系

                    main.c
                      │
                      ↓
                   game.c
                  ↙  ↓  ↘
            snake.c  food.c  score.c
                │       │        │
                └───────┴────────┘
                        ↓
                     utils.c
                        │
                        ↓
                      ui.c
                        │
                        ↓
                    input.c

10.6 包含顺序

// main.c
#include <stdio.h>      // 标准库
#include <stdlib.h>     // 标准库
#include "game.h"       // 自己的头文件
#include "snake.h"
#include "ui.h"

规则:

  1. 先包含标准库
  2. 再包含自己的头文件
  3. 按依赖关系排序

10.7 编译多个文件

方法 1:一次性编译

gcc src/main.c src/snake.c src/food.c src/ui.c -o snake_game

方法 2:分步编译

# 编译每个 .c 文件为 .o 文件
gcc -c src/main.c -o obj/main.o
gcc -c src/snake.c -o obj/snake.o
gcc -c src/food.c -o obj/food.o

# 链接所有 .o 文件
gcc obj/main.o obj/snake.o obj/food.o -o snake_game

10.8 实战:创建模块

utils.h

#ifndef UTILS_H
#define UTILS_H

#include <stdbool.h>

// 点结构
typedef struct {
    int x;
    int y;
} Point;

// 函数声明
Point point_create(int x, int y);
bool point_equals(Point a, Point b);
int random_range(int min, int max);

#endif

utils.c

#include "utils.h"
#include <stdlib.h>
#include <time.h>

static int seeded = 0;

Point point_create(int x, int y) {
    Point p = {x, y};
    return p;
}

bool point_equals(Point a, Point b) {
    return a.x == b.x && a.y == b.y;
}

int random_range(int min, int max) {
    if (!seeded) {
        srand(time(NULL));
        seeded = 1;
    }
    return min + rand() % (max - min + 1);
}

snake.h

#ifndef SNAKE_H
#define SNAKE_H

#include "utils.h"

typedef struct Segment {
    Point position;
    struct Segment* next;
} Segment;

typedef struct Snake {
    Segment* head;
    Segment* tail;
    int length;
    int direction;
} Snake;

Snake* snake_create(int x, int y);
void snake_move(Snake* s);
void snake_destroy(Snake* s);

#endif

snake.c

#include "snake.h"
#include <stdlib.h>

Snake* snake_create(int x, int y) {
    Snake* snake = malloc(sizeof(Snake));
    Segment* head = malloc(sizeof(Segment));
    
    head->position = point_create(x, y);
    head->next = NULL;
    
    snake->head = head;
    snake->tail = head;
    snake->length = 1;
    snake->direction = 0;
    
    return snake;
}

void snake_move(Snake* s) {
    // 实现...
}

void snake_destroy(Snake* s) {
    Segment* current = s->head;
    while (current) {
        Segment* next = current->next;
        free(current);
        current = next;
    }
    free(s);
}

main.c

#include <stdio.h>
#include "snake.h"
#include "utils.h"

int main(void) {
    Snake* snake = snake_create(10, 10);
    
    printf("蛇创建成功!\n");
    printf("位置:(%d, %d)\n", 
           snake->head->position.x,
           snake->head->position.y);
    
    snake_destroy(snake);
    return 0;
}

10.9 常见错误

错误 1:循环包含

// a.h
#include "b.h"  // ❌ b.h 又包含 a.h

// b.h
#include "a.h"  // ❌ 循环依赖!

解决: 使用前向声明

// a.h
typedef struct B B;  // 前向声明,不包含 b.h

错误 2:在头文件中定义变量

// ❌ utils.h
int counter = 0;  // 每个包含的文件都会定义一次

// ✅ utils.h
extern int counter;  // 声明

// ✅ utils.c
int counter = 0;     // 定义

✅ 本课检查清单


📝 作业

  1. 将你的贪吃蛇代码拆分成多个模块

  2. 为每个模块创建对应的 .h.c 文件

  3. 使用 #ifndef 保护所有头文件


下一课:Makefile