013-C语言多文件编程

plana_lab 发布于 14 天前 110 次阅读




前面已经学习了:

结构体
动态内存
文件操作
预处理与头文件

当程序越来越大时,如果所有代码都写在:

main.c

里面,会越来越难维护。

例如一个学生管理系统可能有:

学生相关代码
文件相关代码
菜单相关代码
工具函数
主程序

如果全部塞进一个文件:

main.c

可能会变成几千行。

所以我们可以把程序拆成多个文件:

main.c
student.c
student.h
file.c
file.h

这就是:

多文件编程。


一、为什么要使用多个文件

假设我们有:

void hello() {
    printf("Hello\n");
}

如果只有一个函数当然没问题。

但是如果程序变成:

main()
student()
file()
menu()
config()
utils()

全部放在一个文件里:

main.c

代码会越来越难找。

可以拆成:

main.c
student.c
file.c

每个文件负责一部分功能。

例如:

main.c
↓
程序入口、菜单

student.c
↓
学生相关操作

file.c
↓
文件读写

这就是模块化。


二、.c 和 .h 分别是什么

多文件编程最重要的两个文件:

.c
.h

.c

通常用于:

函数实现。

例如:

void hello() {
    printf("Hello\n");
}

.h

通常用于:

声明和公共定义。

例如:

void hello();

可以简单记:

.h
↓
告诉别人“有什么”

.c
↓
真正实现“怎么做”

三、声明与定义

这个概念在多文件编程中非常重要。

例如:

int add(int a, int b);

这是:

声明。

意思是:

有一个 add 函数
它接收两个 int
返回一个 int

而:

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

这是:

定义。

真正实现这个函数。

所以:

声明
↓
告诉编译器存在这个东西

定义
↓
真正实现这个东西

四、最简单的多文件程序

先建立:

project/
├─ main.c
├─ hello.c
└─ hello.h

hello.h

#ifndef HELLO_H
#define HELLO_H

void hello();

#endif

这里是:

函数声明

hello.c

#include <stdio.h>
#include "hello.h"

void hello() {
    printf("Hello World!\n");
}

这里是:

函数定义

main.c

#include "hello.h"

int main() {
    hello();

    return 0;
}

这里调用:

hello();

五、编译多个 .c 文件

使用 GCC:

gcc main.c hello.c -o main

这里:

main.c
+
hello.c
↓
编译
↓
链接
↓
main

运行:

./main

Windows 下:

main.exe

输出:

Hello World!

六、为什么 main.c 能调用 hello()

因为:

#include "hello.h"

预处理后,main.c 可以看到:

void hello();

于是编译器知道:

hello()
存在

但是:

hello()

真正的实现在哪里?

在:

hello.c

所以最后需要:

链接。


七、编译和链接

多个 .c 文件并不是直接变成一个程序。

可以粗略理解为:

main.c
 ↓
预处理
 ↓
编译
 ↓
main.o

hello.c
 ↓
预处理
 ↓
编译
 ↓
hello.o

main.o + hello.o
 ↓
链接
 ↓
main

所以:

编译
↓
每个 .c 变成目标文件

链接
↓
把多个目标文件组合起来

八、为什么只编译 main.c 会报错

如果执行:

gcc main.c -o main

main.c 虽然通过:

#include "hello.h"

知道:

void hello();

但是 hello() 的实现不在 main.c。

所以链接阶段会找不到:

hello

于是出现:

undefined reference to `hello`

Windows + MinGW 下可能会看到类似:

undefined reference to `hello`

意思就是:

声明找到了,但实现没有链接进来。


九、头文件不是实现

例如:

hello.h

里面:

void hello();

只是告诉编译器:

有一个 hello()

真正实现:

void hello() {
    printf("Hello");
}

应该在:

hello.c

所以:

hello.h
↓
声明

hello.c
↓
定义

十、结构体也可以放进头文件

例如:

student.h

可以写:

#ifndef STUDENT_H
#define STUDENT_H

typedef struct {
    char name[50];
    int age;
    double score;
} Student;

void print_student(Student *p);
void add_score(Student *p, double score);

#endif

然后:

student.c

负责实现:

#include <stdio.h>
#include "student.h"

void print_student(Student *p) {
    printf("%s %d %.2f\n",
           p->name,
           p->age,
           p->score);
}

void add_score(Student *p, double score) {
    p->score += score;
}

然后:

main.c

只负责调用:

#include "student.h"

int main() {

    Student s = {
        "Alice",
        18,
        95.5
    };

    print_student(&s);
    add_score(&s, 2.0);
    print_student(&s);

    return 0;
}

