前面已经学习了:
结构体
动态内存
文件操作
预处理与头文件
当程序越来越大时,如果所有代码都写在:
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
运行:
./mainWindows 下:
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 mainmain.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;
#endifconfig.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
↓
菜单
Comments NOTHING