十一、模块化

现在可以看到:

student.h
↓
Student 类型
函数声明

student.c
↓
学生功能实现

main.c
↓
程序入口

这种方式叫:

模块化。

以后还可以:

file.h
file.c

menu.h
menu.c

utils.h
utils.c

每一个模块负责自己的事情。


十二、extern

多文件编程中还有一个重要关键字:

extern

它用于声明:

某个变量在其他地方定义。

例如:

// config.h

extern int max_students;

这里没有真正创建:

max_students

只是声明:

有一个叫 max_students 的 int

十三、extern 的实际使用

目录:

project/
├─ main.c
├─ config.c
└─ config.h

config.h

#ifndef CONFIG_H
#define CONFIG_H

extern int max_students;

#endif

config.c

#include "config.h"

int max_students = 100;

这里:

int max_students = 100;

才是真正的:

定义。


main.c

#include <stdio.h>
#include "config.h"

int main() {

    printf("%d\n", max_students);

    return 0;
}

编译:

gcc main.c config.c -o main

输出:

100

十四、为什么 extern 有用

假设:

main.c
student.c
file.c

多个模块都需要:

max_students

可以把:

extern int max_students;

放到:

config.h

然后各个 .c:

#include "config.h"

即可使用。

但真正的变量定义:

int max_students = 100;

一般只放在一个 .c 文件里。


十五、为什么不能定义很多次

例如:

int max_students = 100;

同时出现在:

main.c
student.c
file.c

那么链接时会出现:

重复定义。

因为程序里出现了多个同名全局变量定义。

正确:

config.h
↓
extern int max_students;

config.c
↓
int max_students = 100;

十六、static

另一个很重要的关键字:

static

在多文件编程中,static 可以让一个全局函数或全局变量:

只在当前 .c 文件中可见。

例如:

static void helper() {
    printf("helper\n");
}

那么其他 .c 文件不能直接调用:

helper();

十七、为什么需要 static

假设:

student.c

里面有一个内部辅助函数:

static void check_score() {
    ...
}

这个函数只是:

student.c 自己内部使用

不希望其他模块访问。

那么可以:

static void check_score() {
    ...
}

这样可以减少模块之间的依赖。


十八、static 全局变量

例如:

static int count = 0;

表示这个变量只在当前 .c 文件中可见。

其他:

main.c

不能直接访问这个:

count

这也是模块化的重要手段。


十九、static 和普通全局变量

普通:

int count = 0;

可以被其他 .c 文件通过:

extern int count;

引用。

而:

static int count = 0;

只能在当前文件使用。

可以简单记:

普通全局变量
↓
可以跨文件使用

static 全局变量
↓
只属于当前 .c 文件

二十、static 函数

普通函数:

void helper() {
}

理论上可以被其他文件调用。

而:

static void helper() {
}

只允许当前 .c 文件使用。

例如:

student.c

static void check_age() {
    ...
}

void add_student() {
    check_age();
}

其他文件:

main.c

不能:

check_age();

二十一、头文件保护

多文件项目中经常会出现:

#include "student.h"

而不同头文件又互相包含。

因此需要:

#ifndef STUDENT_H
#define STUDENT_H

...

#endif

例如:

#ifndef STUDENT_H
#define STUDENT_H

typedef struct {
    char name[50];
    int age;
} Student;

#endif

这就是:

Include Guard。


二十二、一个完整的学生管理项目结构

现在把之前学的所有东西组合起来。

student_manager/
│
├─ main.c
│
├─ student.c
├─ student.h
│
├─ file.c
├─ file.h
│
└─ config.h

二十三、student.h

#ifndef STUDENT_H
#define STUDENT_H

typedef struct {
    char name[50];
    int age;
    double score;
} Student;

void print_student(Student *p);
void add_score(Student *p, double score);

#endif

负责:

Student 类型
函数声明

二十四、student.c

#include <stdio.h>
#include "student.h"

static void check_score(Student *p) {
    if (p->score < 0) {
        p->score = 0;
    }

    if (p->score > 100) {
        p->score = 100;
    }
}

void print_student(Student *p) {
    printf(
        "姓名: %s\n"
        "年龄: %d\n"
        "成绩: %.2f\n",
        p->name,
        p->age,
        p->score
    );
}

void add_score(Student *p, double score) {
    p->score += score;

    check_score(p);
}

这里:

static void check_score(...)

是内部函数。

只有:

student.c

自己可以使用。


二十五、file.h

#ifndef FILE_H
#define FILE_H

#include "student.h"

int save_students(
    const char *filename,
    Student *students,
    int count
);

int load_students(
    const char *filename,
    Student *students,
    int count
);

#endif

负责:

文件模块对外提供的函数声明

二十六、file.c

#include <stdio.h>
#include "file.h"

int save_students(
    const char *filename,
    Student *students,
    int count
) {
    FILE *fp = fopen(filename, "wb");

    if (fp == NULL) {
        return 0;
    }

    fwrite(
        students,
        sizeof(Student),
        count,
        fp
    );

    fclose(fp);

    return 1;
}

int load_students(
    const char *filename,
    Student *students,
    int count
) {
    FILE *fp = fopen(filename, "rb");

    if (fp == NULL) {
        return 0;
    }

    fread(
        students,
        sizeof(Student),
        count,
        fp
    );

    fclose(fp);

    return 1;
}

这里把上一章学的:

文件操作

封装起来了。


二十七、main.c

#include <stdio.h>
#include "student.h"
#include "file.h"

int main() {

    Student students[2] = {
        {"Alice", 18, 95.5},
        {"Bob", 19, 88.0}
    };

    add_score(&students[0], 2.0);

    for (int i = 0; i < 2; i++) {
        print_student(&students[i]);
        printf("\n");
    }

    save_students(
        "students.dat",
        students,
        2
    );

    return 0;
}

二十八、编译整个项目

使用:

gcc main.c student.c file.c -o student_manager

最终得到:

student_manager

Windows:

student_manager.exe

二十九、为什么这样设计

现在每个文件职责非常明确:

main.c
↓
程序入口

student.h
↓
学生模块接口

student.c
↓
学生模块实现

file.h
↓
文件模块接口

file.c
↓
文件模块实现

这样以后修改文件读写:

只需要改 file.c

修改学生逻辑:

只需要改 student.c

主程序不用塞满所有细节。


三十、接口和实现

多文件编程可以理解成:

.h
↓
接口(Interface)

.c
↓
实现(Implementation)

例如:

int save_students(...);

告诉外界:

我提供 save_students()

至于内部:

fopen
fwrite
fclose

怎么实现:

外部不需要知道

这就是封装。


三十一、为什么不直接 include .c

例如有人可能写:

#include "student.c"

通常不应该这么做。

推荐:

#include "student.h"

然后编译:

gcc main.c student.c -o main

原因是:

.h
↓
提供接口

.c
↓
单独编译

链接器
↓
连接实现

这才是正常的多文件编程方式。


三十二、编译过程再理解一次

假设:

main.c
student.c
student.h

首先:

main.c
↓
预处理
↓
#include "student.h"
↓
得到预处理后的 main.c

然后:

main.c
↓
编译
↓
main.o

同时:

student.c
↓
预处理
↓
编译
↓
student.o

最后:

main.o
+
student.o
↓
链接
↓
可执行文件

三十三、只修改一个 .c 文件

多文件编程还有一个很大的好处:

假设:

main.c
student.c
file.c

如果只修改:

student.c

理论上不需要重新处理所有源码。

可以:

student.c
↓
重新编译
↓
student.o

main.o
↓
继续使用

file.o
↓
继续使用

然后重新:

链接

这也是 Makefile 后面会解决的问题。


三十四、这就是为什么后面要学 Makefile

现在你可能写:

gcc main.c student.c file.c -o main

文件少的时候没关系。

但如果变成:

main.c
student.c
student.h
file.c
file.h
menu.c
menu.h
utils.c
utils.h
config.c
config.h

每次手动:

gcc ...

就很麻烦。

所以:

多文件编程
↓
Makefile

是非常自然的下一步。


三十五、本章练习

练习 1

创建:

main.c
hello.c
hello.h

让:

hello();

可以正常调用。


练习 2

创建:

math.c
math.h
main.c

实现:

int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div_int(int a, int b);

练习 3

创建:

student.c
student.h
main.c

把:

Student
print_student
add_score

全部模块化。


练习 4

创建:

config.c
config.h
main.c

使用:

extern int max_students;

让 main.c 可以访问 config.c 中定义的变量。


练习 5

在 student.c 中创建:

static void check_score(...)

让它只能在:

student.c

中使用。


练习 6

把你之前写的:

课程表程序

拆成:

main.c
course.c
course.h
file.c
file.h

其中:

course.c
↓
课程增删改查

file.c
↓
课程表保存和读取

main.c
↓
菜单

此作者没有提供个人介绍。
最后更新于 2026-09-18